
Von KI -Hype fühlt es sich an, als würde jeder benutzen Visionsprachel-Modelle und groß Imaginative and prescient Transformers Für jedes Downside in der Pc Imaginative and prescient. Viele Menschen sehen diese Instruments als einheitliche Lösungen an und verwenden sofort das neueste, glänzendste Modell anstelle von das zugrunde liegende Sign verstehen Sie wollen extrahieren. Aber oft gibt die Einfachheit Schönheit. Es ist eine der wichtigsten Lektionen, die ich als Ingenieur gelernt habe: Überkomitieren Sie keine Lösungen für einfache Probleme.

Lassen Sie mich Ihnen eine praktische Anwendung einiger einfacher klassischer Pc -Imaginative and prescient -Techniken zeigen, um rechteckige Objekte auf flachen Oberflächen zu erkennen und eine Perspektive -Transformation anzuwenden, um das verzerrte Rechteck zu transformieren. Ähnliche Methoden werden beispielsweise bei Dokumentenscannungs- und Extraktionsanwendungen häufig verwendet.
Unterwegs lernen Sie einige interessante Konzepte, von Standardtechniken für klassische Pc Imaginative and prescient bis hin zur Bestellung von Polygonpunkten und warum dies mit einem Downside der Kombinatoraufgabe zusammenhängt.
Überblick
- Erkennung
- Graustufen
- Kantenerkennung
- Erweiterung
- Konturerkennung
- Perspektive Transformation
- Variante a: Einfache Sortierung basierend auf Summe/Diff
- Variante B: Zuweisungsoptimierungsproblem
- Variante C: Zyklische Sortierung mit Anker
- Anwendung der Perspektiventransformation
- Abschluss
Erkennung
Um Sudoku -Gitter zu erkennen, habe ich viele verschiedene Ansätze in Betracht gezogen, die von einfachem Schwellenwert, Hough -Line -Transformationen oder einer Type der Kantenerkennung bis hin zum Coaching eines Deep -Lern -Modells zur Segmentierung oder zum Keypoint -Erkennung reichen.
Lassen Sie uns einige definieren Annahmen Um das Downside zu umgehen:
- Das Sudoku -Netz ist im Rahmen deutlich und vollständig sichtbar mit einem klaren viereckigen Rand mit starkem Kontrast vom Hintergrund.
- Die Oberfläche, auf der das Sudoku -Gitter gedruckt ist, muss flach sein, kann jedoch aus einem Winkel erfasst werden und erscheinen verzerrt oder gedreht.

Ich werde Ihnen eine einfache Pipeline mit einigen Filterschritten zeigen, um die Grenzen unseres Sudoku -Netzes zu erkennen. Auf hohem Niveau sieht die Verarbeitungspipeline wie folgt aus:


Graustufen
In diesem ersten Schritt konvertieren wir einfach das Eingabebild aus seinen drei Farbkanälen in ein einzelnes Kanal -Graustufenbild, da wir keine Farbinformationen benötigen, um diese Bilder zu verarbeiten.
def find_sudoku_grid(
picture: np.ndarray,
) -> np.ndarray | None:
"""
Finds the biggest square-like contour in a picture, seemingly the Sudoku grid.
Returns:
The contour of the discovered grid as a numpy array, or None if not discovered.
"""
grey = cv2.cvtColor(picture, cv2.COLOR_BGR2GRAY)
Kantenerkennung
Nach dem Konvertieren des Bildes in Graustufen können wir den Canny Edge -Erkennungsalgorithmus verwenden, um Kanten zu extrahieren. Für diesen Algorithmus stehen zwei Schwellenwerte zur Auswahl, die feststellen, ob Pixel als Kanten akzeptiert werden:

