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

193

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

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

A continuación es lo que hice:

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

    Por ejemplo, a continuación:

    ingrese la descripción de la imagen aquí

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

  5. deformar la imagen a un cuadrado perfecto

    por ejemplo, imagen:

    ingrese la descripción de la imagen aquí

  6. Realice OCR (para lo cual utilicé el método que le di en OCR de reconocimiento de dígitos simple en OpenCV-Python )

Y el método funcionó bien.

Problema:

Mira esta imagen.

Realizar el paso 4 en esta imagen da el resultado a continuación:

ingrese la descripción de la imagen aquí

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

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

Lo cual, por supuesto, existe una diferencia entre la línea verde y la línea roja en el borde superior del sudoku. Entonces, al deformar, no obtengo 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?

Abid Rahman K
fuente
1
Estás haciendo tu detección en función de los puntos de esquina, en los que las líneas roja y verde coinciden. No conozco OpenCV, pero presumiblemente querrás detectar las líneas entre esos puntos de esquina y la deformación en función de eso.
Dougal
Tal vez fuerce las líneas que conectan los puntos de las esquinas para que coincidan con los píxeles negros pesados ​​en la imagen. Es decir, en lugar de dejar que las líneas verdes solo encuentren una línea recta entre los puntos de las esquinas, obligarlas a atravesar grandes píxeles negros. Creo que esto hará que su problema sea mucho más difícil, y no conozco ninguna función incorporada de OpenCV que sea inmediatamente útil para usted.
ely
@ Dougal: Creo que la línea verde dibujada es la línea recta aproximada de la línea roja. entonces es la línea entre esos puntos de esquina. Cuando me combino de acuerdo con la línea verde, obtengo una línea roja curva en la parte superior de la imagen deformada. (Espero que entiendas, mi explicación parece un poco mala)
Abid Rahman K
@ EMS: creo que la línea roja dibujada está exactamente en el borde del sudoku. Pero el problema es cómo deformar la imagen exactamente en el borde del sudoku. (Quiero decir, el problema es con la deformación, es decir, convertir esos bordes curvos en un cuadrado exacto, como lo he mostrado en la segunda imagen)
Abid Rahman K

Respuestas:

252

Tengo una solución que funciona, pero tendrá que traducirla a OpenCV usted mismo. 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]]

ingrese la descripción de la imagen aquí

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

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

ingrese la descripción de la imagen aquí

Al llenar esta imagen, obtengo una máscara para la cuadrícula de sudoku:

mask = FillingTransform[largestComponent]

ingrese la descripción de la imagen aquí

Ahora, puedo usar un filtro derivado de segundo 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];

ingrese la descripción de la imagen aquí

Utilizo el análisis de componentes conectados nuevamente 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 del calibrador para seleccionar solo los componentes conectados a las líneas de la cuadrícula. Al ordenarlos por posición, obtengo imágenes de máscara de 2x10 para cada una de las líneas de cuadrícula verticales / horizontales 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]];

ingrese la descripción de la imagen aquí

A continuación, tomo cada par de líneas de cuadrícula verticales / horizontales, 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 la 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}];

ingrese 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]

ingrese la descripción de la imagen aquí

Todas las operaciones son funciones básicas de procesamiento de imágenes, por lo que esto también debería ser posible en OpenCV. La transformación de imagen basada en spline puede ser más difícil, pero no creo que realmente la necesite. Probablemente, usar la transformación de perspectiva que usa ahora en cada celda individual dará resultados suficientemente buenos.

Niki
fuente
3
Oh Dios mío !!!!!!!!! Eso fue maravilloso. Esto es realmente genial. Intentaré hacerlo en OpenCV. Espero que me ayuden con detalles sobre ciertas funciones y terminología ... Gracias.
Abid Rahman K
@arkiaz: No soy un experto en OpenCV, pero ayudaré si puedo, claro.
Niki
¿Puede explicar para qué se utiliza la función de "cierre"? lo que quiero decir es lo que sucede en el fondo? En la documentación, dice que el cierre elimina el ruido de sal y pimienta. ¿Está cerrando el filtro de paso bajo?
Abid Rahman K
2
Increíble respuesta! ¿De dónde sacaste la idea de dividir por el cierre para normalizar el brillo de la imagen? Estoy tratando de mejorar la velocidad de este método, ya que la división de punto flotante es muy lenta en los teléfonos móviles. ¿Tienes alguna sugerencia? @AbidRahmanK
1 ''
1
@ 1 *: Creo que se llama "ajuste de imagen en blanco". No me pregunten dónde lo he leído, es una herramienta estándar de procesamiento de imágenes. El modelo detrás de la idea es simple: la cantidad de luz reflejada desde una superficie (Lambertiana) es solo el brillo de la superficie multiplicado por la cantidad de luz que reflejaría un cuerpo blanco en la misma posición. Estima el brillo aparente de un cuerpo blanco en la misma posición, divide el brillo real entre eso y obtienes el brillo de la superficie.
Niki
209

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 matemático de Nikie. Y también, no pude encontrar el método de interpolación realizado 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 el 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:

ingrese 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:

ingrese 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:

ingrese 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:

ingrese la descripción de la imagen aquí

6. Corrigiendo 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é allí, 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 subcuadro y apliqué Perspectiva de urdimbre a cada una.

Para eso, primero encontramos los centroides.

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:

ingrese la descripción de la imagen aquí

Entonces 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 vea a continuación su orden:

ingrese 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:

ingrese 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, hay mejores métodos disponibles, pero hasta entonces, esto funciona bien.

Saludos ARCA.

