Bonjour à tous,
a implémenté le déminage automatique à l'aide de Python+OpenCV et a battu le record du monde. Jetons d'abord un coup d'œil à l'effet.
Intermédiaire - 0,74 seconde 3BV/S=60,81
Je crois que beaucoup de gens connaissent le jeu classique (test de carte graphique) (logiciel) comme Minesweeper depuis longtemps, et beaucoup de gens en ont entendu parler. Le Chinois Thunder Saint est également le nom principal de Guo Weijia, qui occupe le premier rang en matière de déminage en Chine et le deuxième au monde. En tant que jeu classique né à l'ère de Windows 9x, Minesweeper a toujours son charme unique du passé au présent : des exigences de fonctionnement rapides et de haute précision avec la souris, des capacités de réponse rapides et le plaisir d'établir des records. tout ce que le dragueur de mines apporte L'excitation unique apportée par les dragueurs de mines est propre au dragueur de mines.
Avant de vous préparer à créer un logiciel d'automatisation de dragueur de mines, vous devez préparer les outils/logiciels/environnement suivants
- Environnement de développement
- Logiciel démineur
D'accord, alors nos préparatifs sont terminés. Commençons ~
Quelle est la chose la plus importante avant de faire quelque chose ? Il s’agit de construire dans votre esprit un cadre d’étapes pour ce que vous allez faire. Ce n'est qu'ainsi que nous pourrons garantir que le processus de réalisation de cette tâche soit aussi réfléchi que possible, afin qu'il y ait un bon résultat à la fin. Lorsque nous écrivons des programmes, nous devons faire de notre mieux pour avoir une idée générale en tête avant de commencer officiellement le développement.
Pour ce projet, le processus général de développement est le suivant :
Bon, maintenant que nous avons une idée, retroussons nos manches et travaillons dur !
En fait, pour ce projet, l'interception de formulaire est une partie logiquement simple mais assez gênante à mettre en œuvre, et c'est aussi une partie indispensable. Nous avons obtenu les deux informations suivantes via Spy++ :
class_name = "TMain" title_name = "Minesweeper Arbiter "
Avez-vous remarqué ? Il y a un espace après le nom du formulaire principal. C'est cet espace qui a troublé l'auteur pendant un moment. Ce n'est qu'en ajoutant cet espace que win32gui peut obtenir normalement le handle du formulaire.
Ce projet utilise win32gui pour obtenir les informations de position du formulaire. Le code spécifique est le suivant :
hwnd = win32gui.FindWindow(class_name, title_name) if hwnd: left, top, right, bottom = win32gui.GetWindowRect(hwnd)
Grâce au code ci-dessus, nous obtenons la position du formulaire par rapport à tout l'écran. Après cela, nous devons utiliser PIL pour intercepter l'échiquier de l'interface du dragueur de mines.
Nous devons d'abord importer la bibliothèque PIL
from PIL import ImageGrab
puis effectuer des opérations spécifiques.
left += 15 top += 101 right -= 15 bottom -= 43 rect = (left, top, right, bottom) img = ImageGrab.grab().crop(rect)
Si vous êtes intelligent, vous devez avoir découvert ces étranges nombres magiques en un coup d'œil. Oui, ce sont bien des nombres magiques. Ils sont la position de l'échiquier entier par rapport à la forme que nous obtenons grâce à un petit ajustement subtil. .
Remarque : Ces données sont uniquement testées sous Windows 10. Si elles sont utilisées sous d'autres systèmes Windows, l'exactitude de la position relative n'est pas garantie car les anciennes versions du système peuvent avoir des largeurs de bordures de formulaire différentes.
La zone orange est ce dont nous avons besoin
D'accord, nous avons l'image de l'échiquier, la prochaine étape est de segmenter les images de chaque bloc de mine~
En cours. Avant la segmentation des blocs, nous devons connaître à l’avance la taille du bloc Thunder et la taille de sa bordure. Selon les mesures de l'auteur, sous ms_arbiter, la taille de chaque bloc de mine est de 16px*16px.
Connaissant la taille du bloc tonnerre, nous pouvons couper chaque bloc tonnerre. Nous devons d’abord connaître le nombre de blocs de mines dans les directions horizontale et verticale.
block_width, block_height = 16, 16 blocks_x = int((right - left) / block_width) blocks_y = int((bottom - top) / block_height)
Après cela, nous créons un tableau bidimensionnel pour stocker l'image de chaque bloc tonnerre, segmentons l'image et l'enregistrons dans le tableau précédemment créé.
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)
Encapsuler toute la partie acquisition et segmentation d'images dans une bibliothèque, qui peut être appelée à tout moment~ Dans l'implémentation de l'auteur, nous encapsulons cette partie dans imageProcess.py, dans lequel la fonction get_frame() est utilisée pour compléter ce qui précède Processus d’acquisition et de segmentation d’images.
这一部分可能是整个项目里除了扫雷算法本身之外最重要的部分了。笔者在进行雷块检测的时候采用了比较简单的特征,高效并且可以满足要求。
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
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!