In unserem Fall der Erkennung von Sudoku -Gittern nehmen wir sehr starke Kanten an den Grenzlinien unseres Netzes an. Wir können einen hohen oberen Schwellenwert auswählen, um Geräusche in unserer Maske abzulehnen, und eine nicht zu niedrige Schwelle, die nicht zu niedrig ist, um kleine, verrückte Kanten abzulehnen, die an den Hauptgrenze in unserer Maske angezeigt werden.
Ein Unschärfefilter wird häufig verwendet, bevor Bilder an Cushly geleitet werden, um das Geräusch zu reduzieren. In diesem Fall sind die Kanten jedoch sehr stark, aber schmal, daher wird die Unschärfe weggelassen.
def find_sudoku_grid(
picture: np.ndarray,
canny_threshold_1: int = 100,
canny_threshold_2: int = 255,
) -> np.ndarray | None:
"""
Finds the biggest square-like contour in a picture, seemingly the Sudoku grid.
Args:
picture: The enter picture.
canny_threshold_1: Decrease threshold for the Canny edge detector.
canny_threshold_2: Higher threshold for the Canny edge detector.
Returns:
The contour of the discovered grid as a numpy array, or None if not discovered.
"""
...
canny = cv2.Canny(grey, threshold1=canny_threshold_1, threshold2=canny_threshold_2)

Erweiterung
In diesem nächsten Schritt verarbeiten wir die Kantenerkennungsmaske mit einem Dilatationskern, um kleine Lücken in der Maske zu schließen.
def find_sudoku_grid(
picture: np.ndarray,
canny_threshold_1: int = 100,
canny_threshold_2: int = 255,
morph_kernel_size: int = 3,
) -> np.ndarray | None:
"""
Finds the biggest square-like contour in a picture, seemingly the Sudoku grid.
Args:
picture: The enter picture.
canny_threshold_1: First threshold for the Canny edge detector.
canny_threshold_2: Second threshold for the Canny edge detector.
morph_kernel_size: Measurement of the morphological operation kernel.
Returns:
The contour of the discovered grid as a numpy array, or None if not discovered.
"""
...
kernel = cv2.getStructuringElement(
form=cv2.MORPH_RECT, ksize=(morph_kernel_size, morph_kernel_size)
)
masks = cv2.morphologyEx(canny, op=cv2.MORPH_DILATE, kernel=kernel, iterations=1)

Konturerkennung
Nachdem die binäre Maske fertig ist, können wir einen Konturerkennungsalgorithmus ausführen, um kohärente Blobs zu finden und mit vier Punkten auf eine einzige Kontur zu filtern.
contours, _ = cv2.findContours(
masks, mode=cv2.RETR_EXTERNAL, methodology=cv2.CHAIN_APPROX_SIMPLE
)

Diese anfängliche Konturerkennung gibt eine Liste von Konturen zurück, die jedes einzelne Pixel enthalten, das Teil der Kontur ist. Wir können die verwenden Douglas -Peucker Algorithmus reduzieren die Anzahl der Punkte in der Kontur iterativ und ungefähr die Kontur mit einem einfachen Polygon. Wir können einen Mindestabstand zwischen den Punkten für den Algorithmus auswählen.


Wenn wir annehmen, dass selbst für einige der verzerrtsten Rechtecke die kürzeste Seite mindestens 10% des Formumfangs beträgt, können wir die Konturen auf Polygone mit genau vier Punkten filtern.
contour_candidates: checklist(np.ndarray) = ()
for cnt in contours:
# Approximate the contour to a polygon
epsilon = 0.1 * cv2.arcLength(curve=cnt, closed=True)
approx = cv2.approxPolyDP(curve=cnt, epsilon=epsilon, closed=True)
# Preserve solely polygons with 4 vertices
if len(approx) == 4:
contour_candidates.append(approx)
Schließlich nehmen wir die größte entdeckte Kontur, vermutlich das letzte Sudoku -Netz. Wir sortieren die Konturen in umgekehrter Reihenfolge nach Gebiet und nehmen dann das erste Component auf, das dem größten Konturbereich entspricht.
best_contour = sorted(contour_candidates, key=cv2.contourArea, reverse=True)(0)

Perspektive Transformation
Schließlich müssen wir das erkannte Netz wieder in sein Quadrat umwandeln. Um dies zu erreichen, können wir eine perspektivische Transformation verwenden. Die Transformationsmatrix kann berechnet werden, indem angeben, wo die vier Punkte unserer Sudoku -Gitterkontur am Ende platziert werden müssen: die vier Ecken des Bildes.
rect_dst = np.array(
((0, 0), (width - 1, 0), (width - 1, top - 1), (0, top - 1)),
)

