
تبحث عن هدف
نذهب إلى موقع miniclip.com المواضيعي ونبحث عن هدف. وقع الاختيار على اللغز الملون Coloruid 2 من قسم Puzzles ، حيث نحتاج إلى ملء ملعب دائري بلون واحد في عدد معين من الحركات.
يتم تعبئة منطقة عشوائية باللون المحدد أسفل الشاشة ، بينما يتم دمج المناطق المجاورة من نفس اللون في واحدة.

تدريب
سوف نستخدم بايثون. تم إنشاء الروبوت للأغراض التعليمية فقط. المقال مصمم للمبتدئين في رؤية الكمبيوتر ، وأنا شخصياً.
توجد اللعبة هنا
GitHub من الروبوت هنا
لكي يعمل الروبوت ، نحتاج إلى الوحدات التالية:
- أوبينكف بيثون
- وسادة
- السيلينيوم
تمت كتابة الروبوت واختباره لـ Python 3.8 على Ubuntu 20.04.1. نقوم بتثبيت الوحدات اللازمة في بيئتك الافتراضية أو عبر تثبيت النقطة. بالإضافة إلى ذلك ، لكي يعمل السيلينيوم ، نحتاج إلى geckodriver لـ FireFox ، يمكنك تنزيله هنا github.com/mozilla/geckodriver/releases
تحكم المتصفح
نحن نتعامل مع لعبة على الإنترنت ، لذلك سننظم أولاً التفاعل مع المتصفح. لهذا الغرض ، سوف نستخدم السيلينيوم ، والذي سيوفر لنا واجهة برمجة تطبيقات لإدارة FireFox. فحص كود صفحة اللعبة. اللغز عبارة عن لوحة ، والتي بدورها تقع في إطار iframe.
ننتظر حتى يتم تحميل الإطار الذي يحتوي على id = iframe-game وتبديل سياق برنامج التشغيل إليه. ثم ننتظر قماش. إنه الوحيد في الإطار ومتاح عبر 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")))
بعد ذلك ، ستكون اللوحة القماشية متاحة من خلال خاصية Canvas .__ الذاتية. ينحصر منطق العمل مع المتصفح في أخذ لقطة شاشة للقماش والنقر عليها في تنسيق معين.
كود 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()
حالات اللعبة
دعنا ننتقل إلى اللعبة نفسها. سيتم تنفيذ كل منطق البوت في فئة الروبوت. دعنا نقسم طريقة اللعب إلى 7 حالات ونخصص لها طرقًا لمعالجتها. دعنا نختار مستوى التدريب بشكل منفصل. يحتوي على مؤشر أبيض كبير يشير إلى مكان النقر ، مما يمنع التعرف على اللعبة بشكل صحيح.
- شاشة الترحيب
- شاشة اختيار المستوى
- اختيار اللون على مستوى البرنامج التعليمي
- اختيار منطقة على مستوى التدريس
- اختيار اللون
- اختيار المنطقة
- نتيجة الانتقال
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,
}
لمزيد من الاستقرار في الروبوت ، سوف نتحقق مما إذا كان التغيير في حالة اللعبة قد حدث بنجاح. إذا لم تعيد self.state_next_success_condition القيمة True أثناء self.state_timeout ، فإننا نواصل معالجة الحالة الحالية ، وإلا فإننا ننتقل إلى self.state_next. سنقوم أيضًا بترجمة لقطة الشاشة المستلمة من السيلينيوم إلى تنسيق مفهوم لـ 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
دعنا ننفذ الفحص في طرق معالجة الحالة. نحن في انتظار زر التشغيل في شاشة البدء والنقر عليه. إذا لم نتلق شاشة تحديد المستوى في غضون 10 ثوانٍ ، فسنرجع إلى المرحلة السابقة. STATE_START ، وإلا فإننا ننتقل إلى معالجة self.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):
#
# …
رؤية بوت
عتبة الصورة
دعنا نحدد الألوان المستخدمة في اللعبة. هذه 5 ألوان قابلة للتشغيل ولون مؤشر لمستوى البرنامج التعليمي. سنستخدم COLOR_ALL إذا احتجنا إلى العثور على جميع الكائنات ، بغض النظر عن اللون. بادئ ذي بدء ، سننظر في هذه الحالة.
COLOR_BLUE = 0x01
COLOR_ORANGE = 0x02
COLOR_RED = 0x03
COLOR_GREEN = 0x04
COLOR_YELLOW = 0x05
COLOR_WHITE = 0x06
COLOR_ALL = 0x07
للعثور على كائن ، تحتاج أولاً إلى تبسيط الصورة. على سبيل المثال ، لنأخذ الرمز "0" ونطبق عتبة عليه ، أي أننا سنقوم بفصل الكائن عن الخلفية. في هذه المرحلة ، لا نهتم ما هو لون الرمز. أولاً ، لنحول الصورة إلى أبيض وأسود ، ونجعلها ذات قناة واحدة. ستساعدنا الدالة cv2.cvtColor مع الوسيطة الثانية cv2.COLOR_BGR2GRAY في هذا الأمر ، وهو المسؤول عن التحويل إلى التدرج الرمادي. بعد ذلك ، نقوم بتنفيذ العتبة باستخدام cv2.threshold . يتم تعيين جميع وحدات البكسل في الصورة التي تقل عن حد معين على 0 ، وكل شيء أعلاه - إلى 255. الوسيطة الثانية للدالة cv2.threshold مسؤولة عن قيمة العتبة . في حالتنا ، يمكن أن يكون هناك أي رقم ، لأننا نستخدم cv2.THRESH_OTSU وستحدد الوظيفة نفسها الحد الأمثل بواسطة طريقة أوتسو بناءً على الرسم البياني للصورة.
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
_, thresh = cv2.threshold(image, 0, 255, cv2.THRESH_OTSU)

