Comment supprimer les défauts de convexité dans un carré Sudoku?

202

Je faisais un projet amusant: résoudre un Sudoku à partir d'une image d'entrée en utilisant OpenCV (comme dans les lunettes Google, etc.). Et j'ai terminé la tâche, mais à la fin j'ai trouvé un petit problème pour lequel je suis venu ici.

J'ai fait la programmation en utilisant l'API Python d'OpenCV 2.3.1.

Voici ce que j'ai fait:

  1. Lire l'image
  2. Trouvez les contours
  3. Sélectionnez celui avec une superficie maximale, (et aussi un peu équivalent au carré).
  4. Trouvez les points d'angle.

    par exemple donné ci-dessous:

    entrez la description de l'image ici

    ( Notez ici que la ligne verte coïncide correctement avec la vraie frontière du Sudoku, donc le Sudoku peut être correctement déformé . Vérifiez l'image suivante)

  5. déformer l'image en un carré parfait

    par exemple image:

    entrez la description de l'image ici

  6. Effectuer l'OCR (pour lequel j'ai utilisé la méthode que j'ai donnée dans l' OCR de reconnaissance de chiffres simples dans OpenCV-Python )

Et la méthode a bien fonctionné.

Problème:

Regardez cette image.

L'exécution de l'étape 4 sur cette image donne le résultat ci-dessous:

entrez la description de l'image ici

La ligne rouge dessinée est le contour d'origine qui est le véritable contour de la frontière sudoku.

La ligne verte dessinée est un contour approximatif qui sera le contour de l'image déformée.

Ce qui bien sûr, il y a une différence entre la ligne verte et la ligne rouge sur le bord supérieur du sudoku. Donc, tout en déformant, je n'obtiens pas la limite d'origine du Sudoku.

Ma question :

Comment puis-je déformer l'image sur la limite correcte du Sudoku, c'est-à-dire la ligne rouge OU comment puis-je supprimer la différence entre la ligne rouge et la ligne verte? Existe-t-il une méthode pour cela dans OpenCV?

Abid Rahman K
la source
1
Vous effectuez votre détection en fonction des points d'angle, sur lesquels les lignes rouge et verte s'accordent. Je ne connais pas OpenCV, mais vous voudrez probablement détecter les lignes entre ces points d'angle et la déformation en fonction de cela.
djs
Peut-être forcer les lignes qui relient les points d'angle à coïncider avec des pixels noirs épais dans l'image. Autrement dit, au lieu de laisser les lignes vertes trouver simplement une ligne droite entre les points d'angle, forcez-les à traverser de lourds pixels noirs. Cela rendra votre problème beaucoup plus difficile, je pense, et je ne connais aucun élément intégré d'OpenCV qui vous sera immédiatement utile.
ely
@ Dougal: Je pense que la ligne verte tracée est la ligne droite approximative de la ligne rouge. c'est donc la ligne entre ces points d'angle. Lorsque je déforme selon la ligne verte, j'obtiens une ligne rouge incurvée en haut de l'image déformée. (j'espère que vous comprenez, mon explication semble un peu mauvaise)
Abid Rahman K
@ EMS: je pense que la ligne rouge tracée est exactement à la frontière du sudoku. Mais le problème est de savoir comment déformer l'image exactement à la frontière du sudoku. (Je veux dire, le problème vient de la déformation, c'est-à-dire de la conversion de ces bordures courbes en un carré exact, comme je l'ai montré dans la deuxième image)
Abid Rahman K

Réponses:

259

J'ai une solution qui fonctionne, mais vous devrez la traduire vous-même vers OpenCV. C'est écrit en Mathematica.

La première étape consiste à régler la luminosité de l'image, en divisant chaque pixel avec le résultat d'une opération de fermeture:

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

entrez la description de l'image ici

L'étape suivante consiste à trouver la zone sudoku, afin que je puisse ignorer (masquer) l'arrière-plan. Pour cela, j'utilise l'analyse des composants connectés et je sélectionne le composant qui a la plus grande surface convexe:

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

entrez la description de l'image ici

En remplissant cette image, j'obtiens un masque pour la grille sudoku:

mask = FillingTransform[largestComponent]

entrez la description de l'image ici

Maintenant, je peux utiliser un filtre dérivé du 2ème ordre pour trouver les lignes verticales et horizontales dans deux images séparées:

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

entrez la description de l'image ici

J'utilise à nouveau l'analyse des composants connectés pour extraire les lignes de grille de ces images. Les lignes de la grille sont beaucoup plus longues que les chiffres, je peux donc utiliser la longueur du pied à coulisse pour sélectionner uniquement les composants connectés aux lignes de la grille. En les triant par position, j'obtiens 2 x 10 images de masque pour chacune des lignes de grille verticales / horizontales de l'image:

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

entrez la description de l'image ici

Ensuite, je prends chaque paire de lignes de grille verticales / horizontales, je les dilate, je calcule l'intersection pixel par pixel et je calcule le centre du résultat. Ces points sont les intersections des lignes de quadrillage:

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}];

