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:
- Lire l'image
- Trouvez les contours
- Sélectionnez celui avec une superficie maximale, (et aussi un peu équivalent au carré).
Trouvez les points d'angle.
par exemple donné ci-dessous:
( 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)
déformer l'image en un carré parfait
par exemple image:
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:
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?
la source
Réponses:
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]]
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]]]
En remplissant cette image, j'obtiens un masque pour la grille sudoku:
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];
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]];
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}];
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]
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.
la source
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 :
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 :
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 :
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 :
Bien sûr, celui-ci n'est pas si bon.
5. Recherche de points de grille
Résultat :
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:
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:
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 :
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.
la source
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.
la source
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.
la source
Pour supprimer les coins non détectés, j'ai appliqué une correction gamma avec une valeur gamma de 0,8.
Le cercle rouge est dessiné pour montrer le coin manquant.
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.
la source
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()
la source