
Auf der Suche nach einem Ziel
Wir gehen zur thematischen Seite miniclip.com und suchen nach einem Ziel. Die Wahl fiel auf das Farbpuzzle Coloruid 2 im Abschnitt Puzzles, in dem wir ein rundes Spielfeld mit einer Farbe in einer bestimmten Anzahl von Zügen füllen müssen.
Ein beliebiger Bereich wird mit der am unteren Bildschirmrand ausgewählten Farbe gefüllt, während benachbarte Bereiche derselben Farbe zu einem einzigen zusammengeführt werden.

Ausbildung
Wir werden Python verwenden. Der Bot wurde ausschließlich zu Bildungszwecken erstellt. Der Artikel richtet sich an Anfänger im Bereich Computer Vision, die ich selbst bin.
Das Spiel befindet sich hier
GitHub des Bots hier
Damit der Bot funktioniert, benötigen wir folgende Module:
- opencv-python
- Kissen
- Selen
Der Bot wurde für Python 3.8 unter Ubuntu 20.04.1 geschrieben und getestet. Wir installieren die erforderlichen Module in Ihrer virtuellen Umgebung oder per Pip-Installation. Damit Selen funktioniert, benötigen wir außerdem einen Geckodriver für FireFox. Sie können ihn hier herunterladen: github.com/mozilla/geckodriver/releases
Browsersteuerung
Wir haben es mit einem Online-Spiel zu tun, also organisieren wir zuerst die Interaktion mit dem Browser. Zu diesem Zweck verwenden wir Selenium, das uns eine API zur Verwaltung von FireFox zur Verfügung stellt. Untersuchen des Codes der Spieleseite. Das Puzzle ist eine Leinwand, die sich wiederum in einem Iframe befindet.
Wir warten, bis der Frame mit id = iframe-game geladen ist, und schalten den Treiberkontext darauf um. Dann warten wir auf Leinwand. Es ist das einzige im Frame und über XPath / html / body / canvas verfügbar.
wait(self.__driver, 20).until(EC.frame_to_be_available_and_switch_to_it((By.ID, "iframe-game")))
self.__canvas = wait(self.__driver, 20).until(EC.visibility_of_element_located((By.XPATH, "/html/body/canvas")))
Als nächstes wird unsere Zeichenfläche über die Eigenschaft self .__ canvas verfügbar sein. Die gesamte Logik der Arbeit mit dem Browser besteht darin, einen Screenshot der Leinwand zu erstellen und an einer bestimmten Koordinate darauf zu klicken.
Vollständiger Browser.py-Code:
from selenium import webdriver
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait as wait
from selenium.webdriver.common.by import By
class Browser:
def __init__(self, game_url):
self.__driver = webdriver.Firefox()
self.__driver.get(game_url)
wait(self.__driver, 20).until(EC.frame_to_be_available_and_switch_to_it((By.ID, "iframe-game")))
self.__canvas = wait(self.__driver, 20).until(EC.visibility_of_element_located((By.XPATH, "/html/body/canvas")))
def screenshot(self):
return self.__canvas.screenshot_as_png
def quit(self):
self.__driver.quit()
def click(self, click_point):
action = webdriver.common.action_chains.ActionChains(self.__driver)
action.move_to_element_with_offset(self.__canvas, click_point[0], click_point[1]).click().perform()
Spielzustände
Kommen wir zum Spiel selbst. Die gesamte Bot-Logik wird in der Robot-Klasse implementiert. Teilen wir das Gameplay in 7 Zustände ein und weisen ihnen Methoden zu, um sie zu verarbeiten. Lassen Sie uns die Trainingsstufe separat auswählen. Es enthält einen großen weißen Cursor, der angibt, wo geklickt werden soll, wodurch verhindert wird, dass das Spiel korrekt erkannt wird.
- Begrüßungsbildschirm
- Ebenenauswahlbildschirm
- Farbauswahl auf Tutorial-Ebene
- Auswahl eines Bereichs auf Unterrichtsebene
- Farbauswahl
- Regionsauswahl
- Ergebnis des Umzugs
class Robot:
STATE_START = 0x01
STATE_SELECT_LEVEL = 0x02
STATE_TRAINING_SELECT_COLOR = 0x03
STATE_TRAINING_SELECT_AREA = 0x04
STATE_GAME_SELECT_COLOR = 0x05
STATE_GAME_SELECT_AREA = 0x06
STATE_GAME_RESULT = 0x07
def __init__(self):
self.states = {
self.STATE_START: self.state_start,
self.STATE_SELECT_LEVEL: self.state_select_level,
self.STATE_TRAINING_SELECT_COLOR: self.state_training_select_color,
self.STATE_TRAINING_SELECT_AREA: self.state_training_select_area,
self.STATE_GAME_RESULT: self.state_game_result,
self.STATE_GAME_SELECT_COLOR: self.state_game_select_color,
self.STATE_GAME_SELECT_AREA: self.state_game_select_area,
}
Für eine größere Stabilität des Bots prüfen wir, ob die Änderung des Spielstatus erfolgreich erfolgt ist. Wenn self.state_next_success_condition während self.state_timeout nicht True zurückgibt, verarbeiten wir den aktuellen Status weiter, andernfalls wechseln wir zu self.state_next. Wir werden auch den von Selenium erhaltenen Screenshot in ein Format übersetzen, das OpenCV versteht.
import time
import cv2
import numpy
from PIL import Image
from io import BytesIO
class Robot:
def __init__(self):
# …
self.screenshot = []
self.state_next_success_condition = None
self.state_start_time = 0
self.state_timeout = 0
self.state_current = 0
self.state_next = 0
def run(self, screenshot):
self.screenshot = cv2.cvtColor(numpy.array(Image.open(BytesIO(screenshot))), cv2.COLOR_BGR2RGB)
if self.state_current != self.state_next:
if self.state_next_success_condition():
self.set_state_current()
elif time.time() - self.state_start_time >= self.state_timeout
self.state_next = self.state_current
return False
else:
try:
return self.states[self.state_current]()
except KeyError:
self.__del__()
def set_state_current(self):
self.state_current = self.state_next
def set_state_next(self, state_next, state_next_success_condition, state_timeout):
self.state_next_success_condition = state_next_success_condition
self.state_start_time = time.time()
self.state_timeout = state_timeout
self.state_next = state_next
Lassen Sie uns die Prüfung in den Statusbehandlungsmethoden implementieren. Wir warten auf die Schaltfläche Wiedergabe auf dem Startbildschirm und klicken darauf. Wenn wir innerhalb von 10 Sekunden den Ebenenauswahlbildschirm nicht erhalten haben, kehren wir zur vorherigen Stufe self.STATE_START zurück, andernfalls fahren wir mit der Verarbeitung von self.STATE_SELECT_LEVEL fort.
# …
class Robot:
DEFAULT_STATE_TIMEOUT = 10
# …
def state_start(self):
# Play
# …
if button_play is False:
return False
self.set_state_next(self.STATE_SELECT_LEVEL, self.state_select_level_condition, self.DEFAULT_STATE_TIMEOUT)
return button_play
def state_select_level_condition(self):
#
# …
Bot Vision
Bildschwellenwert
Definieren wir die Farben, die im Spiel verwendet werden. Dies sind 5 spielbare Farben und eine Cursorfarbe für das Tutorial-Level. Wir werden COLOR_ALL verwenden, wenn wir alle Objekte finden müssen, unabhängig von der Farbe. Zunächst werden wir diesen Fall betrachten.
COLOR_BLUE = 0x01
COLOR_ORANGE = 0x02
COLOR_RED = 0x03
COLOR_GREEN = 0x04
COLOR_YELLOW = 0x05
COLOR_WHITE = 0x06
COLOR_ALL = 0x07
Um ein Objekt zu finden, müssen Sie zuerst das Bild vereinfachen. Nehmen wir zum Beispiel das Symbol "0" und wenden einen Schwellenwert an, dh wir trennen das Objekt vom Hintergrund. In diesem Stadium ist es uns egal, welche Farbe das Symbol hat. Zuerst konvertieren wir das Bild in Schwarzweiß und machen es 1-Kanal. Die Funktion cv2.cvtColor mit dem zweiten Argument cv2.COLOR_BGR2GRAY hilft uns dabei , die für die Konvertierung in Graustufen verantwortlich ist. Als nächstes führen wir einen Schwellenwert mit cv2.threshold durch . Alle Pixel des Bildes unterhalb eines bestimmten Schwellenwerts werden auf 0 gesetzt, alles oberhalb - auf 255. Das zweite Argument der Funktion cv2.threshold ist für den Schwellenwert verantwortlich . In unserem Fall kann eine beliebige Zahl vorhanden sein, da wir cv2.THRESH_OTSU verwenden und die Funktion selbst bestimmt den optimalen Schwellenwert durch die Otsu-Methode basierend auf dem Bildhistogramm.
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
_, thresh = cv2.threshold(image, 0, 255, cv2.THRESH_OTSU)

