web-dev-qa-db-fra.com

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

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:

  1. Lire l'image
  2. Trouver les contours
  3. Sélectionnez celui avec la surface maximale (et aussi un peu équivalent au carré).
  4. Trouvez les points de coin.

    par exemple. donnée ci-après:

    enter image description here

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

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

    par exemple image:

    enter image description here

  6. 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:

enter image description here

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?

176
Abid Rahman K

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

enter image description here

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

enter image description here

En remplissant cette image, je reçois un masque pour la grille de sudoku:

mask = FillingTransform[largestComponent]

enter image description here

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

enter image description here

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

enter image description here

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

enter image description here

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]

enter image description here

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.

236
Niki

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 :

Result of closing

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 :

enter image description here

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 :

enter image description here

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 :

enter image description here

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

5. Recherche de points de grille

res = cv2.bitwise_and(closex,closey)

Résultat :

enter image description here

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:

enter image description here

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:

enter image description here

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 :

enter image description here

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.

198
Abid Rahman K

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.

5
sietschie

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.

1
Ali Eren Çelik

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

Before gamma correction

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

After gamma correction

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.

1
Vardan Agarwal