厉害了,用Python破个世界纪录!转载

原创
小哥 3年前 (2022-10-17) 阅读数 57 #大杂烩

来源丨https://zhuanlan.zhihu.com/p/35755039

大家好,我是大家好,我是大家好,我是 Python爱好者 ~

许多合伙人要求一些推荐许多合伙人要求一些推荐Python练习项目,今天再推荐一个好玩的。练习项目,今天推荐一个有趣的项目。

用Python+OpenCV实现了自动扫雷,打破了世界纪录,先来看看效果。

中级 - 0.74秒 3BV/S=60.81

相信很多人早就知道,有像扫雷者这样经典的游戏(显卡测试)(软件),也有很多人听说过中国扫雷舰圣人郭伟佳的顶级名字,他也是中国第一,世界总排名第二。扫雷舰作为一款游戏在Windows9x这款诞生于过去的经典游戏到现在仍有其独特的魅力:快节奏、高精确度的鼠标操作要求、快速的反应能力、刷新记录的刺激,这些都是扫雷舰带给扫雷舰的独特刺激,只属于扫雷舰。

0x00 准备****

在准备创建扫雷舰自动化软件之前,您需要准备一些工具,如下所示/软件/环境

  • 开发环境
  1. Python3 环境 - 推荐3.6或者以上 [更加推荐Anaconda3不需要安装以下许多依赖项]

  2. numpy依赖库 [如有Anaconda然后不需要安装,不需要安装,不需要安装]

  3. PIL依赖库 [如有Anaconda然后不需要安装,不需要安装,不需要安装]

  4. opencv-python

  5. win32gui、win32api依赖库

  6. 支持Python的IDE [可选,如果您可以允许在文本编辑器中编写程序]

  • 扫雷软件

· Minesweeper Arbiter(必须使用(必须使用MS-Arbiter(扫雷!)(进行扫雷!)(进行扫雷!)(进行扫雷!)

好了,我们的准备工作都做好了!我们开始吧!~

0x01 实现思路****

在你做某事之前,最重要的事情是什么?就是要做到心中有数,建立一个循序渐进的框架。只有这样,才能确保在做这件事的过程中,尽可能地想好,让最终的结果有一个好的结果。我们也应该在编写程序之前,尽量做到在正式开始开发之前,心里有个大概的想法。

对于这个项目,总体开发过程如下所示。

  1. 完成表格内容截取部分完成表格内容截取部分

  2. 完成了矿块分割部分,完成了矿块分割部分

  3. 完成地雷区块类型识别部分

  4. 完成扫雷算法完成扫雷算法

好了,现在我们有主意了,让我们卷起袖子加油干吧!

- 01 窗体截取

事实上,对于这个项目来说,表单拦截是一个逻辑上简单但实现起来相当麻烦的部分,而且仍然是必不可少的部分。我们做这件事是通过Spy++获得了以下两条信息。获得了以下两条消息。获得了两条信息。获得了两条消息。

class_name = "TMain"
title_name = "Minesweeper Arbiter "
  • ms_arbiter.exe的主窗体类别的主窗体类别"TMain"

  • ms_arbiter.exe的主窗体的名称"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)

聪明的你一定是一眼就看出了那些奇怪的东西Magic Numbers是的,这确实是,是的,确实是是的,它是。是的,确实是这样Magic Numbers这是整个董事会相对于表格的位置,我们通过一些微调获得了这一位置。

注:这些数据仅在中可用注:这些数据仅在中可用注意:此数据仅在中可用Windows10如果测试在另一项下通过如果测试在另一项下通过如果测试在另一项下通过Windows在该系统下,不能保证正确的相对位置,因为较早版本的系统可能具有不同宽度的窗体边框。

橙色区域是我们所需要的,橙色区域是我们所需要的全部

好的,我们有了黑板的图像,下一步是分割每个地雷区块的图像~

- 02 雷块分割

在进行矿块分割之前,我们需要预先知道矿块的大小和边界大小。经笔者测算,ms_arbiter下,每个雷区的大小是每个雷区的大小,每个雷区的大小是16px*16px。

一旦我们知道了雷区的大小,我们就可以对每个雷区进行切割。首先,我们需要知道水平和垂直方向上的块数。

block_width, block_height = 16, 16
  blocks_x = int((right - left) / block_width)
  blocks_y = int((bottom - top) / block_height)

之后,我们创建一个二维数组来存储每个矿块的图像,并进行图像分割,该图像分割保存在先前创建的数组中。

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)

图像采集和分割的整个部分都封装在一个可以随时调用的库中。OK啦~在作者的实现中,我们将这一部分封装到imageProcess.py函数在哪里,函数在哪里,函数在哪里get_frame()用它来完成上述图像采集和分割过程。

- 03 雷块识别

除了扫雷算法本身,这部分可能是整个项目中最重要的部分。本文采用相对简单的特征进行地块检测,效率较高,能够满足要求。

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

如你所见,我们用读取每个雷块的中心像素来确定雷块的类别,并进一步判断插入标志、未点击、已点击但空白等情况。具体的颜色值由作者直接获取,截图的颜色没有被压缩,通过结合中心像素和其他特征点来确定类别就足够了,实现了很高的效率。

在这个项目中,我们实现了以下符号。

  • 1-8:表示数字:表示数字:表示数字1到8

  • 9:表示地雷表示地雷

  • 0:表示旗帜:表示旗帜插入:表示旗帜放置

  • -1:表示未打开:表示未打开:表示未打开

  • -2:表示打开但为空:表示打开但为空:表示打开但为空

  • -3:表示它不是扫雷舰游戏中的任何类型的方块

通过这种简单、快速、有效的方法,我们成功地实现了高效的图像识别。

- 04 扫雷算法实现扫雷算法实现

这可能是这篇文章最令人兴奋的部分。在这里,我们需要从解释扫雷算法的确切想法开始。

  1. 遍历已有编号的每个地雷区块,并确定其周围九框网格中未打开的地雷区块的数量是否与其自身的编号相同,如果相同,则表示周围九框网格中的所有地雷都已标记。

  2. 再次使用数字遍历每个块,获取九个框范围内所有未打开的块,删除已被上一次迭代标记为地雷的块,记录它们并点击它们。

  3. 如果上述方法不能继续进行,则意味着遇到了死锁,可以选择随机点击所有当前未打开的地雷区块。(这种方法当然不是最优的,有一个更好的解决方案,但实施起来相对繁琐)

基本的扫雷过程是这样的,所以让我们手工完成~

首先,我们需要一种方法,能够找出扫雷舰区块九个方格范围内所有方块的位置。由于扫雷舰游戏的特殊性,棋盘的四边没有九个方格的边缘部分,所以我们需要过滤掉可能超出边界的访问。

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对于舍入),接着是for舍入),接着是for舍入),然后是for舍入),之后传递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"代码(从Internet收集),用于实现win32api表单消息发送工作,依次完成鼠标移动和点击操作。具体的实现封装在mouseOperation.py可以在中查看的完整代码中的完整代码。在完整的代码可以在查看在完整的代码可以查看在

