
Mencari tujuan
Kami pergi ke situs tematik miniclip.com dan mencari target. Pilihannya jatuh pada teka-teki warna Coloruid 2 dari bagian Teka-teki, di mana kita perlu mengisi lapangan permainan bundar dengan satu warna dalam sejumlah gerakan tertentu.
Area arbitrer diisi dengan warna yang dipilih di bagian bawah layar, sementara area yang berdekatan dengan warna yang sama bergabung menjadi satu.

Latihan
Kami akan menggunakan Python. Bot dibuat hanya untuk tujuan pendidikan. Artikel ini ditujukan untuk pemula dalam computer vision, saya sendiri.
Gim ini terletak di sini
GitHub dari bot di sini
Agar bot berfungsi, kita memerlukan modul berikut:
- opencv-python
- Bantal
- selenium
Bot ditulis dan diuji untuk Python 3.8 di Ubuntu 20.04.1. Kami menginstal modul yang diperlukan di lingkungan virtual Anda atau melalui pip install. Selain itu, agar Selenium berfungsi, kami membutuhkan geckodriver untuk FireFox, Anda dapat mengunduhnya di sini github.com/mozilla/geckodriver/releases
Kontrol browser
Kami berurusan dengan game online, jadi pertama-tama kami akan mengatur interaksi dengan browser. Untuk tujuan ini, kami akan menggunakan Selenium, yang akan memberi kami API untuk mengelola FireFox. Memeriksa kode halaman game. Teka-teki tersebut adalah kanvas, yang terletak di iframe.
Kami menunggu frame dengan id = iframe-game dimuat dan mengalihkan konteks driver ke dalamnya. Lalu kita tunggu kanvas. Ini adalah satu-satunya di frame dan tersedia melalui XPath / html / body / canvas.
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")))
Selanjutnya, kanvas kita akan tersedia melalui properti kanvas self .__. Semua logika bekerja dengan browser turun ke mengambil screenshot dari kanvas dan mengkliknya pada koordinat tertentu.
Lengkapi kode Browser.py:
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()
Status permainan
Mari kita turun ke permainan itu sendiri. Semua logika bot akan diimplementasikan di kelas Robot. Mari bagi gameplay menjadi 7 status dan tetapkan metode untuk memprosesnya. Mari pilih tingkat pelatihan secara terpisah. Ini berisi kursor putih besar yang menunjukkan tempat untuk mengklik, yang akan mencegah game dikenali dengan benar.
- Layar selamat datang
- Layar pemilihan level
- Pemilihan warna di tingkat tutorial
- Memilih area di tingkat pengajaran
- Pemilihan warna
- Pemilihan wilayah
- Hasil dari gerakan itu
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,
}
Untuk stabilitas bot yang lebih baik, kami akan memeriksa apakah status game telah berhasil diubah. Jika self.state_next_success_condition tidak mengembalikan True selama self.state_timeout, kami terus memproses keadaan saat ini, jika tidak, kami beralih ke self.state_next. Kami juga akan menerjemahkan tangkapan layar yang diterima dari Selenium ke dalam format yang dimengerti oleh OpenCV.
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
Mari menerapkan pemeriksaan dalam metode penanganan status. Kami menunggu tombol Putar di layar mulai dan klik di atasnya. Jika dalam 10 detik kita belum menerima layar pemilihan level, kita kembali ke tahap sebelumnya sendiri.STATE_START, jika tidak kita lanjutkan ke proses sendiri.STATE_SELECT_LEVEL.
# …
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):
#
# …
Visi bot
Gambar ambang batas
Mari tentukan warna yang digunakan dalam game. Ini adalah 5 warna yang dapat dimainkan dan warna kursor untuk level tutorial. Kami akan menggunakan COLOR_ALL jika kami perlu menemukan semua objek, apa pun warnanya. Untuk memulainya, kami akan mempertimbangkan kasus ini.
COLOR_BLUE = 0x01
COLOR_ORANGE = 0x02
COLOR_RED = 0x03
COLOR_GREEN = 0x04
COLOR_YELLOW = 0x05
COLOR_WHITE = 0x06
COLOR_ALL = 0x07
Untuk menemukan sebuah objek, Anda perlu menyederhanakan gambar terlebih dahulu. Sebagai contoh, ambil simbol "0" dan terapkan thresholding padanya, yaitu, kita akan memisahkan objek dari background. Pada tahap ini, kami tidak peduli dengan warna apa simbol itu. Pertama, mari kita ubah gambar menjadi hitam dan putih, menjadikannya 1 saluran. Fungsi cv2.cvtColor dengan argumen kedua cv2.COLOR_BGR2GRAY , yang bertanggung jawab untuk konversi grayscale , akan membantu kita dalam hal ini . Selanjutnya, kami melakukan thresholding menggunakan cv2.threshold . Semua piksel dari gambar di bawah ambang tertentu disetel ke 0, semuanya di atas - ke 255. Argumen kedua dari fungsi cv2.threshold bertanggung jawab untuk nilai ambang batas . Dalam kasus kami, nomor apa pun bisa ada di sana, karena kami menggunakan cv2.THRESH_OTSU dan fungsi itu sendiri akan menentukan ambang optimal menggunakan metode Otsu berdasarkan histogram gambar.
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
_, thresh = cv2.threshold(image, 0, 255, cv2.THRESH_OTSU)