تجزئة اللون
أكثر إثارة للاهتمام. دعنا نعقد المهمة ونجد جميع الرموز الحمراء على شاشة تحديد المستوى.

بشكل افتراضي ، يتم تخزين جميع صور OpenCV بتنسيق BGR. HSV (تدرج اللون ، والتشبع ، والقيمة - تدرج اللون ، والتشبع ، والقيمة) أكثر ملاءمة لتجزئة اللون. ميزته على RGB هي أن HSV يفصل اللون عن تشبعه وسطوعه. يتم ترميز الصبغة بواسطة قناة Hue واحدة. لنأخذ المستطيل الأخضر الفاتح كمثال ونخفض سطوعه تدريجيًا.

على عكس RGB ، يبدو هذا التحول بديهيًا في HSV - نحن فقط نخفض قيمة قناة القيمة أو السطوع. وتجدر الإشارة هنا إلى أنه في النموذج المرجعي ، يختلف مقياس الظل Hue في نطاق 0-360 درجة. يتوافق لوننا الأخضر الفاتح مع 90 درجة. من أجل ملاءمة هذه القيمة في قناة 8 بت ، يجب تقسيمها على 2.
يعمل تجزئة اللون مع نطاقات ، وليس لونًا واحدًا. يمكنك تحديد النطاق تجريبيًا ، ولكن من الأسهل كتابة نص صغير.
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()
دعنا نطلقها مع لقطة الشاشة الخاصة بنا.

انقر على اللون الأحمر وانظر إلى القناع الناتج. إذا كان الإخراج لا يناسبنا ، نختار ظلال اللون الأحمر ، مما يزيد من نطاق ومساحة القناع. يعتمد البرنامج النصي على وظيفة cv2.inRange ، والتي تعمل كمرشح لوني وتعيد صورة حد لنطاق ألوان معين.
دعنا نتناول النطاقات التالية:
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)),
}
البحث عن ملامح
دعنا نعود إلى شاشة اختيار المستوى. دعنا نطبق مرشح النطاق الأحمر الذي حددناه للتو ونمرر الحد الموجود إلى cv2.findContours . سوف تجد لنا الدالة الخطوط العريضة للعناصر الحمراء. نحدد cv2.RETR_EXTERNAL كوسيطة ثانية - نحتاج فقط إلى ملامح خارجية ، و cv2.CHAIN_APPROX_SIMPLE كالثالث - نحن مهتمون بالخطوط المستقيمة ، وحفظ الذاكرة ، ونخزن الرؤوس فقط.
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