Farbsegmentierung
Weiter interessanter. Lassen Sie uns die Aufgabe komplizieren und alle roten Symbole auf dem Ebenenauswahlbildschirm finden.

Standardmäßig werden alle OpenCV-Bilder im BGR-Format gespeichert. HSV (Farbton, Sättigung, Wert - Farbton, Sättigung, Wert) eignet sich besser für die Farbsegmentierung. Sein Vorteil gegenüber RGB ist, dass HSV Farbe von seiner Sättigung und Helligkeit trennt. Der Farbton wird von einem Farbtonkanal codiert. Nehmen wir als Beispiel ein hellgrünes Rechteck und verringern dessen Helligkeit allmählich.

Im Gegensatz zu RGB sieht diese Transformation im HSV intuitiv aus - wir verringern lediglich den Wert des Wert- oder Helligkeitskanals. Hierbei ist zu beachten, dass im Referenzmodell die Farbton-Farbskala im Bereich von 0 bis 360 ° variiert. Unsere hellgrüne Farbe entspricht 90 °. Um diesen Wert in einen 8-Bit-Kanal einzupassen, sollte er durch 2 geteilt werden.
Die Farbsegmentierung funktioniert mit Bereichen, nicht mit einer einzelnen Farbe. Sie können den Bereich empirisch bestimmen, aber es ist einfacher, ein kleines Skript zu schreiben.
import cv2
import numpy as numpy
image_path = "tests_data/SELECT_LEVEL.png"
hsv_max_upper = 0, 0, 0
hsv_min_lower = 255, 255, 255
def bite_range(value):
value = 255 if value > 255 else value
return 0 if value < 0 else value
def pick_color(event, x, y, flags, param):
if event == cv2.EVENT_LBUTTONDOWN:
global hsv_max_upper
global hsv_min_lower
global image_hsv
hsv_pixel = image_hsv[y, x]
hsv_max_upper = bite_range(max(hsv_max_upper[0], hsv_pixel[0]) + 1), \
bite_range(max(hsv_max_upper[1], hsv_pixel[1]) + 1), \
bite_range(max(hsv_max_upper[2], hsv_pixel[2]) + 1)
hsv_min_lower = bite_range(min(hsv_min_lower[0], hsv_pixel[0]) - 1), \
bite_range(min(hsv_min_lower[1], hsv_pixel[1]) - 1), \
bite_range(min(hsv_min_lower[2], hsv_pixel[2]) - 1)
print('HSV range: ', (hsv_min_lower, hsv_max_upper))
hsv_mask = cv2.inRange(image_hsv, numpy.array(hsv_min_lower), numpy.array(hsv_max_upper))
cv2.imshow("HSV Mask", hsv_mask)
image = cv2.imread(image_path)
cv2.namedWindow('Original')
cv2.setMouseCallback('Original', pick_color)
image_hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
cv2.imshow("Original", image)
cv2.waitKey(0)
cv2.destroyAllWindows()
Lassen Sie es uns mit unserem Screenshot starten.

