Je faisais un projet amusant: Résoudre un Sudoku à partir d'une image d'entrée à l'aide d'OpenCV (comme dans les lunettes de 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 Python API d'OpenCV 2.3.1.
Voici ce que j'ai fait:
Trouvez les points de coin.
par exemple. donnée ci-après:
( Notez ici que la ligne verte coïncide correctement avec la limite réelle du Sudoku, de sorte que le Sudoku puisse être correctement déformé . Voir 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 OCR à reconnaissance simple de chiffres dans OpenCV-Python )
Et la méthode a bien fonctionné.
Problème:
Départ cette image.
Effectuer l’étape 4 sur cette image donne le résultat ci-dessous:
La ligne rouge dessinée est le contour d'origine qui représente le contour exact de la limite de sudoku.
La ligne verte dessinée est un contour approximatif qui sera le contour de l'image déformée.
Bien sûr, il y a une différence entre la ligne verte et la ligne rouge au bord supérieur du sudoku. Ainsi, tout en se déformant, je n’obtiens pas la limite originale du Sudoku.
Ma question:
Comment puis-je déformer l’image sur la limite correcte du Sudoku, c’est-à-dire la ligne rouge OR), comment puis-je supprimer la différence entre la ligne rouge et la ligne verte? Existe-t-il une méthode à cet effet dans OpenCV?
J'ai une solution qui fonctionne, mais vous devrez la traduire vous-même en OpenCV. C'est écrit dans Mathematica.
La première étape consiste à ajuster la luminosité de l'image en divisant chaque pixel par 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 de sudoku afin que je puisse ignorer (masquer) l'arrière-plan. Pour cela, j'utilise l'analyse des composants connectés et sélectionne le composant qui possède 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, je reçois un masque pour la grille de sudoku:
mask = FillingTransform[largestComponent]
Maintenant, je peux utiliser un filtre dérivé du 2ème ordre pour trouver les lignes verticales et horizontales dans deux images distinctes:
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 de l'épaisseur pour sélectionner uniquement les composants liés aux lignes de la grille. En les triant par position, j'obtiens 2x10 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, les dilate, calcule l'intersection pixel par pixel et le centre du résultat. Ces points sont les intersections de la ligne de grille:
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 via 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 possible dans OpenCV également. La transformation d'image basée sur les splines est peut-ê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 donnera probablement de bons résultats.
La réponse de Nikie a résolu mon problème, mais sa réponse était dans Mathematica. Alors j'ai pensé que je devrais donner son adaptation OpenCV ici. Mais après la mise en œuvre, j'ai pu constater que le code OpenCV était bien plus gros que le code mathématique de Nikie. Et aussi, je ne pouvais pas trouver la méthode d'interpolation faite par nikie dans OpenCV (bien que cela puisse être fait en utilisant scipy, je le dirai quand le temps sera 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. Trouver la place Sudoku et créer 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
res = cv2.bitwise_and(closex,closey)
Résultat :
6. Corriger les défauts
Ici, Nikie fait une sorte d'interpolation, sur laquelle je n'ai pas beaucoup de connaissances. Et je ne pouvais trouver aucune fonction correspondante pour cette OpenCV. (peut-être que c'est là, je ne sais pas).
Découvrez ce SOF qui explique comment faire cela en utilisant SciPy, que je ne souhaite pas utiliser: Transformation d'image en OpenCV
Donc, ici, j'ai pris 4 coins de chaque sous-carré et appliqué la perspective Warp à 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 centroïdes résultants ne seront pas triés. Découvrez l'image ci-dessous pour voir leur commande:
Nous les trions donc de gauche à droite et 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 identique à celui de Nikie, mais la longueur du code est grande. Peut-être, de meilleures méthodes sont disponibles, mais jusque-là, cela fonctionne bien.
Cordialement ARK.
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.
Je veux ajouter que la méthode ci-dessus ne fonctionne que lorsque le tableau de sudoku est droit, sinon le test de rapport hauteur/largeur (ou inversement) échouera probablement et vous ne pourrez pas détecter les bords du sudoku. (Je souhaite également ajouter que si les lignes qui ne sont pas perpendiculaires aux bords de l'image, les opérations sobel (dx et dy) fonctionneront quand même, car les lignes auront toujours des bords par rapport aux deux axes.)
Pour pouvoir détecter les lignes droites, vous devez travailler sur une analyse de contour ou de pixel, telle que contourArea/boundingRectArea, les points haut gauche et 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 est médiocre lorsque la pente de la ligne est trop grande (> 1000) ou très proche de 0. Par conséquent, appliquer le test du rapport ci-dessus (dans la réponse la plus élevée) avant la régression linéaire est logique et a fonctionné pour moi.
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)
Cela s'ajoute à la réponse d'Abid Rahman si certains points de coin manquent.