Abid Rahman K
fuente
44
"Prefiero que mi aplicación se bloquee que obtener respuestas incorrectas". <- También estoy de acuerdo con esto al 100%
Viktor Sehr
Gracias, Nikie da su respuesta real. Pero eso fue en matemática, así que lo convertí a OpenCV. Así que la respuesta real tiene suficientes votos positivos, creo
Abid Rahman K
Ah no vi que también publicaste la pregunta :)
Viktor Sehr
Si. La pregunta también es mía. La respuesta mía y de Nikie es diferente solo al final. Tiene algún tipo de función de intepolación en matemática que no está en numpy o opencv (pero está allí en Scipy, pero no quería usar Scipy aquí)
Abid Rahman K
Recibo un error: salida [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 TypeError: el argumento long () debe ser una cadena o un número, no 'builtin_function_or_method'
user898678
6

Podría intentar utilizar algún tipo de modelo basado en cuadrículas de su deformación arbitraria. Y dado que el sudoku ya es una cuadrícula, eso no debería ser demasiado difícil.

Por lo tanto, podría 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.

sietschie
fuente
1

Quiero agregar que el método anterior funciona solo 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 tendrán bordes con respecto a ambos ejes).

Para poder detectar líneas rectas, debe trabajar en el análisis de contorno o píxel, como contourArea / boundingRectArea, puntos superior izquierdo e inferior derecho ...

Editar: logré verificar si un conjunto de contornos forma una línea o no aplicando regresión lineal y verificando el error. Sin embargo, la regresión lineal funcionó 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 razón anterior (en la mayoría de las respuestas votadas) antes de que la regresión lineal sea lógica y funcionó para mí.

Ali Eren Çelik
fuente
1

Para eliminar las esquinas no detectadas, apliqué corrección gamma con un valor gamma de 0.8.

Antes de la corrección gamma

El círculo rojo se dibuja para mostrar la esquina que falta.

Después de la corrección gamma

El codigo es:

gamma = 0.8
invGamma = 1/gamma
table = np.array([((i / 255.0) ** invGamma) * 255
                  for i in np.arange(0, 256)]).astype("uint8")
cv2.LUT(img, table, img)

Esto se suma a la respuesta de Abid Rahman si faltan algunos puntos de esquina.

Vardan Agarwal
fuente
0

Pensé que esta era una gran publicación, y una gran solución de ARK; Muy bien presentado y explicado.

Estaba trabajando en un problema similar y construí todo. Hubo algunos cambios (es decir, xrange a range, argumentos en cv2.findContours), pero esto debería funcionar de forma inmediata (Python 3.5, Anaconda).

Esta es una compilación de los elementos anteriores, con algunos de los códigos faltantes agregados (es decir, etiquetado de puntos).

'''

/programming/10196198/how-to-remove-convexity-defects-in-a-sudoku-square

'''

import cv2
import numpy as np

img = cv2.imread('test.png')

winname="raw image"
cv2.namedWindow(winname)
cv2.imshow(winname, img)
cv2.moveWindow(winname, 100,100)


img = cv2.GaussianBlur(img,(5,5),0)

winname="blurred"
cv2.namedWindow(winname)
cv2.imshow(winname, img)
cv2.moveWindow(winname, 100,150)

gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
mask = np.zeros((gray.shape),np.uint8)
kernel1 = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(11,11))

winname="gray"
cv2.namedWindow(winname)
cv2.imshow(winname, gray)
cv2.moveWindow(winname, 100,200)

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)

winname="res2"
cv2.namedWindow(winname)
cv2.imshow(winname, res2)
cv2.moveWindow(winname, 100,250)

 #find elements
thresh = cv2.adaptiveThreshold(res,255,0,1,19,2)
img_c, 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)

winname="puzzle only"
cv2.namedWindow(winname)
cv2.imshow(winname, res)
cv2.moveWindow(winname, 100,300)

# vertical lines
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)

img_d, 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()

winname="vertical lines"
cv2.namedWindow(winname)
cv2.imshow(winname, img_d)
cv2.moveWindow(winname, 100,350)

# find horizontal lines
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)

img_e, 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()

winname="horizontal lines"
cv2.namedWindow(winname)
cv2.imshow(winname, img_e)
cv2.moveWindow(winname, 100,400)


# intersection of these two gives dots
res = cv2.bitwise_and(closex,closey)

winname="intersections"
cv2.namedWindow(winname)
cv2.imshow(winname, res)
cv2.moveWindow(winname, 100,450)

# text blue
textcolor=(0,255,0)
# points green
pointcolor=(255,0,0)

# find centroids and sort
img_f, 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))

# sorting
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 range(10)])
bm = b.reshape((10,10,2))

# make copy
labeled_in_order=res2.copy()

for index, pt in enumerate(b):
    cv2.putText(labeled_in_order,str(index),tuple(pt),cv2.FONT_HERSHEY_DUPLEX, 0.75, textcolor)
    cv2.circle(labeled_in_order, tuple(pt), 5, pointcolor)

winname="labeled in order"
cv2.namedWindow(winname)
cv2.imshow(winname, labeled_in_order)
cv2.moveWindow(winname, 100,500)

# create final

output = np.zeros((450,450,3),np.uint8)
for i,j in enumerate(b):
    ri = int(i/10) # row index
    ci = i%10 # column index
    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()

winname="final"
cv2.namedWindow(winname)
cv2.imshow(winname, output)
cv2.moveWindow(winname, 600,100)

cv2.waitKey(0)
cv2.destroyAllWindows()
asylumax
fuente