Klicken Sie auf die rote Farbe und sehen Sie sich die resultierende Maske an. Wenn die Ausgabe nicht zu uns passt, wählen wir die Rottöne, um den Bereich und die Fläche der Maske zu vergrößern. Das Skript basiert auf der Funktion cv2.inRange , die als Farbfilter fungiert und ein Schwellenwertbild für einen bestimmten Farbbereich zurückgibt.
Lassen Sie uns auf die folgenden Bereiche eingehen:
COLOR_HSV_RANGE = {
COLOR_BLUE: ((112, 151, 216), (128, 167, 255)),
COLOR_ORANGE: ((8, 251, 93), (14, 255, 255)),
COLOR_RED: ((167, 252, 223), (171, 255, 255)),
COLOR_GREEN: ((71, 251, 98), (77, 255, 211)),
COLOR_YELLOW: ((27, 252, 51), (33, 255, 211)),
COLOR_WHITE: ((0, 0, 159), (7, 7, 255)),
}
Konturen finden
Kehren wir zu unserem Level-Auswahlbildschirm zurück. Wenden wir den soeben definierten Rotbereichsfarbfilter an und übergeben den gefundenen Schwellenwert an cv2.findContours . Die Funktion findet die Umrisse der roten Elemente. Wir geben cv2.RETR_EXTERNAL als zweites Argument an - wir brauchen nur äußere Konturen und als drittes cv2.CHAIN_APPROX_SIMPLE - wir sind an geraden Konturen interessiert, speichern Speicher und speichern nur ihre Eckpunkte.
thresh = cv2.inRange(image, self.COLOR_HSV_RANGE[self.COLOR_RED][0], self.COLOR_HSV_RANGE[self.COLOR_RED][1])
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE

Lärm entfernen
Die resultierenden Konturen enthalten viel Hintergrundgeräusch. Um es zu entfernen, verwenden wir die Eigenschaft unserer Zahlen. Sie bestehen aus Rechtecken, die parallel zu den Koordinatenachsen verlaufen. Wir durchlaufen alle Pfade und passen sie mit cv2.minAreaRect in das minimale Rechteck ein . Das Rechteck wird durch 4 Punkte definiert. Wenn unser Rechteck parallel zu den Achsen ist, muss eine der Koordinaten für jedes Punktpaar übereinstimmen. Dies bedeutet, dass wir maximal 4 eindeutige Werte haben, wenn wir die Koordinaten des Rechtecks als eindimensionales Array darstellen. Außerdem filtern wir zu lange Rechtecke heraus, bei denen das Seitenverhältnis größer als 3 zu 1 ist. Dazu ermitteln wir ihre Breite und Länge mithilfe von cv2.boundingRect .
squares = []
for cnt in contours:
rect = cv2.minAreaRect(cnt)
square = cv2.boxPoints(rect)
square = numpy.int0(square)
(_, _, w, h) = cv2.boundingRect(square)
a = max(w, h)
b = min(w, h)
if numpy.unique(square).shape[0] <= 4 and a <= b * 3:
squares.append(numpy.array([[square[0]], [square[1]], [square[2]], [square[3]]]))

Konturen kombinieren
Schon besser. Jetzt müssen wir die gefundenen Rechtecke zu einem gemeinsamen Umriss von Symbolen kombinieren. Wir brauchen ein Zwischenbild. Erstellen wir es mit numpy.zeros_like . Die Funktion erstellt eine Kopie der Bildmatrix unter Beibehaltung ihrer Form und Größe und füllt sie dann mit Nullen. Mit anderen Worten, wir haben eine Kopie unseres Originalbildes mit einem schwarzen Hintergrund erhalten. Wir konvertieren es in einen Kanal und wenden die gefundenen Konturen mit cv2.drawContours an und füllen sie mit Weiß. Wir erhalten einen binären Schwellenwert, auf den wir cv2.dilate anwenden können . Die Funktion erweitert den weißen Bereich durch Verbinden separater Rechtecke, deren Abstand innerhalb von 5 Pixeln liegt. Ich rufe noch einmal cv2.findContours auf und erhalte die Konturen der roten Zahlen.
image_zero = numpy.zeros_like(image)
image_zero = cv2.cvtColor(image_zero, cv2.COLOR_BGR2RGB)
cv2.drawContours(image_zero, contours_of_squares, -1, (255, 255, 255), -1)
_, thresh = cv2.threshold(image_zero, 0, 255, cv2.THRESH_OTSU)
kernel = numpy.ones((5, 5), numpy.uint8)
thresh = cv2.dilate(thresh, kernel, iterations=1)
dilate_contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

Das verbleibende Rauschen wird mit cv2.contourArea durch den Konturbereich gefiltert . Entfernen Sie alles, was weniger als 500 Pixel² groß ist.
digit_contours = [cnt for cnt in digit_contours if cv2.contourArea(cnt) > 500]

Das ist großartig. Lassen Sie uns all das in unserer Robot-Klasse implementieren.
# ...
class Robot:
# ...
def get_dilate_contours(self, image, color_inx, distance):
thresh = self.get_color_thresh(image, color_inx)
if thresh is False:
return []
kernel = numpy.ones((distance, distance), numpy.uint8)
thresh = cv2.dilate(thresh, kernel, iterations=1)
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
return contours
def get_color_thresh(self, image, color_inx):
if color_inx == self.COLOR_ALL:
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
_, thresh = cv2.threshold(image, 0, 255, cv2.THRESH_OTSU)
else:
image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
thresh = cv2.inRange(image, self.COLOR_HSV_RANGE[color_inx][0], self.COLOR_HSV_RANGE[color_inx][1])
return thresh
def filter_contours_of_rectangles(self, contours):
squares = []
for cnt in contours:
rect = cv2.minAreaRect(cnt)
square = cv2.boxPoints(rect)
square = numpy.int0(square)
(_, _, w, h) = cv2.boundingRect(square)
a = max(w, h)
b = min(w, h)
if numpy.unique(square).shape[0] <= 4 and a <= b * 3:
squares.append(numpy.array([[square[0]], [square[1]], [square[2]], [square[3]]]))
return squares
def get_contours_of_squares(self, image, color_inx, square_inx):
thresh = self.get_color_thresh(image, color_inx)
if thresh is False:
return False
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
contours_of_squares = self.filter_contours_of_rectangles(contours)
if len(contours_of_squares) < 1:
return False
image_zero = numpy.zeros_like(image)
image_zero = cv2.cvtColor(image_zero, cv2.COLOR_BGR2RGB)
cv2.drawContours(image_zero, contours_of_squares, -1, (255, 255, 255), -1)
dilate_contours = self.get_dilate_contours(image_zero, self.COLOR_ALL, 5)
dilate_contours = [cnt for cnt in dilate_contours if cv2.contourArea(cnt) > 500]
if len(dilate_contours) < 1:
return False
else:
return dilate_contours
Erkennung von Zahlen
Fügen wir die Fähigkeit hinzu, Zahlen zu erkennen. Warum brauchen wir das?
# …
class Robot:
# ...
SQUARE_BIG_SYMBOL = 0x01
SQUARE_SIZES = {
SQUARE_BIG_SYMBOL: 9,
}
IMAGE_DATA_PATH = "data/"
def __init__(self):
# ...
self.dilate_contours_bi_data = {}
for image_file in os.listdir(self.IMAGE_DATA_PATH):
image = cv2.imread(self.IMAGE_DATA_PATH + image_file)
contour_inx = os.path.splitext(image_file)[0]
color_inx = self.COLOR_RED
dilate_contours = self.get_dilate_contours_by_square_inx(image, color_inx, self.SQUARE_BIG_SYMBOL)
self.dilate_contours_bi_data[contour_inx] = dilate_contours[0]
def get_dilate_contours_by_square_inx(self, image, color_inx, square_inx):
distance = math.ceil(self.SQUARE_SIZES[square_inx] / 2)
return self.get_dilate_contours(image, color_inx, distance)
OpenCV verwendet die Funktion cv2.matchShapes , um Konturen basierend auf Hu-Momenten zu vergleichen . Es verbirgt die Implementierungsdetails vor uns, indem es zwei Pfade als Eingabe verwendet und das Vergleichsergebnis als Zahl zurückgibt. Je kleiner es ist, desto ähnlicher sind die Konturen.
cv2.matchShapes(dilate_contour, self.dilate_contours_bi_data['digit_' + str(digit)], cv2.CONTOURS_MATCH_I1, 0)
Vergleichen Sie die aktuelle Kontur digit_contour mit allen Standards und ermitteln Sie den Mindestwert von cv2.matchShapes. Wenn der Mindestwert weniger als 0,15 beträgt, wird die Ziffer als erkannt betrachtet. Die Schwelle des Minimalwertes wurde empirisch ermittelt. Kombinieren wir auch eng beieinander liegende Zeichen zu einer Zahl.
# …
class Robot:
# …
def scan_digits(self, image, color_inx, square_inx):
result = []
contours_of_squares = self.get_contours_of_squares(image, color_inx, square_inx)
before_digit_x, before_digit_y = (-100, -100)
if contours_of_squares is False:
return result
for contour_of_square in reversed(contours_of_squares):
crop_image = self.crop_image_by_contour(image, contour_of_square)
dilate_contours = self.get_dilate_contours_by_square_inx(crop_image, self.COLOR_ALL, square_inx)
if (len(dilate_contours) < 1):
continue
dilate_contour = dilate_contours[0]
match_shapes = {}
for digit in range(0, 10):
match_shapes[digit] = cv2.matchShapes(dilate_contour, self.dilate_contours_bi_data['digit_' + str(digit)], cv2.CONTOURS_MATCH_I1, 0)
min_match_shape = min(match_shapes.items(), key=lambda x: x[1])
if len(min_match_shape) > 0 and (min_match_shape[1] < self.MAX_MATCH_SHAPES_DIGITS):
digit = min_match_shape[0]
rect = cv2.minAreaRect(contour_of_square)
box = cv2.boxPoints(rect)
box = numpy.int0(box)
(digit_x, digit_y, digit_w, digit_h) = cv2.boundingRect(box)
if abs(digit_y - before_digit_y) < digit_y * 0.3 and abs(
digit_x - before_digit_x) < digit_w + digit_w * 0.5:
result[len(result) - 1][0] = int(str(result[len(result) - 1][0]) + str(digit))
else:
result.append([digit, self.get_contour_centroid(contour_of_square)])
before_digit_x, before_digit_y = digit_x + (digit_w / 2), digit_y
return result
Bei der Ausgabe gibt die Methode self.scan_digits ein Array zurück, das die erkannte Ziffer und die Koordinate des Klicks darauf enthält. Der Klickpunkt ist der Schwerpunkt des Umrisses.
# …
class Robot:
# …
def get_contour_centroid(self, contour):
moments = cv2.moments(contour)
return int(moments["m10"] / moments["m00"]), int(moments["m01"] / moments["m00"])
Wir freuen uns über das empfangene Ziffernerkennungstool, aber nicht lange. Die Hu-Momente sind neben der Skalierung auch für Rotation und Spiegelung unveränderlich. Daher wird der Bot die Zahlen 6 und 9/2 und 5 verwechseln. Fügen wir eine zusätzliche Überprüfung dieser Symbole an den Eckpunkten hinzu. 6 und 9 werden durch den oberen rechten Punkt unterschieden. Wenn es unterhalb der horizontalen Mitte liegt, ist es 6 und 9 für das Gegenteil. Überprüfen Sie für Paar 2 und 5, ob sich der obere rechte Punkt am rechten Rand des Symbols befindet
if digit == 6 or digit == 9:
extreme_bottom_point = digit_contour[digit_contour[:, :, 1].argmax()].flatten()
x_points = digit_contour[:, :, 0].flatten()
extreme_right_points_args = numpy.argwhere(x_points == numpy.amax(x_points))
extreme_right_points = digit_contour[extreme_right_points_args]
extreme_top_right_point = extreme_right_points[extreme_right_points[:, :, :, 1].argmin()].flatten()
if extreme_top_right_point[1] > round(extreme_bottom_point[1] / 2):
digit = 6
else:
digit = 9
if digit == 2 or digit == 5:
extreme_right_point = digit_contour[digit_contour[:, :, 0].argmax()].flatten()
y_points = digit_contour[:, :, 1].flatten()
extreme_top_points_args = numpy.argwhere(y_points == numpy.amin(y_points))
extreme_top_points = digit_contour[extreme_top_points_args]
extreme_top_right_point = extreme_top_points[extreme_top_points[:, :, :, 0].argmax()].flatten()
if abs(extreme_right_point[0] - extreme_top_right_point[0]) > 0.05 * extreme_right_point[0]:
digit = 2
else:
digit = 5