https://github.com/ArtrixTech/BoomMine

·················END·················

最后送出100张我们星球的优惠券,感兴趣的伙伴可以看看,三天内随意退款,一起学习Python吧!

推荐阅读:
入门: 最完整的零基学习最全面的零基学习最完整的零基学习Python的问题  | 从零开始学习从零基础学习从零基础学习8个月的Python  | 实战项目 |学Python这是捷径这是捷径就是这条捷径
干货:爬行豆瓣短评,电影《后来的我们》 | 38年NBA最佳球员分析最佳球员分析 |   从万众期待到口碑惨败!唐探3令人失望  | 笑新伊田图龙记笑新伊田图龙记笑新伊田图龙记 | 谜语之王回答灯谜之王灯谜之王谜语之王 |用Python人山人海素描图人山人海素描图人山人海 Dishonor太火了,我用机器学习做了一个迷你推荐系统电影
趣味:弹球游戏  | 九宫格  | 漂亮的花 | 两百行Python日常酷跑游戏日常酷跑游戏日常酷跑游戏!
AI: 会写诗的机器人会写诗的机器人会写诗的机器人 | 给图片上色给图片上色给图片上色 | 预测收入 | 《耻辱》太火了,我用机器学习做了一部迷你推荐系统电影
小工具: Pdf转Word易于修复表单和水印!易于处理的表单和水印!轻松修复桌子和水印!易于修复的形式和水印! | 一键把html将页面另存为网页另存为网页另存为pdf!|  再见PDF提款费!提款费!提款费!提款费用! | 用90构建最强大的代码行构建最强大的代码行构建最强大的代码行PDF转换器,word、PPT、excel、markdown、html一键转换 | 制作一个固定的低成本机票提醒!制作一张别针的低价机票提醒! |60代码行做了一个语音墙纸切换,天天见女士!

年度弹出文案年度弹出文案年度爆炸性文案

  • 1). 卧槽!Pdf转Word用Python轻松搞定 !

  • 2).学Python闻起来好香!我用100一行代码做了一个网站,帮助人们做了一行代码,做了一个网站,帮助了人们做了一行代码,帮助了人们PS旅行图片赚鸡腿吃旅行图片赚鸡腿

  • 3).第一次播放量过亿,火爆全网,我分析了《波妹》,发现了这些秘密

  • 4). 80一行行代码!使用Python让救济金做正确的事做做的人做好事的人A梦分身

  • 5).你必须掌握的东西你必须掌握20个python代码,简短而紧凑,永无止境的有用代码,简短而甜蜜,永无止境的有用的代码,简短而紧凑,永无止境的使用代码,简短而甜蜜,永无止境的用途

  • 6). 30个Python古怪技能集古怪小贴士收藏古怪技能集

  • 7). 我总结的80《菜鸟学习专页》《菜鸟学习专页》《菜鸟学习》Python精选干货.pdf》,都是干货

  • 8). 再见Python!我要学Go了!2500词深度分析词深度分析词深度分析 !

  • 9).发现一只舔狗的福利!这Python爬虫神器太酷了,不能自动下载女孩的照片

点击阅读原文点击查看点击点击阅读点击阅读原文点击查看B放我鸽子看录像!站在我的录像带上!在视频里放我鸽子!站在我的录像带上!

版权声明

所有资源都来源于爬虫采集,如有侵权请联系我们,我们将立即删除

热门