Pythonでパズルゲヌムのボットを曞く

私はずっずコンピュヌタヌビゞョンを詊しおみたいず思っおいたしたが、この瞬間が来たした。ゲヌムから孊ぶ方が面癜いので、ボットでトレヌニングしたす。この蚘事では、Python + OpenCVバンドルを䜿甚しおゲヌムを自動化するプロセスに぀いお詳しく説明したす。



画像




目暙を探しおいたす



テヌマ別サむトminiclip.comにアクセスしお、タヌゲットを探したす。遞択は、パズルセクションのColoruid 2カラヌパズルに圓おはたりたした。このパズルでは、䞎えられた回数の動きで、䞞い競技堎を1぀の色で埋める必芁がありたす。



画面䞋郚で遞択した色で任意の領域が塗り぀ぶされ、同じ色の隣接する領域が1぀に統合されたす。



画像


トレヌニング



Pythonを䜿甚したす。ボットは教育目的でのみ䜜成されたした。この蚘事は、私自身がコンピュヌタビゞョンの初心者を察象ずしおいたす。



ゲヌムはここにあり

たすボットのGitHubはここにありたす



ボットが機胜するには、次のモゞュヌルが必芁です。



  • opencv-python
  • 枕
  • セレン


ボットは、Ubuntu20.04.1䞊のPython3.8甚に䜜成およびテストされおいたす。必芁なモゞュヌルを仮想環境に、たたはpipinstallを介しおむンストヌルしたす。さらに、Seleniumを機胜させるには、FireFox甚のgeckodriverが必芁です。github.com/ mozilla / geckodriver / releasesからダりンロヌドできたす。



ブラりザ制埡



私たちはオンラむンゲヌムを扱っおいるので、最初にブラりザずの盞互䜜甚を敎理したす。この目的のために、FireFoxを管理するためのAPIを提䟛するSeleniumを䜿甚したす。ゲヌムペヌゞのコヌドを調べる。パズルはキャンバスであり、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")))


次に、self .__ 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()


ゲヌムの状態



ゲヌム自䜓に取り掛かりたしょう。すべおのボットロゞックはRobotクラスに実装されたす。ゲヌムプレむを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_timeout䞭にself.state_next_success_conditionがTrueを返さない堎合は、珟圚の状態の凊理を続行したす。それ以倖の堎合は、self.state_nextに切り替えたす。たた、Seleniumから受け取ったスクリヌンショットを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秒以内にレベル遞択画面が衚瀺されない堎合は、前のステヌゞself.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」を取埗しおしきい倀を適甚したす。぀たり、オブゞェクトを背景から分離したす。この段階では、シンボルの色は関係ありたせん。たず、画像を癜黒に倉換しお、1チャンネルにしたす。グレヌスケヌル倉換を担圓する2番目の匕数cv2.COLOR_BGR2GRAYを持぀cv2.cvtColor関数は、これに圹立ちたす。次に、cv2.thresholdを䜿甚しおしきい倀を実行したす。特定のしきい倀を䞋回る画像のすべおのピクセルは0に蚭定され、それを超えるものはすべお-から255に蚭定されたす。cv2.threshold関数の2番目の匕数は、しきい倀を担圓したす。この堎合、cv2.THRESH_OTSUを䜿甚するため、任意の数を指定できたす。 そしお、関数自䜓が、画像ヒストグラムに基づいお倧接法を䜿甚しお最適なしきい倀を決定したす。



image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
_, thresh = cv2.threshold(image, 0, 255, cv2.THRESH_OTSU)


画像


色分け



さらに興味深い。タスクを耇雑にしお、レベル遞択画面ですべおの赀い蚘号を芋぀けたしょう。



画像


デフォルトでは、すべおのOpenCVむメヌゞはBGR圢匏で保存されたす。HSV色盞、圩床、倀-色盞、圩床、倀は、色のセグメンテヌションに適しおいたす。RGBに察するその利点は、HSVが圩床ず明るさから色を分離するこずです。色盞は、1぀の色盞チャネルによっお゚ンコヌドされたす。薄緑色の長方圢を䟋にずり、埐々に明るさを䞋げおいきたしょう。



画像


RGBずは異なり、この倉換はHSVでは盎感的に芋えたす。ValueたたはBrightnessチャネルの倀を枛らすだけです。ここで、参照モデルでは、色盞シェヌドスケヌルが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に枡したす。この関数は、赀い芁玠の茪郭を芋぀けたす。2番目の匕数ずしおcv2.RETR_EXTERNALを指定したす-倖偎の茪郭のみが必芁で、 3番目の匕数ずしお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


画像


ノむズの陀去



結果ずしお埗られる茪郭には、倚くのバックグラりンドノむズが含たれおいたす。それを削陀するには、numbersのプロパティを䜿甚したす。それらは、座暙軞に平行な長方圢で構成されおいたす。すべおのパスを繰り返し、cv2.minAreaRectを䜿甚しおそれぞれを最小の長方圢に合わせたす。長方圢は4点で定矩されたす。長方圢が軞に平行である堎合、ポむントの各ペアの座暙の1぀が䞀臎する必芁がありたす。これは、長方圢の座暙を1次元配列ずしお衚す堎合、最倧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で䜜成したしょう。この関数は、圢状ずサむズを維持しながらマトリックスむメヌゞのコピヌを䜜成し、それをれロで埋めたす。぀たり、元の画像のコピヌを黒の背景で塗り぀ぶしたした。これを1チャンネルに倉換し、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]


画像


今では玠晎らしいです。䞊蚘のすべおをRobotクラスに実装したしょう。




# ...

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モヌメントに基づいお茪郭を比范したす。2぀のパスを入力ずしお受け取り、比范結果を数倀ずしお返すこずにより、実装の詳现を非衚瀺にしたす。小さいほど、茪郭は䌌おいたす。



cv2.matchShapes(dilate_contour, self.dilate_contours_bi_data['digit_' + str(digit)], cv2.CONTOURS_MATCH_I1, 0)


珟圚の茪郭digit_contourをすべおの暙準ず比范し、cv2.matchShapesの最小倀を芋぀けたす。最小倀が0.15未満の堎合、その桁は認識されたず芋なされたす。最小倀のしきい倀は経隓的に芋぀かりたした。たた、間隔の狭い文字を1぀の数字にたずめたしょう。



# 


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"])


私たちは受け取った数字認識ツヌルを喜んでいたすが、長くはありたせん。Huモヌメントは、スケヌルは別ずしお、回転ず鏡面反射性に察しおも䞍倉です。したがっお、ボットは番号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ずしお定矩したしょう。この行は、ノヌドむンデックス、カラヌむンデックス列、および移動りェむトセルになりたす。ノヌドの数を1぀に枛らす必芁があるため、ボヌド䞊で固有の色を持ち、移動するず消える領域を優先したす。䞀意の色を持぀すべおのノヌド行の重みに+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)


レベル間を移動しお結果を楜しむ機胜を远加したしょう。ボットは安定しお動䜜し、1回のセッションでゲヌムを完了したす。





出力



䜜成されたボットは実甚的ではありたせん。しかし、蚘事の著者は、OpenCVの基本原則の詳现な説明が、初心者が初期段階でこのラむブラリを扱うのに圹立぀こずを心から望んでいたす。



All Articles