Das Spielfeld analysieren
Lassen Sie uns die Trainingsstufe überspringen. Sie wird per Skript erstellt, indem Sie auf den weißen Cursor klicken und mit dem Spielen beginnen.
Stellen wir uns das Spielfeld als Netzwerk vor. Jeder Farbbereich ist ein Knoten, der mit benachbarten Nachbarn verbunden ist. Erstellen wir eine Klasse self.ColorArea , die den Farbbereich / Knoten beschreibt.
class ColorArea:
def __init__(self, color_inx, click_point, contour):
self.color_inx = color_inx #
self.click_point = click_point #
self.contour = contour #
self.neighbors = [] #
Definieren wir eine Liste der Knoten self.color_areas und eine Liste, wie oft die Farbe auf dem Spielfeld self.color_areas_color_count erscheint . Beschneiden Sie das Spielfeld aus dem Leinwand-Screenshot.
image[pt1[1]:pt2[1], pt1[0]:pt2[0]]
Wobei pt1, pt2 die Extrempunkte des Rahmens sind. Wir durchlaufen alle Farben des Spiels und wenden die Methode self.get_dilate_contours auf jede an . Das Finden der Kontur des Knotens ähnelt dem Suchen nach der allgemeinen Kontur von Symbolen, mit dem Unterschied, dass auf dem Spielfeld kein Rauschen auftritt. Die Form der Knoten kann konkav sein oder ein Loch haben, sodass der Schwerpunkt aus der Form herausfällt und nicht als Koordinate für einen Klick geeignet ist. Suchen Sie dazu den äußersten oberen Punkt und legen Sie ihn um 20 Pixel ab. Die Methode ist nicht universell, aber in unserem Fall funktioniert sie.
self.color_areas = []
self.color_areas_color_count = [0] * self.SELECT_COLOR_COUNT
image = self.crop_image_by_rectangle(self.screenshot, numpy.array(self.GAME_MAIN_AREA))
for color_inx in range(1, self.SELECT_COLOR_COUNT + 1):
dilate_contours = self.get_dilate_contours(image, color_inx, 10)
for dilate_contour in dilate_contours:
click_point = tuple(
dilate_contour[dilate_contour[:, :, 1].argmin()].flatten() + [0, int(self.CLICK_AREA)])
self.color_areas_color_count[color_inx - 1] += 1
color_area = self.ColorArea(color_inx, click_point, dilate_contour)
self.color_areas.append(color_area)