Segmentasi warna
Lebih jauh lebih menarik. Mari kita memperumit tugas dan menemukan semua simbol merah di layar pemilihan level.

Secara default, semua gambar OpenCV disimpan dalam format BGR. HSV (Hue, Saturation, Value - hue, saturation, value) lebih cocok untuk segmentasi warna. Keunggulannya dibandingkan RGB adalah HSV memisahkan warna dari saturasi dan kecerahan. Rona dikodekan oleh satu saluran Hue. Mari kita ambil persegi panjang hijau muda sebagai contoh dan secara bertahap kurangi kecerahannya.

Tidak seperti RGB, transformasi ini terlihat intuitif di HSV - kami hanya menurunkan nilai saluran Value atau Brightness. Perlu dicatat di sini bahwa dalam model referensi, skala warna Hue bervariasi dalam kisaran 0-360 °. Warna hijau muda kami sesuai dengan 90 °. Untuk memasukkan nilai ini ke dalam saluran 8-bit, itu harus dibagi dengan 2.
Segmentasi warna bekerja dengan rentang, bukan satu warna. Anda dapat menentukan rentangnya secara empiris, tetapi lebih mudah untuk menulis skrip kecil.
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()
Mari kita luncurkan dengan tangkapan layar kita.

Klik pada warna merah dan lihat topeng yang dihasilkan. Jika hasilnya tidak sesuai dengan kami, kami memilih warna merah, meningkatkan jangkauan dan area topeng. Skrip ini didasarkan pada fungsi cv2.inRange , yang bertindak sebagai filter warna dan mengembalikan gambar ambang batas untuk rentang warna tertentu.
Mari kita bahas kisaran berikut:
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)),
}
Menemukan kontur
Mari kembali ke layar pemilihan level kita. Mari terapkan filter warna rentang merah yang baru saja kita tentukan dan teruskan ambang yang ditemukan ke cv2.findContours . Fungsi ini akan menemukan garis besar elemen merah. Kami menetapkan cv2.RETR_EXTERNAL sebagai argumen kedua - kami hanya membutuhkan kontur luar, dan sebagai cv2.CHAIN_APPROX_SIMPLE ketiga - kami tertarik pada kontur lurus, menghemat memori dan hanya menyimpan simpulnya.
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

Menghilangkan kebisingan
Kontur yang dihasilkan mengandung banyak kebisingan latar belakang. Untuk menghapusnya, kami akan menggunakan properti bilangan kami. Mereka terdiri dari persegi panjang yang sejajar dengan sumbu koordinat. Kami mengulangi semua jalur dan memasukkan masing-masing ke dalam persegi panjang minimum menggunakan cv2.minAreaRect . Persegi panjang ditentukan oleh 4 titik. Jika persegi panjang kita sejajar dengan sumbu, maka salah satu koordinat untuk setiap pasangan titik harus sama. Ini berarti kita akan memiliki maksimal 4 nilai unik jika kita merepresentasikan koordinat persegi panjang sebagai array satu dimensi. Selain itu, kami memfilter persegi panjang yang terlalu panjang, yang rasio aspeknya lebih besar dari 3 berbanding 1. Untuk melakukan ini, kami mencari lebar dan panjangnya menggunakan 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]]]))

Menggabungkan kontur
Lebih baik. Sekarang kita perlu menggabungkan persegi panjang yang ditemukan menjadi garis besar simbol. Kami membutuhkan citra perantara. Mari membuatnya dengan numpy.zeros_like . Fungsi ini membuat salinan matriks gambar dengan tetap mempertahankan bentuk dan ukurannya, lalu mengisinya dengan nol. Dengan kata lain, kami mendapat salinan gambar asli kami yang diisi dengan latar belakang hitam. Kami mengubahnya menjadi 1-saluran dan menerapkan kontur yang ditemukan menggunakan cv2.drawContours , mengisinya dengan warna putih. Kami mendapatkan ambang biner tempat kami dapat menerapkan cv2.dilate . Fungsi ini memperluas area putih dengan menghubungkan persegi panjang terpisah, yang jaraknya dalam 5 piksel. Sekali lagi saya memanggil cv2.findContours dan mendapatkan kontur nomor merah.
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)

Kebisingan yang tersisa disaring oleh area kontur menggunakan cv2.contourArea . Hapus semua yang berukuran kurang dari 500 piksel².
digit_contours = [cnt for cnt in digit_contours if cv2.contourArea(cnt) > 500]

Nah itu bagus. Mari terapkan semua hal di atas di kelas Robot kita.
# ...
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
Pengakuan angka
Mari tambahkan kemampuan untuk mengenali angka. kenapa kita butuh ini?
# …
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 menggunakan fungsi cv2.matchShapes untuk membandingkan kontur berdasarkan momen Hu . Ini menyembunyikan detail implementasi dari kami dengan mengambil dua jalur sebagai masukan dan mengembalikan hasil perbandingan sebagai angka. Semakin kecil ukurannya, semakin mirip konturnya.
cv2.matchShapes(dilate_contour, self.dilate_contours_bi_data['digit_' + str(digit)], cv2.CONTOURS_MATCH_I1, 0)
Bandingkan digit_contour kontur saat ini dengan semua standar dan temukan nilai minimum cv2.matchShapes. Jika nilai minimum kurang dari 0,15, digit tersebut dianggap diakui. Ambang batas nilai minimum ditemukan secara empiris. Mari kita juga menggabungkan simbol yang berjarak dekat menjadi satu angka.
# …
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
Pada keluaran, metode self.scan_digits akan mengembalikan larik yang berisi digit yang dikenali dan koordinat klik di atasnya. Titik klik akan menjadi titik pusat garis besarnya.
# …
class Robot:
# …
def get_contour_centroid(self, contour):
moments = cv2.moments(contour)
return int(moments["m10"] / moments["m00"]), int(moments["m01"] / moments["m00"])
Kami bersukacita atas alat pengenalan digit yang diterima, tapi tidak lama. Momen Hu, selain skala, juga invarian terhadap rotasi dan spekularitas. Oleh karena itu, bot akan mengacaukan angka 6 dan 9/2 dan 5. Mari tambahkan pemeriksaan simpul tambahan untuk simbol-simbol ini. 6 dan 9 akan dibedakan dengan titik kanan atas. Jika di bawah pusat horizontal, maka itu adalah 6 dan 9 untuk kebalikannya. Untuk pasangan 2 dan 5, periksa apakah titik kanan atas berada di tepi kanan simbol.
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