إزالة الضوضاء
تحتوي المعالم الناتجة على الكثير من ضوضاء الخلفية. لإزالته ، سنستخدم خاصية أرقامنا. تتكون من مستطيلات موازية لمحاور الإحداثيات. نحن نكرر جميع المسارات ونلائم كل منها في الحد الأدنى للمستطيل باستخدام cv2.minAreaRect . يتم تحديد المستطيل بمقدار 4 نقاط. إذا كان المستطيل موازيًا للمحاور ، فيجب أن يتطابق أحد إحداثيات كل زوج من النقاط. هذا يعني أنه سيكون لدينا 4 قيم فريدة كحد أقصى إذا قمنا بتمثيل إحداثيات المستطيل كمصفوفة أحادية البعد. بالإضافة إلى ذلك ، دعنا نقوم بتصفية المستطيلات الطويلة جدًا ، حيث تكون نسبة العرض إلى الارتفاع أكبر من 3 إلى 1. للقيام بذلك ، ابحث عن عرضها وطولها باستخدام 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]]]))

الجمع بين ملامح
افضل الآن. نحتاج الآن إلى دمج المستطيلات الموجودة في مخطط عام للرموز. نحن بحاجة إلى صورة وسيطة. لنقم بإنشائه باستخدام numpy.zeros_like . تنشئ الوظيفة نسخة من صورة المصفوفة مع الحفاظ على شكلها وحجمها ، ثم تملأها بالأصفار. بمعنى آخر ، حصلنا على نسخة من صورتنا الأصلية مليئة بخلفية سوداء. نقوم بتحويلها إلى قناة واحدة ونطبق الخطوط العريضة باستخدام cv2.drawContours ، ونملأها باللون الأبيض. نحصل على حد ثنائي يمكننا تطبيق cv2.dilate عليه . تعمل الوظيفة على توسيع المنطقة البيضاء عن طريق توصيل مستطيلات منفصلة ، المسافة بينهما في حدود 5 بكسل. مرة أخرى اتصل بـ cv2.findContours وأحصل على ملامح الأرقام الحمراء.
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)

يتم ترشيح الضوضاء المتبقية بواسطة منطقة الكنتور باستخدام cv2.contourArea . قم بإزالة كل ما يقل حجمه عن 500 بكسل².
digit_contours = [cnt for cnt in digit_contours if cv2.contourArea(cnt) > 500]

الآن هذا رائع. دعنا ننفذ كل ما سبق في فئة الروبوت.
# ...
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
التعرف على الأرقام
دعونا نضيف القدرة على التعرف على الأرقام. لماذا نحتاج هذا؟
# …
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 وظيفة cv2.matchShapes لمقارنة المعالم بناءً على لحظات Hu . إنه يخفي تفاصيل التنفيذ عنا من خلال اتخاذ مسارين كمدخلات وإرجاع نتيجة المقارنة كرقم. كلما كانت أصغر ، كلما كانت الخطوط متشابهة.
cv2.matchShapes(dilate_contour, self.dilate_contours_bi_data['digit_' + str(digit)], cv2.CONTOURS_MATCH_I1, 0)
قارن الكنتور digit_contour الحالي بجميع المعايير واعثر على الحد الأدنى لقيمة cv2.matchShapes. إذا كانت القيمة الدنيا أقل من 0.15 ، فسيتم التعرف على الرقم. تم العثور على عتبة القيمة الدنيا تجريبيا. دعنا أيضًا نجمع الأحرف المتقاربة في رقم واحد.
# …
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
عند الإخراج ، سيعيد التابع self.scan_digits مصفوفة تحتوي على الرقم الذي تم التعرف عليه وتنسيق الضغط عليه. ستكون نقطة النقر هي النقطه الوسطى لمخططها.
# …
class Robot:
# …
def get_contour_centroid(self, contour):
moments = cv2.moments(contour)
return int(moments["m10"] / moments["m00"]), int(moments["m01"] / moments["m00"])
نحن نبتهج بأداة التعرف على الأرقام المستلمة ، ولكن ليس لفترة طويلة. لحظات هيو ، بصرف النظر عن المقياس ، هي أيضًا ثابتة في الدوران والانعكاسات. لذلك ، سوف يخلط الروبوت بين الأرقام 6 و 9/2 و 5. لنضيف فحصًا إضافيًا لهذه الرموز عند الرؤوس. سيتم تمييز 6 و 9 بالنقطة اليمنى العليا. إذا كان أسفل المركز الأفقي ، فسيكون 6 و 9 للعكس. بالنسبة للزوج 2 و 5 ، تحقق مما إذا كانت النقطة اليمنى العلوية تقع على الحد الأيمن للرمز.
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


تحليل الملعب
دعونا نتخطى مستوى التدريب ، يتم كتابته بالضغط على المؤشر الأبيض وبدء اللعب.
لنتخيل الملعب كشبكة. ستكون كل منطقة من مناطق اللون بمثابة عقدة مرتبطة بالجيران المتجاورين. لنقم بإنشاء فئة self.ColorArea التي ستصف منطقة / عقدة اللون.
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 = [] #
دعونا نحدد قائمة بالعقد self.color_areas وقائمة بعدد المرات التي يظهر فيها اللون على ساحة اللعب self.color_areas_color_count . قص مساحة اللعب من لقطة الشاشة.
image[pt1[1]:pt2[1], pt1[0]:pt2[0]]
حيث pt1 ، pt2 هي النقاط القصوى للإطار. نحن نكرر كل ألوان اللعبة ونطبق طريقة self.get_dilate_contours على كل منها . يشبه العثور على مخطط العقدة الطريقة التي كنا نبحث بها عن المخطط العام للرموز ، مع الاختلاف في عدم وجود ضوضاء في الملعب. يمكن أن يكون شكل العقد مقعرًا أو به ثقب ، لذا فإن النقطه الوسطى ستسقط من الشكل ولا تتناسب مع إحداث نقرة. للقيام بذلك ، ابحث عن أعلى نقطة وقم بإسقاطها بمقدار 20 بكسل. الطريقة ليست عالمية ، لكنها تعمل في حالتنا.
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)

ربط المناطق
سننظر في المناطق كجيران إذا كانت المسافة بين معالمها في حدود 15 بكسل. نحن نكرر كل عقدة مع كل منها ، ونتخطى المقارنة إذا كانت ألوانها متطابقة.
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)

نحن نبحث عن الخطوة المثلى
لدينا كل المعلومات حول الملعب. لنبدأ في اختيار خطوة. لهذا نحتاج إلى فهرس عقدة ولون. يمكن تحديد عدد خيارات النقل من خلال الصيغة:
خيارات النقل = عدد العقد * عدد الألوان - 1
بالنسبة لملعب اللعب السابق ، لدينا 7 * (5-1) = 28 خيارًا. لا يوجد الكثير منهم ، لذلك يمكننا تكرار جميع الحركات واختيار أفضلها. دعنا نحدد الخيارات كمصفوفة
select_color_weights ، حيث سيكون الصف هو مؤشر العقدة وعمود فهرس اللون وخلية نقل الوزن. نحتاج إلى تقليل عدد العقد إلى واحدة ، لذلك سنعطي الأولوية للمناطق التي لها لون فريد على السبورة والتي ستختفي بعد أن ننتقل إليها. دعونا نعطي وزن +10 لجميع صفوف العقد بلون فريد. كم مرة يظهر اللون في الملعب ، قمنا بجمعه مسبقًاself.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]
بعد ذلك ، لنلق نظرة على ألوان المناطق المجاورة. إذا كانت العقدة تحتوي على جيران لـ color_inx ، وكان عددها يساوي العدد الإجمالي لهذا اللون في ميدان اللعب ، فقم بتعيين +10 لوزن الخلية. سيؤدي هذا أيضًا إلى إزالة اللون color_inx من الحقل.
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
دعنا نعطي +1 لوزن الخلية لكل جار من نفس اللون. أي ، إذا كان لدينا 3 جيران أحمر ، فستتلقى الخلية الحمراء +3 إلى وزنها.
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
بعد جمع كل الأوزان ، نجد الحركة بأقصى وزن. دعنا نحدد أي عقدة ولون تنتمي إليها.
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)
أكمل الكود لتحديد الخطوة المثلى.
# …
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)
دعونا نضيف القدرة على التنقل بين المستويات والاستمتاع بالنتيجة. يعمل الروبوت بثبات ويكمل اللعبة في جلسة واحدة.
انتاج |
الروبوت الذي تم إنشاؤه ليس له استخدام عملي. لكن مؤلف المقال يأمل بصدق أن يساعد الوصف التفصيلي للمبادئ الأساسية لـ OpenCV المبتدئين على فهم هذه المكتبة في المرحلة الأولية.