entrez la description de l'image ici

La dernière étape consiste à définir deux fonctions d'interpolation pour le mappage X / Y à travers ces points, et à transformer l'image à l'aide de ces fonctions:

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]

entrez la description de l'image ici

Toutes les opérations sont des fonctions de traitement d'image de base, cela devrait donc être également possible dans OpenCV. La transformation d'image basée sur les splines pourrait être plus difficile, mais je ne pense pas que vous en ayez vraiment besoin. L'utilisation de la transformation de perspective que vous utilisez maintenant sur chaque cellule individuelle donnera probablement de bons résultats.

Niki
la source
4
Oh mon Dieu !!!!!!!!! C'était merveilleux. C'est vraiment super. Je vais essayer de le faire dans OpenCV. J'espère que vous m'aideriez avec des détails sur certaines fonctions et la terminologie ... Merci.
Abid Rahman K
@arkiaz: Je ne suis pas un expert OpenCV, mais j'aiderai si je peux, bien sûr.
Niki
Pouvez-vous expliquer à quoi sert la fonction "fermeture"? ce que je veux dire, c'est ce qui se passe en arrière-plan? Dans la documentation, il est dit que la fermeture supprime le bruit du sel et du poivre? Est-ce que la fermeture du filtre passe-bas?
Abid Rahman K
2
Réponse incroyable! D'où vous est venue l'idée de diviser par la fermeture pour normaliser la luminosité de l'image? J'essaie d'améliorer la vitesse de cette méthode, car la division en virgule flottante est extrêmement lente sur les téléphones mobiles. Avez-vous des suggestions? @AbidRahmanK
1 ''
1
@ 1 *: Je pense que ça s'appelle "réglage de l'image blanche". Ne me demandez pas où j'ai lu à ce sujet, c'est un outil de traitement d'image standard. Le modèle derrière l'idée est simple: la quantité de lumière réfléchie par une surface (lambertienne) est juste la luminosité de la surface multipliée par la quantité de lumière qu'un corps blanc dans la même position refléterait. Estimez la luminosité apparente d'un corps blanc dans la même position, divisez la luminosité réelle par cela et vous obtenez la luminosité de la surface.
Niki
213

La réponse de Nikie a résolu mon problème, mais sa réponse était en Mathematica. J'ai donc pensé que je devrais donner ici son adaptation OpenCV. Mais après l'implémentation, j'ai pu voir que le code OpenCV est beaucoup plus volumineux que le code mathématique de Nikie. Et aussi, je n'ai pas trouvé de méthode d'interpolation faite par nikie dans OpenCV (bien que cela puisse être fait en utilisant scipy, je le dirai le moment venu.)

1. Prétraitement de l'image (opération de fermeture)

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)

Résultat :

Résultat de la fermeture

2. Recherche de Sudoku Square et création d'une image de masque

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)

Résultat :

entrez la description de l'image ici

3. Recherche de lignes 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()

Résultat :

entrez la description de l'image ici

4. Recherche de lignes 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()

Résultat :

entrez la description de l'image ici

Bien sûr, celui-ci n'est pas si bon.

5. Recherche de points de grille

res = cv2.bitwise_and(closex,closey)

Résultat :

entrez la description de l'image ici

6. Correction des défauts

Ici, Nikie fait une sorte d'interpolation, dont je n'ai pas beaucoup de connaissances. Et je n'ai pas trouvé de fonction correspondante pour cet OpenCV. (peut-être est-ce là, je ne sais pas).

