안녕하세요 여러분,
Python+OpenCV를 사용하여 자동 지뢰 제거를 구현하고 세계 기록을 깨뜨렸습니다. 먼저 그 효과를 살펴보겠습니다.
중급 - 0.74초 3BV/S=60.81
고전 게임(그래픽 카드 테스트) 게임(소프트웨어)인 지뢰찾기에 대해 오랫동안 많은 분들이 알고 계시고, 많은 분들이 들어보셨으리라 믿습니다. 중국의 썬더 세인트(Thunder Saint)는 중국 지뢰 제거 1위, 세계 2위인 궈웨이가(Guo Weijia)의 별명이기도 하다. Windows 9x 시대에 탄생한 고전 게임인 지뢰찾기는 빠른 속도와 고정밀 마우스 조작 요구 사항, 빠른 응답 기능, 기록을 세우는 스릴 등 과거부터 현재까지 여전히 고유한 매력을 갖고 있습니다. 지뢰찾기가 가져오는 모든 것 지뢰찾기 친구들이 가져다주는 독특한 즐거움은 지뢰찾기만의 고유한 특징입니다.
지뢰제거 자동화 소프트웨어를 준비하기 전에 다음 도구/소프트웨어/환경을 준비해야 합니다.
- 개발 환경
- 지뢰 찾기 소프트웨어
알겠습니다. , 그러면 준비가 완료되었습니다. 시작해 보세요~
어떤 일을 하기 전에 가장 중요한 것은 무엇인가요? 그것은 당신이 하려는 일에 대한 단계별 틀을 마음속에 구축하는 것입니다. 그래야만 이 일을 하는 과정이 최대한 신중하게 이루어지므로 결국 좋은 결과를 얻을 수 있습니다. 프로그램을 작성할 때 공식적으로 개발을 시작하기 전에 일반적인 아이디어를 염두에 두도록 최선을 다해야 합니다.
본 프로젝트의 일반적인 개발 과정은 다음과 같습니다.
자, 이제 아이디어가 생겼으니 팔을 걷어붙이고 열심히 해보자!
사실 이 프로젝트에서 폼 가로채기는 논리적으로는 간단하지만 구현하기 꽤 까다로운 부분이자 꼭 필요한 부분이기도 합니다. 우리는 Spy++를 통해 다음 두 가지 정보를 얻었습니다.
class_name = "TMain" title_name = "Minesweeper Arbiter "
알고 계셨나요? ? 메인폼 이름 뒤에 공백이 있습니다. 작성자를 한동안 고민했던 것이 바로 이 공간이었습니다. 이 공간을 추가해야만 win32gui가 정상적으로 양식의 핸들을 얻을 수 있습니다.
이 프로젝트는 win32gui를 사용하여 양식의 위치 정보를 얻습니다. 구체적인 코드는 다음과 같습니다.
hwnd = win32gui.FindWindow(class_name, title_name) if hwnd: left, top, right, bottom = win32gui.GetWindowRect(hwnd)
위 코드를 통해 전체 화면을 기준으로 한 양식의 위치를 얻습니다. 그런 다음 PIL을 사용하여 지뢰 찾기 인터페이스의 체스판을 가로채야 합니다.
먼저 PIL 라이브러리를 가져와야 합니다
from PIL import ImageGrab
그런 다음 특정 작업을 수행해야 합니다.
left += 15 top += 101 right -= 15 bottom -= 43 rect = (left, top, right, bottom) img = ImageGrab.grab().crop(rect)
당신이 똑똑하다면, 그 이상한 매직 넘버를 한 눈에 발견했을 것입니다. 그렇습니다. 이것은 우리가 약간의 미묘한 조정을 통해 얻은 형태에 대한 전체 체스판의 위치입니다. .
참고: 이 데이터는 Windows 10에서만 테스트되었습니다. 다른 Windows 시스템에서 사용하는 경우 이전 버전의 시스템에서는 양식 테두리의 너비가 다를 수 있으므로 상대 위치의 정확성이 보장되지 않습니다.
주황색 영역이 필요합니다
자, 체스판 이미지가 생겼습니다. 다음 단계는 각 광산 블록의 이미지를 분할하는 것입니다~
진행 중입니다. 블록을 분할하기 전에 천둥 블록의 크기와 경계 크기를 미리 알아야 합니다. 작성자의 측정에 따르면 ms_arbiter에서 각 광산 블록의 크기는 16px*16px입니다.
천둥 블록의 크기를 알면 각 천둥 블록을 잘라낼 수 있습니다. 먼저 수평 및 수직 방향의 지뢰 블록 수를 알아야 합니다.
block_width, block_height = 16, 16 blocks_x = int((right - left) / block_width) blocks_y = int((bottom - top) / block_height)
이후에는 각 천둥 블록의 이미지를 저장할 2차원 배열을 생성하고, 이미지를 분할하여 이전에 생성된 배열에 저장합니다.
def crop_block(hole_img, x, y): x1, y1 = x * block_width, y * block_height x2, y2 = x1 + block_width, y1 + block_height return hole_img.crop((x1, y1, x2, y2)) blocks_img = [[0 for i in range(blocks_y)] for i in range(blocks_x)] for y in range(blocks_y): for x in range(blocks_x): blocks_img[x][y] = crop_block(img, x, y)
전체 이미지 획득 및 분할 부분을 언제든지 호출할 수 있는 라이브러리로 캡슐화합니다~ 작성자 구현에서는 이 부분을 imageProcess.py에 캡슐화하고, 여기서 get_frame() 함수를 사용하여 위의 작업을 완료합니다. 이미지 획득 및 분할 프로세스.
这一部分可能是整个项目里除了扫雷算法本身之外最重要的部分了。笔者在进行雷块检测的时候采用了比较简单的特征,高效并且可以满足要求。
def analyze_block(self, block, location): block = imageProcess.pil_to_cv(block) block_color = block[8, 8] x, y = location[0], location[1] # -1:Not opened # -2:Opened but blank # -3:Un initialized # Opened if self.equal(block_color, self.rgb_to_bgr((192, 192, 192))): if not self.equal(block[8, 1], self.rgb_to_bgr((255, 255, 255))): self.blocks_num[x][y] = -2 self.is_started = True else: self.blocks_num[x][y] = -1 elif self.equal(block_color, self.rgb_to_bgr((0, 0, 255))): self.blocks_num[x][y] = 1 elif self.equal(block_color, self.rgb_to_bgr((0, 128, 0))): self.blocks_num[x][y] = 2 elif self.equal(block_color, self.rgb_to_bgr((255, 0, 0))): self.blocks_num[x][y] = 3 elif self.equal(block_color, self.rgb_to_bgr((0, 0, 128))): self.blocks_num[x][y] = 4 elif self.equal(block_color, self.rgb_to_bgr((128, 0, 0))): self.blocks_num[x][y] = 5 elif self.equal(block_color, self.rgb_to_bgr((0, 128, 128))): self.blocks_num[x][y] = 6 elif self.equal(block_color, self.rgb_to_bgr((0, 0, 0))): if self.equal(block[6, 6], self.rgb_to_bgr((255, 255, 255))): # Is mine self.blocks_num[x][y] = 9 elif self.equal(block[5, 8], self.rgb_to_bgr((255, 0, 0))): # Is flag self.blocks_num[x][y] = 0 else: self.blocks_num[x][y] = 7 elif self.equal(block_color, self.rgb_to_bgr((128, 128, 128))): self.blocks_num[x][y] = 8 else: self.blocks_num[x][y] = -3 self.is_mine_form = False if self.blocks_num[x][y] == -3 or not self.blocks_num[x][y] == -1: self.is_new_start = False
可以看到,我们采用了读取每个雷块的中心点像素的方式来判断雷块的类别,并且针对插旗、未点开、已点开但是空白等情况进行了进一步判断。具体色值是笔者直接取色得到的,并且屏幕截图的色彩也没有经过压缩,所以通过中心像素结合其他特征点来判断类别已经足够了,并且做到了高效率。
在本项目中,我们实现的时候采用了如下标注方式:
通过这种简单快速又有效的方式,我们成功实现了高效率的图像识别。
这可能是本篇文章最激动人心的部分了。在这里我们需要先说明一下具体的扫雷算法思路:
基本的扫雷流程就是这样,那么让我们来亲手实现它吧~
首先我们需要一个能够找出一个雷块的九宫格范围的所有方块位置的方法。因为扫雷游戏的特殊性,在棋盘的四边是没有九宫格的边缘部分的,所以我们需要筛选来排除掉可能超过边界的访问。
def generate_kernel(k, k_width, k_height, block_location): ls = [] loc_x, loc_y = block_location[0], block_location[1] for now_y in range(k_height): for now_x in range(k_width): if k[now_y][now_x]: rel_x, rel_y = now_x - 1, now_y - 1 ls.append((loc_y + rel_y, loc_x + rel_x)) return ls kernel_width, kernel_height = 3, 3 # Kernel mode:[Row][Col] kernel = [[1, 1, 1], [1, 1, 1], [1, 1, 1]] # Left border if x == 0: for i in range(kernel_height): kernel[i][0] = 0 # Right border if x == self.blocks_x - 1: for i in range(kernel_height): kernel[i][kernel_width - 1] = 0 # Top border if y == 0: for i in range(kernel_width): kernel[0][i] = 0 # Bottom border if y == self.blocks_y - 1: for i in range(kernel_width): kernel[kernel_height - 1][i] = 0 # Generate the search map to_visit = generate_kernel(kernel, kernel_width, kernel_height, location)
我们在这一部分通过检测当前雷块是否在棋盘的各个边缘来进行核的删除(在核中,1为保留,0为舍弃),之后通过generate_kernel函数来进行最终坐标的生成。
def count_unopen_blocks(blocks): count = 0 for single_block in blocks: if self.blocks_num[single_block[1]][single_block[0]] == -1: count += 1 return count def mark_as_mine(blocks): for single_block in blocks: if self.blocks_num[single_block[1]][single_block[0]] == -1: self.blocks_is_mine[single_block[1]][single_block[0]] = 1 unopen_blocks = count_unopen_blocks(to_visit) if unopen_blocks == self.blocks_num[x][y]: mark_as_mine(to_visit)
在完成核的生成之后,我们有了一个需要去检测的雷块“地址簿”:to_visit。之后,我们通过count_unopen_blocks函数来统计周围九宫格范围的未打开数量,并且和当前雷块的数字进行比对,如果相等则将所有九宫格内雷块通过mark_as_mine函数来标注为地雷。
def mark_to_click_block(blocks): for single_block in blocks: # Not Mine if not self.blocks_is_mine[single_block[1]][single_block[0]] == 1: # Click-able if self.blocks_num[single_block[1]][single_block[0]] == -1: # Source Syntax: [y][x] - Converted if not (single_block[1], single_block[0]) in self.next_steps: self.next_steps.append((single_block[1], single_block[0])) def count_mines(blocks): count = 0 for single_block in blocks: if self.blocks_is_mine[single_block[1]][single_block[0]] == 1: count += 1 return count mines_count = count_mines(to_visit) if mines_count == block: mark_to_click_block(to_visit)
扫雷流程中的第二步我们也采用了和第一步相近的方法来实现。先用和第一步完全一样的方法来生成需要访问的雷块的核,之后生成具体的雷块位置,通过count_mines函数来获取九宫格范围内所有雷块的数量,并且判断当前九宫格内所有雷块是否已经被检测出来。
如果是,则通过mark_to_click_block函数来排除九宫格内已经被标记为地雷的雷块,并且将剩余的安全雷块加入next_steps数组内。
# Analyze the number of blocks self.iterate_blocks_image(BoomMine.analyze_block) # Mark all mines self.iterate_blocks_number(BoomMine.detect_mine) # Calculate where to click self.iterate_blocks_number(BoomMine.detect_to_click_block) if self.is_in_form(mouseOperation.get_mouse_point()): for to_click in self.next_steps: on_screen_location = self.rel_loc_to_real(to_click) mouseOperation.mouse_move(on_screen_location[0], on_screen_location[1]) mouseOperation.mouse_click()
在最终的实现内,笔者将几个过程都封装成为了函数,并且可以通过iterate_blocks_number方法来对所有雷块都使用传入的函数来进行处理,这有点类似Python中Filter的作用。
之后笔者做的工作就是判断当前鼠标位置是否在棋盘之内,如果是,就会自动开始识别并且点击。具体的点击部分,笔者采用了作者为"wp"的一份代码(从互联网搜集而得),里面实现了基于win32api的窗体消息发送工作,进而完成了鼠标移动和点击的操作。具体实现封装在mouseOperation.py中,可以在查看完整代码:
https://www.php.cn/link/b8a6550662b363eb34145965d64d0cfb
위 내용은 대단해요, Python을 사용하여 세계 기록을 경신했어요!의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!