Menganalisis lapangan bermain
Mari lewati level pelatihan, ini ditulis dengan mengklik kursor putih dan mulai bermain.
Bayangkan lapangan bermain sebagai jaringan. Setiap area warna akan menjadi simpul yang terhubung ke tetangga yang berdekatan. Mari buat kelas self.ColorArea yang akan mendeskripsikan color area / node.
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 = [] #
Mari kita tentukan daftar node self.color_areas dan daftar seberapa sering warna muncul di lapangan bermain self.color_areas_color_count . Pangkas lapangan bermain dari tangkapan layar kanvas.
image[pt1[1]:pt2[1], pt1[0]:pt2[0]]
Di mana pt1, pt2 adalah titik ekstrem dari bingkai. Kami mengulangi semua warna permainan dan menerapkan metode self.get_dilate_contours untuk masing-masing warna . Menemukan kontur node serupa dengan cara kami mencari kontur umum simbol, dengan perbedaan bahwa tidak ada noise di lapangan bermain. Bentuk node bisa cekung atau berlubang, sehingga centroid akan keluar dari bentuknya dan tidak cocok sebagai koordinat klik. Untuk melakukan ini, temukan titik paling atas dan jatuhkan 20 piksel. Metodenya tidak universal, tetapi dalam kasus kami ini berhasil.
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)

Menghubungkan area
Kami akan menganggap area sebagai tetangga jika jarak antara konturnya dalam 15 piksel. Kami mengulangi setiap node dengan masing-masing, melewatkan perbandingan jika warnanya cocok.
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)

Kami mencari langkah yang optimal
Kami memiliki semua informasi tentang lapangan bermain. Mari mulai memilih gerakan. Untuk ini kita membutuhkan indeks node dan warna. Jumlah opsi pindah dapat ditentukan dengan rumus:
Opsi pindah = Jumlah node * Jumlah warna - 1
Untuk lapangan bermain sebelumnya, kami memiliki 7 * (5-1) = 28 opsi. Jumlahnya tidak banyak, jadi kami dapat mengulang semua gerakan dan memilih yang optimal. Mari tentukan opsi sebagai matriks
select_color_weights , di mana barisnya akan menjadi indeks node, kolom indeks warna, dan sel bobot gerak. Kita perlu mengurangi jumlah node menjadi satu, jadi kita akan memprioritaskan area yang memiliki warna unik di papan dan akan hilang setelah Anda pindah ke sana. Mari beri bobot +10 untuk semua baris node dengan warna yang unik. Seberapa sering warna muncul di lapangan permainan, yang telah kita kumpulkan sebelumnyaself.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]
Selanjutnya, mari kita lihat warna area yang berdekatan. Jika node memiliki tetangga color_inx, dan jumlahnya sama dengan jumlah total warna ini di lapangan bermain, tetapkan +10 ke bobot sel. Ini juga akan menghapus warna color_inx dari bidang.
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
Mari beri +1 pada bobot sel untuk setiap tetangga dengan warna yang sama. Artinya, jika kita memiliki 3 tetangga merah, sel darah merah akan menerima +3 untuk bobotnya.
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
Setelah mengumpulkan semua bobot, kami menemukan gerakan dengan bobot maksimum. Mari kita tentukan node mana dan warnanya.
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)
Lengkapi kode untuk menentukan langkah optimal.
# …
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)
Mari tambahkan kemampuan untuk berpindah antar level dan nikmati hasilnya. Bot bekerja dengan stabil dan menyelesaikan permainan dalam satu sesi.
Keluaran
Bot yang dibuat tidak memiliki kegunaan praktis. Tetapi penulis artikel ini dengan tulus berharap penjelasan mendetail tentang prinsip-prinsip dasar OpenCV akan membantu pemula memahami pustaka ini pada tahap awal.