5.27. Kreise und Rechtecke finden

Linien und Segmente erfassen die geraden Kanten im aufgenommenen Einzelbild, doch viele reale Merkmale, nach denen die Kamera sucht, sind nicht gerade. Eine auf einem Schreibtisch liegende Münze ist ein Kreis. Ein gedrucktes Label, eine Haftnotiz, die Oberseite einer schräg betrachteten Schachtel ist ein Viereck. Das image-Modul stellt für jeden Fall einen eigenen Detektor bereit: eine Suche im Hough-Stil für Kreise und eine von AprilTag abgeleitete Suche für vierseitige Formen.

Beide Methoden folgen derselben Vorlage wie die Linien-Detektoren – threshold steuert, wie viele Stimmen eine Erkennung benötigt, roi grenzt die Suche ein, und die zurückgegebenen Objekte tragen sowohl eine Position als auch einen Vertrauenswert – doch die Parameterräume und die richtigen Standardwerte unterscheiden sich genug, um eine ausdrückliche Behandlung zu verdienen.

5.27.1. Hough-Kreise

find_circles() führt die kreisförmige Variante der Hough-Transformation aus. Jeder Kantenpixel aus dem vorgeschalteten Sobel-Durchlauf stimmt für jeden Kreis ab, der durch ihn verlaufen könnte; Kreise, die genügend Stimmen sammeln, werden zurückgegeben.

circles = img.find_circles(threshold=3500,
                            x_margin=10, y_margin=10, r_margin=10,
                            r_min=10, r_max=80, r_step=2)

for c in circles:
    img.draw_circle((c.x, c.y, c.r), color=(255, 0, 0))

threshold ist die minimale Summe der Sobel-Kantenstärken entlang des Kandidatenkreises. Größere Kreise durchlaufen mehr Pixel und benötigen daher höhere Schwellenwerte, um durchzukommen; ein Wert, der eine Münze mit 20 Pixel Radius findet, schlägt auch bei Rauschen rund um eine 100-Pixel-Kante an, während ein auf die große Münze abgestimmter Wert die kleine verfehlt. Wenn der Zielradiusbereich bekannt ist, skaliert der richtige Schwellenwert mit dem Umfang – ungefähr threshold = 50 * 2 * pi * r liefert einen vernünftigen Ausgangspunkt, und der richtige Wert ergibt sich aus einem kurzen Abstimmungsdurchlauf.

r_min, r_max und r_step legen die Radiussuche fest. Ohne Grenzen würde der Detektor jeden Radius von wenigen Pixeln bis zur halben Bildbreite durchsuchen, was sowohl langsam ist als auch ein Rezept für Fehlerkennungen. Wenn man r_min und r_max so setzt, dass sie die erwartete Zielgröße mit großzügigem Spielraum einrahmen (z. B. r_min=15, r_max=25 für eine Münze, von der bekannt ist, dass sie etwa 20 Pixel misst), reduziert das den Aufwand erheblich und verbessert das Signal-Rausch-Verhältnis der Stimmen. r_step steuert die Granularität der Suche; größere Schritte laufen schneller und können einen Kreis verfehlen, dessen wahrer Radius zwischen zwei abgetasteten Werten liegt. Der Standardwert r_step=2 ist ein vernünftiger Kompromiss.

x_margin, y_margin und r_margin steuern das Zusammenführen benachbarter Erkennungen, so wie es theta_margin und rho_margin bei der Linienerkennung tun. Ein einzelner physischer Kreis im Bild stimmt für ein Bündel von Kandidatenkreisen, deren Mittelpunkte und Radien bis auf wenige Pixel übereinstimmen; die Margins reduzieren jedes Bündel auf seinen Höhepunkt, bevor die Ergebnisliste erstellt wird. Größere Margins liefern weniger, dafür vertrauenswürdigere Erkennungen; kleinere Margins liefern mehr Erkennungen mit möglichen Beinahe-Duplikaten.

x_stride und y_stride schreiten den Abstimmungs-Scan voran, genauso wie sie es in den anderen Detektoren tun. Der Standardwert von 2 und 1 ist für die meisten Bilder geeignet; beide auf 4 hochzudrehen ist der übliche Geschwindigkeitskompromiss für ein Bild, von dem bekannt ist, dass es große Kreise enthält.

Jeder zurückgegebene Circle trägt x, y, r (Mittelpunkt und Radius) und magnitude (die Stimmensumme, nützlich als Vertrauenswert zum Sortieren oder Filtern). Die Erkennung wieder ins Einzelbild zu zeichnen ist ein einziger Aufruf – draw_circle() nimmt das (x, y, r)-3-Tupel entgegen, das direkt über das Ergebnis als (c.x, c.y, c.r) verfügbar ist.

