Cómo eliminar defectos de convexidad en un cuadrado de Sudoku?


Estaba haciendo un proyecto divertido: Resolver un Sudoku a partir de una imagen de entrada usando OpenCV (como en Google goggles, etc.). Y he completado la tarea, pero al final me encontré con un pequeño problema para el que vine aquí.

Hice la programación usando Python API de OpenCV 2.3.1.

A continuación está lo que hice:

  1. Lea la imagen
  2. Encuentra los contornos
  3. Seleccione la que tenga el área máxima, ( y también algo equivalente a cuadrado).
  4. Encuentra la esquina punto.

    Por ejemplo, se indica a continuación:

    introduzca la descripción de la imagen aquí

    (Observe aquí que la línea verde coincide correctamente con el límite verdadero del Sudoku, por lo que el Sudoku se puede deformar correctamente. Compruebe la siguiente imagen)

  5. Deformar la imagen a un cuadrado perfecto

    Por ejemplo, imagen:

    introduzca la descripción de la imagen aquí

  6. Realizar OCR (para lo cual utilicé el método que he dado en OCR de Reconocimiento de Dígitos Simple en OpenCV-Python )

Y el método funcionó bien.

Problema:

Mira esta imagen.

Realizando el paso 4 en esta imagen se obtiene el siguiente resultado:

introduzca la descripción de la imagen aquí

La línea roja dibujada es el contorno original que es el verdadero contorno del límite del sudoku.

La línea verde dibujada es un contorno aproximado que será el contorno de la imagen deformada.

Que por supuesto, hay diferencia entre la línea verde y la línea roja en el borde superior del sudoku. Así que mientras warping, no entiendo el límite original del Sudoku.

Mi Pregunta :

¿Cómo puedo deformar la imagen en el límite correcto del Sudoku, es decir, la línea roja O cómo puedo eliminar la diferencia entre la línea roja y la línea verde? ¿Hay algún método para esto en OpenCV?

Author: Community, 2012-04-17

4 answers

Tengo una solución que funciona, pero tendrás que traducirla tú mismo a OpenCV. Está escrito en Mathematica.

El primer paso es ajustar el brillo de la imagen, dividiendo cada píxel con el resultado de una operación de cierre:

src = ColorConvert[Import["http://davemark.com/images/sudoku.jpg"], "Grayscale"];
white = Closing[src, DiskMatrix[5]];
srcAdjusted = Image[ImageData[src]/ImageData[white]]

introduzca la descripción de la imagen aquí

El siguiente paso es encontrar el área del sudoku, para que pueda ignorar (enmascarar) el fondo. Para eso, utilizo el análisis de componentes conectados y selecciono el componente que tiene el convexo más grande superficie:

components = 
  ComponentMeasurements[
    ColorNegate@Binarize[srcAdjusted], {"ConvexArea", "Mask"}][[All, 
    2]];
largestComponent = Image[SortBy[components, First][[-1, 2]]]

introduzca la descripción de la imagen aquí

Rellenando esta imagen, obtengo una máscara para la cuadrícula del sudoku:

mask = FillingTransform[largestComponent]

introduzca la descripción de la imagen aquí

Ahora, puedo usar un filtro derivado de 2do orden para encontrar las líneas verticales y horizontales en dos imágenes separadas:

lY = ImageMultiply[MorphologicalBinarize[GaussianFilter[srcAdjusted, 3, {2, 0}], {0.02, 0.05}], mask];
lX = ImageMultiply[MorphologicalBinarize[GaussianFilter[srcAdjusted, 3, {0, 2}], {0.02, 0.05}], mask];

introduzca la descripción de la imagen aquí

Utilizo de nuevo el análisis de componentes conectados para extraer las líneas de cuadrícula de estas imágenes. Las líneas de la cuadrícula son mucho más largas que los dígitos, por lo que puedo usar la longitud de la pinza para seleccionar solo los componentes conectados a las líneas de la cuadrícula. Ordenándolas por posición, obtengo imágenes de máscara 2x10 para cada una de las líneas de cuadrícula vertical/horizontal en la imagen:

verticalGridLineMasks = 
  SortBy[ComponentMeasurements[
      lX, {"CaliperLength", "Centroid", "Mask"}, # > 100 &][[All, 
      2]], #[[2, 1]] &][[All, 3]];
horizontalGridLineMasks = 
  SortBy[ComponentMeasurements[
      lY, {"CaliperLength", "Centroid", "Mask"}, # > 100 &][[All, 
      2]], #[[2, 2]] &][[All, 3]];

introduzca la descripción de la imagen aquí

A continuación tomo cada par de líneas de cuadrícula vertical/horizontal, las dilato, calculo la intersección píxel por píxel y calculo el centro del resultado. Estos puntos son las intersecciones de la línea de cuadrícula:

centerOfGravity[l_] := 
 ComponentMeasurements[Image[l], "Centroid"][[1, 2]]
gridCenters = 
  Table[centerOfGravity[
    ImageData[Dilation[Image[h], DiskMatrix[2]]]*
     ImageData[Dilation[Image[v], DiskMatrix[2]]]], {h, 
    horizontalGridLineMasks}, {v, verticalGridLineMasks}];

introduzca la descripción de la imagen aquí

El último paso es definir dos funciones de interpolación para el mapeo X/Y a través de estos puntos, y transformar la imagen usando estas funciones:

fnX = ListInterpolation[gridCenters[[All, All, 1]]];
fnY = ListInterpolation[gridCenters[[All, All, 2]]];
transformed = 
 ImageTransformation[
  srcAdjusted, {fnX @@ Reverse[#], fnY @@ Reverse[#]} &, {9*50, 9*50},
   PlotRange -> {{1, 10}, {1, 10}}, DataRange -> Full]

introduzca la descripción de la imagen aquí

Todas las operaciones son una función básica de procesamiento de imágenes, por lo que esto también debería ser posible en OpenCV. La transformación de imágenes basada en splines puede ser más difícil, pero no creo que realmente la necesites. Probablemente el uso de la transformación de perspectiva que utiliza ahora en cada celda individual dará resultados lo suficientemente buenos.

 218
Author: Niki,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-04-20 07:21:37

La respuesta de Nikie resolvió mi problema, pero su respuesta estaba en Mathematica. Así que pensé que debería dar su adaptación OpenCV aquí. Pero después de implementar pude ver que el código OpenCV es mucho más grande que el código de Mathematica de Nikie. Y también, no pude encontrar el método de interpolación hecho por nikie en OpenCV (aunque se puede hacer usando scipy, lo diré cuando llegue el momento.)

1. Preprocesamiento de imagen (operación de cierre)

import cv2
import numpy as np

img = cv2.imread('dave.jpg')
img = cv2.GaussianBlur(img,(5,5),0)
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
mask = np.zeros((gray.shape),np.uint8)
kernel1 = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(11,11))

close = cv2.morphologyEx(gray,cv2.MORPH_CLOSE,kernel1)
div = np.float32(gray)/(close)
res = np.uint8(cv2.normalize(div,div,0,255,cv2.NORM_MINMAX))
res2 = cv2.cvtColor(res,cv2.COLOR_GRAY2BGR)

Resultado :

Resultado del cierre

2. Encontrar un Cuadrado de Sudoku y Crear una Imagen de Máscara

thresh = cv2.adaptiveThreshold(res,255,0,1,19,2)
contour,hier = cv2.findContours(thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)

max_area = 0
best_cnt = None
for cnt in contour:
    area = cv2.contourArea(cnt)
    if area > 1000:
        if area > max_area:
            max_area = area
            best_cnt = cnt

cv2.drawContours(mask,[best_cnt],0,255,-1)
cv2.drawContours(mask,[best_cnt],0,0,2)

res = cv2.bitwise_and(res,mask)

Resultado:

introduzca la descripción de la imagen aquí

3. Encontrar líneas verticales

kernelx = cv2.getStructuringElement(cv2.MORPH_RECT,(2,10))

dx = cv2.Sobel(res,cv2.CV_16S,1,0)
dx = cv2.convertScaleAbs(dx)
cv2.normalize(dx,dx,0,255,cv2.NORM_MINMAX)
ret,close = cv2.threshold(dx,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)
close = cv2.morphologyEx(close,cv2.MORPH_DILATE,kernelx,iterations = 1)

contour, hier = cv2.findContours(close,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
for cnt in contour:
    x,y,w,h = cv2.boundingRect(cnt)
    if h/w > 5:
        cv2.drawContours(close,[cnt],0,255,-1)
    else:
        cv2.drawContours(close,[cnt],0,0,-1)
close = cv2.morphologyEx(close,cv2.MORPH_CLOSE,None,iterations = 2)
closex = close.copy()

Resultado:

introduzca la descripción de la imagen aquí

4. Encontrar líneas horizontales

kernely = cv2.getStructuringElement(cv2.MORPH_RECT,(10,2))
dy = cv2.Sobel(res,cv2.CV_16S,0,2)
dy = cv2.convertScaleAbs(dy)
cv2.normalize(dy,dy,0,255,cv2.NORM_MINMAX)
ret,close = cv2.threshold(dy,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)
close = cv2.morphologyEx(close,cv2.MORPH_DILATE,kernely)

contour, hier = cv2.findContours(close,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
for cnt in contour:
    x,y,w,h = cv2.boundingRect(cnt)
    if w/h > 5:
        cv2.drawContours(close,[cnt],0,255,-1)
    else:
        cv2.drawContours(close,[cnt],0,0,-1)

close = cv2.morphologyEx(close,cv2.MORPH_DILATE,None,iterations = 2)
closey = close.copy()

Resultado:

introduzca la descripción de la imagen aquí

Por supuesto, este no es tan bueno.

5. Encontrar Puntos de Cuadrícula

res = cv2.bitwise_and(closex,closey)

Resultado:

introduzca la descripción de la imagen aquí

6. Correcto los defectos

Aquí, Nikie hace algún tipo de interpolación, sobre la cual no tengo mucho conocimiento. Y no pude encontrar ninguna función correspondiente para este OpenCV. (puede ser que esté ahí, no lo sé).

Echa un vistazo a este SOF que explica cómo hacer esto usando SciPy, que no quiero usar: Transformación de imagen en OpenCV

Entonces, aquí tomé 4 esquinas de cada sub-cuadrado y apliqué perspectiva de urdimbre a cada uno.

Para eso, primero encontramos la centroids.

contour, hier = cv2.findContours(res,cv2.RETR_LIST,cv2.CHAIN_APPROX_SIMPLE)
centroids = []
for cnt in contour:
    mom = cv2.moments(cnt)
    (x,y) = int(mom['m10']/mom['m00']), int(mom['m01']/mom['m00'])
    cv2.circle(img,(x,y),4,(0,255,0),-1)
    centroids.append((x,y))

Pero los centroides resultantes no se ordenarán. Echa un vistazo a la imagen de abajo para ver su orden:

introduzca la descripción de la imagen aquí

Así que los ordenamos de izquierda a derecha, de arriba a abajo.

centroids = np.array(centroids,dtype = np.float32)
c = centroids.reshape((100,2))
c2 = c[np.argsort(c[:,1])]

b = np.vstack([c2[i*10:(i+1)*10][np.argsort(c2[i*10:(i+1)*10,0])] for i in xrange(10)])
bm = b.reshape((10,10,2))

Ahora ver a continuación su orden:

introduzca la descripción de la imagen aquí

Finalmente aplicamos la transformación y creamos una nueva imagen de tamaño 450x450.

output = np.zeros((450,450,3),np.uint8)
for i,j in enumerate(b):
    ri = i/10
    ci = i%10
    if ci != 9 and ri!=9:
        src = bm[ri:ri+2, ci:ci+2 , :].reshape((4,2))
        dst = np.array( [ [ci*50,ri*50],[(ci+1)*50-1,ri*50],[ci*50,(ri+1)*50-1],[(ci+1)*50-1,(ri+1)*50-1] ], np.float32)
        retval = cv2.getPerspectiveTransform(src,dst)
        warp = cv2.warpPerspective(res2,retval,(450,450))
        output[ri*50:(ri+1)*50-1 , ci*50:(ci+1)*50-1] = warp[ri*50:(ri+1)*50-1 , ci*50:(ci+1)*50-1].copy()

Resultado:

introduzca la descripción de la imagen aquí

El resultado es casi el mismo que el de nikie, pero la longitud del código es grande. Puede ser, mejores métodos son disponible por ahí, pero hasta entonces, esto funciona bien.

Saludos ARK.

 183
Author: Abid Rahman K,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 10:31:10

Podría intentar usar algún tipo de modelado basado en cuadrícula de su deformación arbitraria. Y como el sudoku ya es una cuadrícula, no debería ser demasiado difícil.

Así que podrías intentar detectar los límites de cada subregión 3x3 y luego deformar cada región individualmente. Si la detección tiene éxito le daría una mejor aproximación.

 5
Author: sietschie,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-04-18 06:54:09

Quiero agregar que el método anterior solo funciona cuando el tablero de sudoku está recto, de lo contrario, la prueba de relación altura/ancho (o viceversa) probablemente fallará y no podrá detectar los bordes del sudoku. (También quiero agregar que si las líneas que no son perpendiculares a los bordes de la imagen, las operaciones sobel (dx y dy) seguirán funcionando ya que las líneas seguirán teniendo bordes con respecto a ambos ejes.)

Para poder detectar líneas rectas se debe trabajar en el análisis de contorno o píxel tales como contourArea/boundingRectArea, arriba a la izquierda y abajo a la derecha...

Editar: Logré comprobar si un conjunto de contornos forman una línea o no aplicando regresión lineal y comprobando el error. Sin embargo, la regresión lineal se realiza mal cuando la pendiente de la línea es demasiado grande (es decir, >1000) o está muy cerca de 0. Por lo tanto, aplicar la prueba de relación anterior (en la respuesta más votada) antes de la regresión lineal es lógico y funcionó para mí.

 1
Author: Ali Eren Çelik,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-10-11 11:10:54