Um den Konturpunkten an die Ecken zu entsprechen, müssen sie zuerst bestellt werden, damit sie korrekt zugewiesen werden können. Definieren wir die folgende Reihenfolge für unsere Eckpunkte:

Variante A: Einfache Sortierung basierend auf Summe/Diff
Um die extrahierten Ecken zu sortieren und diese Zielpunkte zuzuweisen, könnte ein einfacher Algorithmus die Ansicht der sum Und variations der x Und y Koordinaten für jede Ecke.
p_sum = p_x + p_y
p_diff = p_x - p_y
Basierend auf diesen Werten ist es jetzt möglich, die Ecken zu unterscheiden:
- Die obere linke Ecke hat sowohl einen kleinen x- als auch y -Wert, sie hat die kleinste Summe
argmin(p_sum) - Die untere rechte Ecke hat die größte Summe
argmax(p_sum) - Die obere rechte Ecke hat den größten Diff
argmax(p_diff) - Die untere linke Ecke hat den kleinsten Unterschied
argmin(p_diff)
In der folgenden Animation habe ich versucht, diese Aufgabe der vier Ecken eines rotierenden Quadrats zu visualisieren. Die farbigen Linien repräsentieren die jeweilige Bildecke, die jeder quadratischen Ecke zugeordnet ist.

def order_points(pts: np.ndarray) -> np.ndarray:
"""
Orders the 4 nook factors of a contour in a constant
top-left, top-right, bottom-right, bottom-left sequence.
Args:
pts: A numpy array of form (4, 2) representing the 4 corners.
Returns:
A numpy array of form (4, 2) with the factors ordered.
"""
# Reshape from (4, 1, 2) to (4, 2) if wanted
pts = pts.reshape(4, 2)
rect = np.zeros((4, 2), dtype=np.float32)
# The highest-left level can have the smallest sum, whereas
# the bottom-right level can have the biggest sum
s = pts.sum(axis=1)
rect(0) = pts(np.argmin(s))
rect(2) = pts(np.argmax(s))
# The highest-right level can have the smallest distinction,
# whereas the bottom-left can have the biggest distinction
diff = np.diff(pts, axis=1)
rect(1) = pts(np.argmin(diff))
rect(3) = pts(np.argmax(diff))
return rect
Dies funktioniert intestine, es sei denn, das Rechteck ist wie das folgende stark verzerrt. In diesem Fall können Sie deutlich erkennen, dass diese Methode fehlerhaft ist, da dieselbe Rechteck -Ecke mehrere Bild -Ecken zugewiesen wird.

Variante B: Zuweisungsoptimierungsproblem
Ein anderer Ansatz wäre, die Abstände zwischen jedem Punkt und seiner zugewiesenen Ecke zu minimieren. Dies kann mit a implementiert werden pairwise_distances Berechnung zwischen jedem Punkt und den Ecken und den linear_sum_assignment Funktion von Scipywas das Zuordnungsproblem löst und gleichzeitig eine Kostenfunktion minimiert.
def order_points_simplified(pts: np.ndarray) -> np.ndarray:
"""
Orders a set of factors to finest match a goal set of nook factors.
Args:
pts: A numpy array of form (N, 2) representing the factors to order.
Returns:
A numpy array of form (N, 2) with the factors ordered.
"""
# Reshape from (N, 1, 2) to (N, 2) if wanted
pts = pts.reshape(-1, 2)
# Calculate the gap between every level and every goal nook
D = pairwise_distances(pts, pts_corner)
# Discover the optimum one-to-one project
# row_ind(i) ought to be matched with col_ind(i)
row_ind, col_ind = linear_sum_assignment(D)
# Create an empty array to carry the sorted factors
ordered_pts = np.zeros_like(pts)
# Place every level within the appropriate slot primarily based on the nook it was matched to.
# For instance, the purpose matched to target_corners(0) goes into ordered_pts(0).
ordered_pts(col_ind) = pts(row_ind)
return ordered_pts

Obwohl diese Lösung funktioniert, ist sie nicht best, da sie auf den Bildabstand zwischen den Formpunkten und den Ecken basiert und rechenintensiv teurer ist, da eine Entfernungsmatrix konstruiert werden muss. Natürlich ist hier bei vier zugewiesenen Punkten vernachlässigbar, aber diese Lösung wäre nicht intestine für ein Polygon mit vielen Punkten geeignet!
Variante C: Zyklische Sortierung mit Anker
Diese dritte Variante ist eine sehr leichte und effiziente Methode, um die Bildpunkte den Bildwunden zu sortieren und zuzuordnen. Die Idee ist, einen Winkel für jeden Punkt der Type basierend auf der Schwerpunktposition zu berechnen.

