In diesem Jahr wurde ein Othello-Adventskalender geplant und eingeladen, daher habe ich beschlossen, einen Artikel zu schreiben. Eigentlich habe ich am [Tag 1] einen weiteren Artikel geschrieben (https://qiita.com/lavox/items/e21d32d2931a24cfdc97), aber ich wusste nicht, was ich tat, und habe diesmal plötzlich den Artikel eingegeben Eine kleine Selbsteinführung.
Ich entwickle eine App für iOS namens [Kifu Box](https://itunes.apple.com/jp/app/Kifu box / id1434692901? Mt = 8). Als eine der Funktionen haben wir eine Funktion integriert, um ein Bild der Othello-Platine mit einer Kamera aufzunehmen und den Auswertungswert mit AR auf die Platine zu legen. Klicken Sie auf das folgende Bild, um das Einführungsvideo zu sehen. Es hat auch die Möglichkeit, ein Bild von der Tafel aufzunehmen (oder das aufgenommene Bild auszuwählen) und die Platzierung der Steine auf der Tafel zu erkennen. Dies hat auch ein Einführungsvideo.
In diesem Artikel möchte ich erklären, wie diese Funktionen die Oberfläche der Othello-Platine erkennen. Wenn Sie das Bild links unten eingeben, besteht das Ziel darin, die Position von schwarzen und weißen Steinen sowie den Status von Leerstellen zu erkennen, wie im Bild rechts gezeigt.
Da die eigentliche Anwendung iOS ist, habe ich sie mit Objective C + OpenCV implementiert, aber in diesem Artikel werde ich sie mit der Python-Version erklären. Der Inhalt selbst entspricht fast der App-Version.
Bitte beziehen Sie sich auf die gesamte Python-Version des folgenden Quellcodes. https://github.com/lavox/reversi_recognition
Es gibt zwei Hauptansätze zum Erkennen von Bildern.
In Bezug auf Ersteres schien es schwierig zu sein, eine große Anzahl von Lehrerbildern zum Lernen zu sammeln, und es schien schwierig zu stimmen, wenn etwas schief ging, und so entschied ich mich für Letzteres. Die Bilderkennung erfolgt in den folgenden Schritten.
In der Funktion zum Aufnehmen eines Bildes mit der Kamera und zum Aufnehmen der Karte wird nach Ausführung von 1 der Bildschirm zur Bereichsbestätigung angezeigt und dann 2 bis 4, in der AR-Funktion werden jedoch 1 bis 4 gleichzeitig ausgeführt.
import cv2
image = cv2.imread("./sample.jpg ")
image = cv2.blur(image,(3,3))
Das Bild wird in einer Größe von 3x3 gelesen und geglättet (unscharf). Es scheint die etablierte Theorie zu sein, das Bild bei der Analyse des Bildes im Voraus zu glätten, also habe ich es getan.
Im Fall von Python ist das geladene Bild ein Array von NumPy mit 3 Elementen für jedes Pixel. Beachten Sie jedoch, dass die Reihenfolge der 3 Elemente BGR anstelle von RGB ist. Bei der Anzeige mit matplotlib usw. ist der Farbton seltsam, es sei denn, das Format (Farbreihenfolge) wird geändert.
rgbImage = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
Da die Oberfläche des Othello-Bretts grün ist, habe ich mich entschlossen, die Grünfläche zu extrahieren. Da es schwierig ist, den Farbbereich im BGR-Format zu extrahieren, konvertieren Sie ihn in das HSV-Format.
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
Da es zum HSV-Format wurde, habe ich beschlossen, den Bereich des Farbbereichs mit H anzugeben, und S und V betrachten den Bereich mit einem bestimmten Wert oder mehr als grün.
Ich wollte es wirklich gekrümmt machen, aber da die Verarbeitung mühsam ist, wird der Bereich, in dem die obere rechte Seite der beiden Bereiche kombiniert wird, als "grün" bezeichnet. Der Grenzwert wurde durch Versuch und Irrtum bestimmt, während das Bild tatsächlich betrachtet wurde.
lower = np.array([45,89,30])
upper = np.array([90,255,255])
green1 = cv2.inRange(hsv,lower,upper)
lower = np.array([45,64,89])
upper = np.array([90,255,255])
green2 = cv2.inRange(hsv,lower,upper)
green = cv2.bitwise_or(green1,green2)
In OpenCV können Sie den Bereich mit inRange
angeben. bitwise_or
ist buchstäblich ein ODER zwischen Regionen.
Die Grünfläche wurde extrahiert. Von hier aus möchte ich die Koordinaten der Eckpunkte an den vier Ecken der Tafel finden, aber die Frage ist, wie man sie aus diesem binärisierten Bild findet. Ich mache mir auch Sorgen, dass es mehrere Boards gibt. Nachdem ich verschiedene Dinge durchsucht hatte, fand ich eine Funktion namens "findContours", die Konturen erkennt, also werde ich sie verwenden.
contours, hierarchy = cv2.findContours(green, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
Infolgedessen wurden mehr als 2000 Konturen extrahiert. Was ist passiert? Ich habe einige große Umrisse gezeichnet. Es war durch schwarze Linien und Steine auf den Quadraten getrennt. Bei Betrachtung der feineren Konturen stellte ich fest, dass die Konturen nur aus einem Pixel bestanden und dass diese "findContours" die Konturen ziemlich streng beurteilt. Als ich im Internet nach Techniken zum Entfernen von Hindernissen wie schwarzen Linien suchte, stellte ich fest, dass Morphologiekonvertierung /html/py_tutorials/py_imgproc/py_morphological_ops/py_morphological_ops.html) Ich habe es gefunden. Mit dieser Methode können Sie feine Linien und Staub entfernen, indem Sie den Bereich einmal erweitern, um ihn zu vergrößern, und ihn dann erodieren.
Wie viel sollte es also expandieren und schrumpfen? Wenn ich es für den Fall berechnet habe, dass die Tafel voller Bilder ist, scheint die Dicke der Linie etwa 0,6 bis 0,7% der langen Seite zu betragen, also 0,35 auf der langen Seite, um diese von beiden Seiten zu erweitern und zu löschen Ich habe beschlossen, es% zu machen.
kernelSize = max(1, int(0.0035 * max(width, height))) * 2 + 1
kernel = np.ones((kernelSize, kernelSize), dtype=np.int)
green = cv2.dilate(green, kernel) #Erweiterung
green = cv2.erode(green, kernel) #Schrumpfen
Es gibt eine "* 2 + 1", die etwas unklar ist, aber es scheint, dass die Größe der Expansion und Kontraktion eine ungerade Zahl sein muss, daher ist es eine Korrektur, sie zu einer ungeraden Zahl zu machen. Es ist mir gelungen, die Linie zu löschen. Wenn jedoch die Anzahl der Steine zunimmt, scheint sie durch Steine geteilt zu werden, was ein bisschen gefährlich ist. Ich konnte nicht anders, also beschloss ich, den Sprung zu wagen und nicht nur den grünen Teil, sondern auch den weißen Teil als Teil des Bretts zu betrachten. (Der schwarze Teil ist wirklich schwarz, deshalb habe ich ihn gestoppt, weil er sich zu stark ausbreitet.)
lower = np.array([0, 0, 128])
upper = np.array(([180, 50, 255])
white = cv2.inRange(hsv, lower, upper)
greenWhite = cv2.bitwise_or(green, white)
Der Tisch ist ebenfalls weiß, daher mache ich mir ein wenig Sorgen, aber das Risiko einer Teilung hat erheblich abgenommen, und die Reichweite des Boards ist klar. Deshalb habe ich beschlossen, damit fortzufahren. Lassen Sie uns nun die Kontur erneut extrahieren.
contours, hierarchy = cv2.findContours(greenWhite, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
Es gibt eine Delle im Stein um die Seite, aber es ist nicht mehr geteilt. Was ich tun möchte ist
――Was kann ich gegen die Delle tun? ――Wie Sie die Kontur des Boards auswählen, auf das Sie achten, wenn viele Konturen extrahiert werden
Es ist so ein Ort. Als ich nach etwas suchte, das mit der ersten Delle nicht möglich war, konvexes Paket Es gab eine Methode (/py_contour_features/py_contour_features.html#convex-hull).
Wenn es konvex gemacht werden kann, ist es dasjenige, das den Mittelpunkt des Bildes in die extrahierte Kontur einbezieht (ich nahm an, dass es in der Mitte liegt, weil es genommen werden sollte, um die Plattenoberfläche zu erkennen), und wenn es das zweite ist Es scheint möglich zu sein, zu klären, welche Kontur ausgewählt werden soll, was ein Problem war.
for i, c in enumerate(contours):
hull = cv2.convexHull(c) #Konvexes Paket
if cv2.pointPolygonTest(hull,(width / 2 , height / 2), False) > 0: #Bestimmen Sie, ob es das Zentrum enthält
mask = np.zeros((height, width),dtype=np.uint8)
cv2.fillPoly(mask, pts=[hull], color=(255)) #Bild innerhalb des Konturbereichs gefüllt
break
Vorerst wird die Fläche der Platte erhalten. Da ich jedoch einen weißen Teil hinzugefügt habe, um eine Teilung zu verhindern, bin ich etwas besorgt, wenn es sich um einen zusätzlichen Bereich handelt. Also habe ich versucht, den grünen Teil in diesem Bereich zu extrahieren, den Umriss zu nehmen und alle zu verbinden.
green = cv2.bitwise_and(green, green, mask=mask)
contours, hierarchy = cv2.findContours(green, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
greenContours = functools.reduce(lambda x, y: np.append(x, y,axis=0),contours)
hull = cv2.convexHull(greenContours)
Ich dachte, dass der Umriss damit sicher extrahiert werden könnte, aber als ich den Wert anzeigte, war es ein 26-seitiges Quadrat.
> print(hull.shape)
(26, 1, 2)
Obwohl es fast gerade aussieht, scheint es tatsächlich Eckpunkte dazwischen zu geben. OpenCV hatte auch eine Gegenmaßnahme für diese Situation. Eine Funktion, die sich einem Polygon namens "approxPolyDP" annähert.
epsilon = 0.004 * cv2.arcLength(hull, True)
approx = cv2.approxPolyDP(hull, epsilon, True)
Als Ergebnis von Versuch und Irrtum scheint es, dass es in den meisten Fällen zu einem Quadrat mit einem guten Gefühl wird, indem 0,4% des Umfangs als Fehler eingestellt werden.
> print(approx)
[[[2723 2702]]
[[ 675 2669]]
[[1045 1639]]
[[2418 1613]]]
Damit konnte ich die vier Eckpunkte der Tafel finden, aber ich möchte einige Details ansprechen, die hier nicht geschrieben werden konnten.
Ich werde die in Schritt 1 erhaltene Tafel in ein Quadrat umwandeln, aber für die spätere Verarbeitung werde ich einen Rand (blauer Rahmen) darum geben. Der Zweck der Marge ist wie folgt.
margin = 13 #Randbreite
cell_size = 42 #1 Quadratmeter groß
size = cell_size * 8 + margin * 2 #Seitenlänge nach dem Umbau
outer = (254, 0, 0) #Randfarbe
Das Konvertieren in ein Quadrat ist eigentlich einfach. Wenn Sie die Quell- und Zielscheitelpunkte kennen, können Sie eine Projektionskonvertierung durchführen.
#4 Eckpunkte der Quelle
src = np.array([[1045,1639], [2418,1613], [2723,2702], [ 675,2669]], dtype=np.float32)
#4 Eckpunkte zum Verschieben
dst = np.array([
[margin, margin],
[size - 1 - margin, margin],
[size - 1 - margin, size - 1 - margin],
[margin, size - 1 - margin]
], dtype=np.float32)
#Transformationsmatrix
trans = cv2.getPerspectiveTransform(src, dst)
#Projektionskonvertierung
board = cv2.warpPerspective(image, trans, (int(size), int(size)), flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_CONSTANT, borderValue=outer)
Hinweis: Die obige äußere Projektionskonvertierung ist eine Farbe, die den hervorstehenden Teil ausgleicht, wenn die vier Eckpunkte aus dem Bild herausragen. Es füllt die Ränder nicht aus. Also werde ich auch den Randteil malen.
cv2.rectangle(board, (0, 0), (size - 1, margin), outer, -1)
cv2.rectangle(board, (0, 0), (margin, size - 1), outer, -1)
cv2.rectangle(board, (size - margin, 0), (size - 1, size - 1), outer, -1)
cv2.rectangle(board, (0, size - margin), (size - 1, size - 1), outer, -1)
Sie haben jetzt das Brett quadratisch gemacht.
Als ich den Stein fand, fragte ich mich, ob ich annehmen sollte, dass sich der Stein in der Mitte des Platzes befand. Da es sich um eine tatsächliche Tafel handelt, gibt es Fälle, in denen der Stein außerhalb der Mitte platziert wird. Wenn das Bild aus einem Winkel aufgenommen wird, kann die Mitte aufgrund der Dicke des Steins im Bild nach der Umwandlung in ein Quadrat versetzt sein. Also beschloss ich fortzufahren, ohne anzunehmen, dass sich der Stein in der Mitte des Platzes befindet.
Um die Position des Steins zu bestimmen, dachte ich zuerst, dass es besser wäre, eine Huff-Transformation zu verwenden, die den Kreis erkennt, weil der Stein kreisförmig ist. Als ich es jedoch tatsächlich versuchte, entdeckte ich eine große Anzahl mysteriöser Kreise, die für das menschliche Auge unsichtbar waren, und die Anpassung war frustriert, weil es viele Parameter gab. * Außerdem habe ich in der Anwendung ScoreNow vom Entwickler gehört, dass die Huff-Konvertierung zum Erkennen von Steinen verwendet wird, sodass ich sie gut angepasst habe. Ich denke du kannst es schaffen. * *
Nachdem ich unterschiedlich ratlos gesucht hatte, kam ich zu dem Artikel Details zum Objektsegmentierungsalgorithmus "Wasserscheide" und entschied mich, diese Methode auszuprobieren. tat. In diesem Fall suchen wir nicht nach dem genauen Umriss des Steins, sondern nach der Mitte des Steins, sodass wir nur einen Teil des verknüpften Artikels ausführen.
Um das Brett und den Stein zu trennen, extrahieren Sie zuerst das Brettteil im grünen Teil wie in Schritt 1.
hsv = cv2.cvtColor(board, cv2.COLOR_BGR2HSV)
lower = np.array([45,89,30])
upper = np.array([90,255,255])
green1 = cv2.inRange(hsv,lower,upper)
lower = np.array([45,64,89])
upper = np.array([90,255,255])
green2 = cv2.inRange(hsv,lower,upper)
green = cv2.bitwise_or(green1,green2)
Sie können den Steinteil extrahieren, indem Sie den blauen Teil des Randes hinzufügen und dann umdrehen.
outer = cv2.inRange(board, (254, 0, 0), (254, 0, 0))
green = cv2.bitwise_or(green, outer)
disc = cv2.bitwise_not(green)
Die Linie des Quadrats bleibt erhalten, hat jedoch keinen Einfluss auf die nachfolgende Verarbeitung. Lassen Sie es also so, wie es diesmal ist.
In Anbetracht der Möglichkeit, dass menschliche Hände tatsächlich reflektiert werden, gibt es auch einen Prozess, der den Teil, der weder grün, weiß noch schwarz ist, als "unbekannte" Zelle behandelt, aber er wird lang sein, also werde ich ihn weglassen. ..
Finden Sie nun laut Qiitas früherem Artikel den Abstand von der Außenseite des Steins zu jedem Punkt.
dist = cv2.distanceTransform(disc, cv2.DIST_L2, 5)
Der Punkt mit dem Maximalwert jeder Insel (?) Ist wahrscheinlich das Zentrum des Steins. Da jedoch einige der Inseln jetzt verbunden sind, werden Punkte über einem bestimmten Wert extrahiert und getrennt. Wenn der "einige Wert" zu groß ist, können nicht erkennbare Steine auftreten. Wenn er jedoch zu klein ist, wird die Verbindung möglicherweise nicht getrennt, und als Ergebnis von Versuch und Irrtum wurde 13.0 als Schwellenwert festgelegt.
_, distThr = cv2.threshold(dist, 13.0, 255.0, cv2.THRESH_BINARY)
Der maximale Punkt jedes Verbindungsbereichs sollte die Mitte des Steins sein, also finden Sie ihn.
distThr = np.uint8(distThr) #Typkonvertierung erforderlich
#Holen Sie sich die Verknüpfungskomponente
labelnum, labelimg, data, center = cv2.connectedComponentsWithStats(distThr)
for i in range(1, labelnum): # i =Ausschließen, da 0 der Hintergrund ist
x, y, w, h, s = data[i]
distComponent = dist[y:y+h, x:x+w]
maxCoord = np.unravel_index(distComponent.argmax(), distComponent.shape) + np.array([y, x])
Da die Inseln nicht gut getrennt sind und bimodal werden, führt die eigentliche Quelle außerdem eine etwas komplizierte Schleife durch, während die Umgebung des Maximalwerts gelöscht wird. Hier ist dies jedoch einfach Es hat sich verwandelt. Ich konnte die Mitte des Steins mit angemessener Genauigkeit finden. Als Anliegen sind die Steine wie Trommeln anstelle von Kreisen geformt, da die diagonalen Fotos in Quadrate gezwungen werden, vorzugsweise liegt die Farbe des Steins nahe der Mitte der Oberseite und die Position des Steins in der Mitte der Unterseite. Ich möchte beurteilen. Um dies zu erreichen, wird basierend auf der Kameraposition, die zum Zeitpunkt der Plattenextraktion erhalten wurde, die Mitte des Steins von der Kamera zur anderen Seite und zur nahen Seite verschoben und als die Mitte der oberen und unteren Oberfläche betrachtet, aber sie wird länger. Also (obwohl es schon lang genug ist ...) werde ich diesmal die Erklärung weglassen.
Jetzt ist es Zeit, die Farbe des Steins zu beurteilen. Grundsätzlich scheint es gut zu sein, die Helligkeit der Farbe nahe der Mitte des Steins zu beurteilen, aber als ich es tatsächlich versuchte, traten einige Probleme auf.
In Bezug auf den zweiten Punkt stellte ich die Bedingung zunächst durch Vergleichen der Helligkeit mit der Umgebung her, fand danach jedoch eine einfachere Methode zur Beurteilung mit OpenCV. Dies ist der Prozess der Binärisierung basierend auf der Umgebungssituation, die als adaptive Schwelle bezeichnet wird. Damit werden weiße Steine unabhängig von der Helligkeit der Umgebung weiß.
grayBoard = cv2.cvtColor(board, cv2.COLOR_BGR2GRAY)
binBoardWide = cv2.adaptiveThreshold(grayBoard, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 127, -20)
Nach der Graustufe führe ich die adaptive Schwelle aus. Jetzt sind die weißen Steine ziemlich weiß, aber es gibt immer noch einige schwarze Steine, die weißlich sind, deshalb würde ich gerne etwas mehr erfinden. Es ist schwierig, einen Stein zu beurteilen, der vollständig reflektiert wird. Wenn Sie sich das Bild jedoch genau ansehen, wird deutlich, dass es einige "Farbunebenheiten" gibt, wenn es sich um einen schwarzen Stein handelt, der leicht reflektiert wird. Es wurde festgestellt, dass diese Farbungleichmäßigkeit durch Verengen des als "Umgebung" betrachteten Bereichs durch Verarbeiten der adaptiven Schwelle erfasst werden kann. Der Block ist der Bereich, der als "Umgebung" betrachtet wird.
binBoardWide = cv2.adaptiveThreshold(grayBoard, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 127, -20)
binBoardNarrow = cv2.adaptiveThreshold(grayBoard, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 7, 2)
#Als Ergebnis von Versuch und Irrtum wurde es nach dem Verwischen mit einem Schwellenwert binärisiert.
binBoardNarrow = cv2.blur(binBoardNarrow, (3, 3))
_, binBoardNarrow = cv2.threshold(binBoardNarrow, 168, 255, cv2.THRESH_BINARY)
Wenn Block = 127 breit ist, ist ein ziemlich breiter Bereich weiß, aber wenn er schmal wie Block = 7 ist, treten Farbunebenheiten auf. Außerdem ist es gut, dass Shiraishi weiß bleibt.
Daher habe ich beschlossen, die Farbe des Steins wie folgt zu bestimmen, je nachdem, wie viele schwarze Pixel sich in dem Bereich mit einem Radius von 10 vom Mittelpunkt (Oberseite) des Steins befinden, der in Schritt 3 erhalten wurde.
―― 10 oder mehr für "breit" oder 26 oder mehr für "schmal" → Kuroishi
Diese Schwelle wurde auch aus der Tendenz der Verteilung bestimmt, indem die Anzahl der schwarzen Pixel aus dem tatsächlichen Bild mit schwarzen und weißen Steinen aufgetragen wurde.
Wie genau ist die Genauigkeit, nachdem Sie das Othello-Board erkannt haben? Als ich die 76 Bilder erkannte, die ich hatte, waren die Ergebnisse wie folgt.
--67 Blatt: Alle Steine richtig beurteilen --1 Blatt: Der Bereich der Karte konnte nicht erkannt werden (die Karte wurde nicht in der Mitte angezeigt). ―― 8 Blatt: Fehleinschätzung der Farbe mit insgesamt 14 Steinen
Da es jedoch viele Bilder enthält, die bei der Entwicklung der Erkennungslogik zum Ausprobieren verwendet werden, denke ich, dass die tatsächliche Genauigkeit etwas geringer ist.
Hier sind einige Fälle, die ich nicht gut erkennen konnte. Ich habe festgestellt, dass a5 ein leerer Raum und h3 Shiraishi ist, aber die richtigen Antworten sind beide Kuroishi. Es scheint, dass es bei einem Foto, das leicht unscharf ist, nicht gut erkannt werden kann.
Ich habe den Stein von e3 als schwarz beurteilt, aber die richtige Antwort ist Shiraishi, wie Sie sehen können. Abhängig vom Material der Tafel kann die Tafel selbst reflektieren, aber es scheint, dass sie für die Reflexion der Tafel anfällig ist, wahrscheinlich weil die Tafel grün beurteilt wird. Ich habe festgestellt, dass die Steine von e7, e8 und f8 weiße Steine sind, aber die richtige Antwort sind die reflektierten schwarzen Steine. Da die Reflexion so intensiv ist, ist es für das menschliche Auge schwierig, ein Urteil zu fällen, aber dieser Algorithmus konnte kein korrektes Urteil fällen.Was die Erkennungsleistung betrifft, ist die Leistung angemessen, wenn die lange Seite auf 1024 Pixel geändert und dann erkannt wird. Solange sie auf dem iPhone 8 ausgeführt wird, scheint sie mehrmals pro Sekunde erkannt zu werden.
Das ist alles, um das tatsächliche Othello-Board zu erkennen. Wenn Sie jedoch die Funktion zum Erkennen des Othello-Boards in der App implementieren,
――Ich möchte auch die Möglichkeit haben, Screenshots anderer Apps zu erkennen ――Ich möchte, dass eine Funktion die Tafel mit schwarz-weiß gedruckten Büchern erkennt
Es bedeutet das. Für Screenshots kann zum Teil derselbe Algorithmus wie für die eigentliche Karte verwendet werden, aber es gab Fälle, in denen er ohne besondere Berücksichtigung nicht gut erkannt werden konnte. Wenn es um schwarz-weiß gedruckte Bücher geht, kann die Strategie, die Tafeloberfläche in Grün zu beurteilen, nicht grundsätzlich angewendet werden. Sie ist daher nutzlos, wenn sie nicht von dort aus überprüft wird.
Ich habe diese auf eine andere Weise implementiert, aber ich lasse es hier, da die schlampigen Artikel länger werden. Der Quellcode enthält Screenshots und Bücher. Wenn Sie also interessiert sind, schauen Sie bitte dort nach.