Animierte Perspektive Transformation, die das Sudoku -Netz verzerrt

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.

Animation der Verarbeitung von Pipeline
Verarbeitung von Pipeline -Schritten animiert

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:

  1. Das Sudoku -Netz ist im Rahmen deutlich und vollständig sichtbar mit einem klaren viereckigen Rand mit starkem Kontrast vom Hintergrund.
  2. 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.
Beispiele für verschiedene Bildqualitäten
Beispiele für verschiedene Bildqualitäten

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:

Diagramm der Verarbeitung von Pipeline -Schritten
Visualisierung der Verarbeitung von Pipeline -Schritten
Visualisierung der Verarbeitung von Pipeline -Schritten

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:

Canny Edge -Schwellenwerte erklärten
Schwellenwerte der cany Kantenerkennung

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)
Maske Bild nach Canny Edge

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)
Maskenbild nach Dilatation

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
)
Erkannte Konturen auf dem Maskenbild

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)
Filterte Kontur auf Originalbild hervorgehoben

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.

Animation eines rotierenden Quadrats, jede Ecke mit einer anderen Farbe und Linien, die die Zuordnung zu Bild -Ecken anzeigen
Animation eines rotierenden Quadrats, jede Ecke mit einer anderen Farbe und Linien, die die Zuordnung zu Bild -Ecken anzeigen
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.

Das gleiche Zuordnungsverfahren schlägt mit einer verdrängten rotierenden viereckigen Form fehl
Das gleiche Zuordnungsverfahren schlägt mit einer verdrängten rotierenden viereckigen Type fehl

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
Animierte rotierende verdrehte Vierecker mit korrekten Ecken, die den Image -Ecken zugewiesen sind
Animierte rotierende verdrehte Viereck mit den Ecken korrekt zugeordneter Ecken zugewiesen

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.

Skizze der Winkel, die jeder Ecke zugeordnet sind
Skizze der Winkel, die jeder Ecke zugeordnet sind

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)
Animierte rotierende verdrehte Vierecker mit korrekt zugewiesenen Ecken mit der Winkelzuweisungsmethode
Animierte rotierende verdrehte Vierecker mit korrekt zugewiesenen Ecken mit der Winkelzuweisungsmethode

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.

Animation der Anwendung der Perspektiventransformation
Animation der Anwendung der Perspektiventransformation

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!

Letzte flache Prime-Down-Ansicht des Sudoku Sq. nach Perspektivenumwandlung

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.

Von admin

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert