كتابة روبوت للعبة ألغاز بيثون

لطالما أردت أن أجرب يدي في رؤية الكمبيوتر وقد حانت هذه اللحظة. من المثير للاهتمام التعلم من الألعاب ، لذلك سوف نتدرب على روبوت. سأحاول في هذه المقالة أن أصف بالتفصيل عملية أتمتة اللعبة باستخدام حزمة Python + OpenCV.



صورة




تبحث عن هدف



نذهب إلى موقع 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


التعرف على الأرقام



دعونا نضيف القدرة على التعرف على الأرقام. لماذا نحتاج هذا؟ لأننا نستطيع... هذه الميزة ليست إلزامية لكي يعمل الروبوت ، وإذا رغبت في ذلك ، يمكنك قطعها بأمان. ولكن نظرًا لأننا نتعلم ، فسنضيفه لحساب النقاط التي تم تسجيلها وفهم الروبوت في أي خطوة على المستوى. بمعرفة الحركة النهائية للمستوى ، سيبحث الروبوت عن زر للانتقال إلى الخطوة التالية أو تكرار الزر الحالي. وإلا ، فسيتعين عليك البحث عنها بعد كل خطوة. دعونا نتخلى عن استخدام Tesseract وننفذ كل شيء باستخدام OpenCV. يعتمد التعرف على الأرقام على مقارنة لحظات hu ، مما سيتيح لنا مسح الأحرف بمقاييس مختلفة. هذا مهم لأن هناك أحجام خطوط مختلفة في واجهة اللعبة. المستوى الحالي ، حيث نختار المستوى ، نحدد SQUARE_BIG_SYMBOL: 9 ، حيث 9 هو الجانب الأوسط من المربع بالبكسل الذي يتكون منه الرقم. قص صور الأرقام وحفظها في مجلد البيانات. في القاموس الذاتي.dilate_contours_bi_data يحتوي على مراجع كفافية يمكن مقارنتها. سيكون الفهرس هو اسم الملف بدون ملحق (على سبيل المثال "digit_0").



# …

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 المبتدئين على فهم هذه المكتبة في المرحلة الأولية.



All Articles