Da sind die Winkel zyklischWir müssen einen Anker auswählen, um die absolute Reihenfolge der Punkte zu garantieren. Wir wählen einfach den Punkt mit der niedrigsten Summe von x und y aus.
def order_points(self, pts: np.ndarray) -> np.ndarray:
"""
Orders factors by angle across the centroid, then rotates to begin from top-left.
Args:
pts: A numpy array of form (4, 2).
Returns:
A numpy array of form (4, 2) with factors ordered."""
pts = pts.reshape(4, 2)
heart = pts.imply(axis=0)
angles = np.arctan2(pts(:, 1) - heart(1), pts(:, 0) - heart(0))
pts_cyclic = pts(np.argsort(angles))
sum_of_coords = pts_cyclic.sum(axis=1)
top_left_idx = np.argmin(sum_of_coords)
return np.roll(pts_cyclic, -top_left_idx, axis=0)

Wir können diese Funktion jetzt verwenden, um unsere Konturpunkte zu sortieren:
rect_src = order_points(grid_contour)
Anwendung der Perspektiventransformation
Nachdem wir wissen, welche Punkte wohin müssen, können wir endlich zum interessantesten Teil übergehen: Erstellen und tatsächlich anwenden der Perspektivveränderung auf das Bild.

Da haben wir bereits unsere Liste der Punkte für den erkannten viereckigen Sortieren haben rect_srcund wir haben unsere Zieleckepunkte in rect_dstwir können die verwenden Opencv Methode zur Berechnung der Transformationsmatrix:
warp_mat = cv2.getPerspectiveTransform(rect_src, rect_dst)
Das Ergebnis ist a 3 × 3 Warp -Matrixdefinieren Sie, wie man sich aus einer verzerrten 3D-Perspektive zu einer 2D-Flat-Prime-Down-Ansicht verwandelt. Um diese flache Prime-Down-Sicht auf unser Sudoku-Raster zu erhalten, können wir diese Perspektive-Transformation auf unser Originalbild anwenden:
warped = cv2.warpPerspective(img, warp_mat, (side_len, side_len))
Und voilà, wir haben unser perfekt quadratisches Sudoku -Netz!

Abschluss
In diesem Projekt gingen wir eine einfache Pipeline mit klassischen Pc -Imaginative and prescient -Techniken durch, um Sudoku -Gitter aus Bildern zu extrahieren. Diese Methoden bieten eine einfache Möglichkeit, die Grenzen der Sudoku -Gitter zu erkennen. Natürlich gibt es aufgrund seiner Einfachheit einige Einschränkungen dafür, wie intestine dieser Ansatz auf unterschiedliche Einstellungen und excessive Umgebungen wie schlechte oder harte Schatten verallgemeinert wird. Die Verwendung eines tief lernbasierten Ansatzes könnte sinnvoll sein, wenn die Erkennung auf eine Vielzahl verschiedener Einstellungen verallgemeinert werden muss.
Als nächstes wird eine perspektivische Transformation verwendet, um eine flache Prime-Down-Sicht auf das Netz zu erhalten. Dieses Bild kann nun in der weiteren Verarbeitung verwendet werden, z. B. das Extrahieren der Zahlen im Raster und die tatsächlich Lösung des Sudoku. In einem nächsten Artikel werden wir uns weiter in diese natürlichen nächsten Schritte in diesem Projekt befassen.
Schauen Sie sich den Quellcode des unten stehenden Projekts an und lassen Sie mich wissen, ob Sie Fragen oder Gedanken zu diesem Projekt haben. Bis dahin fröhliche Codierung!
Weitere Informationen und die vollständige Implementierung einschließlich des Codes für alle Animationen und Visualisierungen finden Sie im Quellcode dieses Projekts in meinem GitHub:
https://github.com/trflorian/sudoku-extraction
Alle Visualisierungen in diesem Beitrag wurden vom Autor erstellt.