Découvrez ce SOF qui explique comment faire cela en utilisant SciPy, que je ne veux pas utiliser: Transformation d'image dans OpenCV

Donc, ici, j'ai pris 4 coins de chaque sous-carré et appliqué la perspective de chaîne à chacun.

Pour cela, nous trouvons d'abord les centroïdes.

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))

Mais les centres de gravité résultants ne seront pas triés. Regardez l'image ci-dessous pour voir leur commande:

entrez la description de l'image ici

Nous les trions donc de gauche à droite, de haut en bas.

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))

Maintenant, voyez ci-dessous leur commande:

entrez la description de l'image ici

Enfin, nous appliquons la transformation et créons une nouvelle image de taille 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()

Résultat :

entrez la description de l'image ici

Le résultat est presque le même que celui de nikie, mais la longueur du code est grande. Peut-être que de meilleures méthodes sont disponibles, mais jusque-là, cela fonctionne bien.

Cordialement ARK.

Abid Rahman K
la source
4
"Je préfère que mon application plante plutôt que de recevoir de mauvaises réponses." <- Je suis également d'accord à 100%
Viktor Sehr
Merci, sa vraie réponse est donnée par Nikie. Mais c'était en mathématique, donc je l'ai juste converti en OpenCV. Donc, la vraie réponse a eu assez de votes positifs, je pense
Abid Rahman K
Ah je n'ai pas vu que vous avez également posté la question :)
Viktor Sehr
Ouais. La question est aussi la mienne. La mienne et la réponse de Nikie ne sont différentes qu'à la fin. Il a une sorte de fonction d'interpolation dans Mathematica qui n'est pas dans numpy ou opencv (mais elle est là dans Scipy, mais je ne voulais pas utiliser Scipy ici)
Abid Rahman K
J'obtiens une erreur: sortie [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: l'argument long () doit être une chaîne ou un nombre, pas 'builtin_function_or_method'
user898678
6

Vous pouvez essayer d'utiliser une sorte de modélisation basée sur une grille de votre déformation arbitraire. Et comme le sudoku est déjà une grille, cela ne devrait pas être trop difficile.

Vous pouvez donc essayer de détecter les limites de chaque sous-région 3x3, puis déformer chaque région individuellement. Si la détection réussit, cela vous donnerait une meilleure approximation.

sietschie
la source
1

Je veux ajouter que la méthode ci-dessus ne fonctionne que lorsque le tableau de sudoku se tient droit, sinon le test de rapport hauteur / largeur (ou vice versa) échouera très probablement et vous ne pourrez pas détecter les bords du sudoku. (Je veux également ajouter que si des lignes qui ne sont pas perpendiculaires aux bordures de l'image, les opérations sobel (dx et dy) fonctionneront toujours car les lignes auront toujours des bords par rapport aux deux axes.)

Pour pouvoir détecter les lignes droites, vous devez travailler sur l'analyse des contours ou des pixels comme contourArea / boundingRectArea, points en haut à gauche et en bas à droite ...

Edit: J'ai réussi à vérifier si un ensemble de contours forme une ligne ou non en appliquant une régression linéaire et en vérifiant l'erreur. Cependant, la régression linéaire a mal fonctionné lorsque la pente de la droite est trop grande (c'est-à-dire> 1000) ou très proche de 0. Par conséquent, appliquer le test de ratio ci-dessus (dans la réponse la plus votée) avant la régression linéaire est logique et a fonctionné pour moi.

Ali Eren Çelik
la source
1

Pour supprimer les coins non détectés, j'ai appliqué une correction gamma avec une valeur gamma de 0,8.

Avant la correction gamma

Le cercle rouge est dessiné pour montrer le coin manquant.

Après correction gamma

Le code est:

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)

Ceci s'ajoute à la réponse d'Abid Rahman s'il manque des points de coin.

Vardan Agarwal
la source
0

J'ai trouvé que c'était un excellent article et une excellente solution par ARK; très bien présenté et expliqué.

Je travaillais sur un problème similaire et j'ai tout construit. Il y a eu quelques changements (ie xrange to range, arguments dans cv2.findContours), mais cela devrait fonctionner immédiatement (Python 3.5, Anaconda).

Ceci est une compilation des éléments ci-dessus, avec une partie du code manquant ajouté (c.-à-d. L'étiquetage des points).

'''

/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()
asile
la source