5.27.2. Rechtecke

find_rects() übernimmt den Viereck-Detektor aus der AprilTag-Pipeline – dieselbe Routine, die das schwarze Quadrat um einen Tag lokalisiert, wird eigenständig als universeller Rechteckfinder bereitgestellt.

rects = img.find_rects(threshold=12000)

for r in rects:
    img.draw_rectangle(r.rect, color=(0, 255, 0))
    for corner in r.corners:
        img.draw_circle((corner[0], corner[1], 4),
                        color=(0, 255, 0))

threshold ist die minimale Summe der Kantenstärken entlang des Rechteckumfangs. Ein gedrucktes schwarzes Rechteck auf weißem Grund in einem gut beleuchteten Einzelbild überschreitet leicht 10000; ein schwaches Rechteck auf strukturiertem Hintergrund muss möglicherweise auf 2000 abfallen – ein Abwägen von Fehlerkennungen gegen Empfindlichkeit. Wie beim Kreisdetektor ergibt sich der richtige Wert aus einem schnellen Abstimmungsdurchlauf mit den vorgesehenen Zielen im Blick.

Der Detektor ist projektiv – er findet Vierecke, deren Seiten gerade, aber nicht notwendigerweise parallel oder achsenparallel sind. Ein schräg betrachtetes Label sieht im Bild wie ein Trapez aus, und der Rechteckdetektor findet es korrekt; ein achsenparalleles Rechteck ist nur der entartete Sonderfall, bei dem die vier Ecken zufällig eine rechtwinklige Box bilden. roi schränkt die Suche ein; die übrigen Schlüsselwortargumente übernehmen ihre Standardwerte aus der AprilTag-Pipeline und müssen selten angepasst werden.

Jeder zurückgegebene Rect trägt den achsenparallelen Begrenzungsrahmen – x, y, w, h sowie das rect-4-Tupel, das draw_rectangle() erwartet – und die vier erkannten Ecken als corners. Der Begrenzungsrahmen ist das, was die Anwendung für grobe Position und Größe verwendet; die Ecken beschreiben das projektive Viereck selbst. Wenn die Kamera ein flaches Ziel aus einem Winkel betrachtet und die Anwendung die Trapezverzerrung rückgängig machen muss – Text auf einem Label lesen, Farbe von einer flachen Fläche abtasten – fließen die Ecken direkt über das Schlüsselwort corners= in rotation_corr() ein (siehe Objektiv- und Perspektivkorrektur), und das Ergebnis ist das entzerrte Rechteck, bereit für die jeweils anschließende Analyse.

Warnung

Da der Detektor auf das abgestimmt ist, was die AprilTag-Pipeline benötigt – Vierecke mit kräftigen, kontrastreichen Rändern, etwa ein schwarzer Tag-Umriss auf weißem Papier – ist er kein Durchlauf, der jedes Rechteck findet. Rechtecke mit geringem Kontrast, strukturierten Kanten oder unruhiger Umgebung können vollständig unerkannt bleiben. Wie gut er funktioniert, hängt von der Situation ab: Teste ihn frühzeitig an den realen Zielen, bevor du eine Pipeline darum herum aufbaust.

5.27.3. Wenn der Detektor danebenliegt

Insbesondere Kreise profitieren von einem Vorfilter auf der Eingabe. Ein verrauschtes Einzelbild liefert viele verstreute Kantenpixel, die alle mit abstimmen, und der resultierende Hough-Raum hat breite, verwaschene Spitzen, die der Zusammenführer nur schwer trennen kann. Ein gaussian()- oder mean()-Durchlauf vor find_circles() glättet das Rauschen weg und lässt die echten Kanten intakt; der Detektor liefert sauberere Spitzen in kürzerer Zeit.

Bei Rechtecken ist der häufige Fehlerfall der gegenteilige: zu geringer Kontrast zwischen dem Rechteck und seinem Hintergrund bedeutet, dass die Summe der Kantenstärken den threshold nie überschreitet. Ein histeq()-Durchlauf, der den Helligkeitsbereich über die volle Spanne von 0 bis 255 umverteilt, stellt den vom Detektor benötigten Kontrast wieder her. (Der Kontrast muss irgendwo im Bild vorhanden sein; die Histogrammausgleichung kann nur verstärken, was bereits da ist.)