Bereiche verknüpfen
Wir werden Bereiche als Nachbarn betrachten, wenn der Abstand zwischen ihren Konturen innerhalb von 15 Pixeln liegt. Wir durchlaufen jeden Knoten mit jedem Knoten und überspringen den Vergleich, wenn ihre Farben übereinstimmen.
blank_image = numpy.zeros_like(image)
blank_image = cv2.cvtColor(blank_image, cv2.COLOR_BGR2GRAY)
for color_area_inx_1 in range(0, len(self.color_areas)):
for color_area_inx_2 in range(color_area_inx_1 + 1, len(self.color_areas)):
color_area_1 = self.color_areas[color_area_inx_1]
color_area_2 = self.color_areas[color_area_inx_2]
if color_area_1.color_inx == color_area_2.color_inx:
continue
common_image = cv2.drawContours(blank_image.copy(), [color_area_1.contour, color_area_2.contour], -1, (255, 255, 255), cv2.FILLED)
kernel = numpy.ones((15, 15), numpy.uint8)
common_image = cv2.dilate(common_image, kernel, iterations=1)
common_contour, _ = cv2.findContours(common_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if len(common_contour) == 1:
self.color_areas[color_area_inx_1].neighbors.append(color_area_inx_2)
self.color_areas[color_area_inx_2].neighbors.append(color_area_inx_1)

Wir suchen den optimalen Umzug
Wir haben alle Informationen über das Spielfeld. Beginnen wir mit der Auswahl eines Zuges. Dazu benötigen wir einen Knotenindex und eine Farbe. Die Anzahl der Bewegungsoptionen kann durch die Formel bestimmt werden:
Bewegungsoptionen = Anzahl der Knoten * Anzahl der Farben - 1
Für das vorherige Spielfeld haben wir 7 * (5-1) = 28 Optionen. Es gibt nicht viele von ihnen, daher können wir alle Bewegungen durchlaufen und die optimale auswählen. Definieren wir die Optionen als Matrix
select_color_weights , in der die Zeile der Knotenindex, die Farbindexspalte und die Verschiebungsgewichtszelle ist. Wir müssen die Anzahl der Knoten auf eins reduzieren, damit Bereiche, die eine eindeutige Farbe auf der Platine haben und die verschwinden, nachdem wir zu ihnen gewechselt sind, Vorrang haben. Geben wir dem Gewicht für alle Knotenzeilen mit einer eindeutigen Farbe +10. Wie oft die Farbe auf dem Spielfeld erscheint, haben wir zuvor gesammeltself.color_areas_color_count
if self.color_areas_color_count[color_area.color_inx - 1] == 1:
select_color_weight = [x + 10 for x in select_color_weight]
Schauen wir uns als nächstes die Farben benachbarter Bereiche an. Wenn der Knoten Nachbarn von color_inx hat und deren Anzahl der Gesamtzahl dieser Farbe auf dem Spielfeld entspricht, weisen Sie dem Gewicht der Zelle +10 zu. Dadurch wird auch color_inx aus dem Feld entfernt.
for color_inx in range(0, len(select_color_weight)):
color_count = select_color_weight[color_inx]
if color_count != 0 and self.color_areas_color_count[color_inx] == color_count:
select_color_weight[color_inx] += 10
Geben wir dem Gewicht der Zelle für jeden Nachbarn derselben Farbe +1. Das heißt, wenn wir 3 rote Nachbarn haben, erhält die rote Zelle +3 auf ihr Gewicht.
for select_color_weight_inx in color_area.neighbors:
neighbor_color_area = self.color_areas[select_color_weight_inx]
select_color_weight[neighbor_color_area.color_inx - 1] += 1
Nachdem wir alle Gewichte gesammelt haben, finden wir die Bewegung mit dem maximalen Gewicht. Definieren wir, zu welchem Knoten und zu welcher Farbe er gehört.
max_index = select_color_weights.argmax()
self.color_area_inx_next = max_index // self.SELECT_COLOR_COUNT
select_color_next = (max_index % self.SELECT_COLOR_COUNT) + 1
self.set_select_color_next(select_color_next)
Vervollständigen Sie den Code, um den optimalen Zug zu bestimmen.
# …
class Robot:
# …
def scan_color_areas(self):
self.color_areas = []
self.color_areas_color_count = [0] * self.SELECT_COLOR_COUNT
image = self.crop_image_by_rectangle(self.screenshot, numpy.array(self.GAME_MAIN_AREA))
for color_inx in range(1, self.SELECT_COLOR_COUNT + 1):
dilate_contours = self.get_dilate_contours(image, color_inx, 10)
for dilate_contour in dilate_contours:
click_point = tuple(
dilate_contour[dilate_contour[:, :, 1].argmin()].flatten() + [0, int(self.CLICK_AREA)])
self.color_areas_color_count[color_inx - 1] += 1
color_area = self.ColorArea(color_inx, click_point, dilate_contour, [0] * self.SELECT_COLOR_COUNT)
self.color_areas.append(color_area)
blank_image = numpy.zeros_like(image)
blank_image = cv2.cvtColor(blank_image, cv2.COLOR_BGR2GRAY)
for color_area_inx_1 in range(0, len(self.color_areas)):
for color_area_inx_2 in range(color_area_inx_1 + 1, len(self.color_areas)):
color_area_1 = self.color_areas[color_area_inx_1]
color_area_2 = self.color_areas[color_area_inx_2]
if color_area_1.color_inx == color_area_2.color_inx:
continue
common_image = cv2.drawContours(blank_image.copy(), [color_area_1.contour, color_area_2.contour],
-1, (255, 255, 255), cv2.FILLED)
kernel = numpy.ones((15, 15), numpy.uint8)
common_image = cv2.dilate(common_image, kernel, iterations=1)
common_contour, _ = cv2.findContours(common_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if len(common_contour) == 1:
self.color_areas[color_area_inx_1].neighbors.append(color_area_inx_2)
self.color_areas[color_area_inx_2].neighbors.append(color_area_inx_1)
def analysis_color_areas(self):
select_color_weights = []
for color_area_inx in range(0, len(self.color_areas)):
color_area = self.color_areas[color_area_inx]
select_color_weight = numpy.array([0] * self.SELECT_COLOR_COUNT)
for select_color_weight_inx in color_area.neighbors:
neighbor_color_area = self.color_areas[select_color_weight_inx]
select_color_weight[neighbor_color_area.color_inx - 1] += 1
for color_inx in range(0, len(select_color_weight)):
color_count = select_color_weight[color_inx]
if color_count != 0 and self.color_areas_color_count[color_inx] == color_count:
select_color_weight[color_inx] += 10
if self.color_areas_color_count[color_area.color_inx - 1] == 1:
select_color_weight = [x + 10 for x in select_color_weight]
color_area.set_select_color_weights(select_color_weight)
select_color_weights.append(select_color_weight)
select_color_weights = numpy.array(select_color_weights)
max_index = select_color_weights.argmax()
self.color_area_inx_next = max_index // self.SELECT_COLOR_COUNT
select_color_next = (max_index % self.SELECT_COLOR_COUNT) + 1
self.set_select_color_next(select_color_next)
Fügen wir die Möglichkeit hinzu, zwischen Ebenen zu wechseln und das Ergebnis zu genießen. Der Bot arbeitet stabil und beendet das Spiel in einer Sitzung.
Ausgabe
Der erstellte Bot hat keinen praktischen Nutzen. Der Autor des Artikels hofft jedoch aufrichtig, dass eine detaillierte Beschreibung der Grundprinzipien von OpenCV Anfängern helfen wird, diese Bibliothek in der Anfangsphase zu verstehen.