当前位置: 首页 > 后端技术 > Python

Python+OpenCV实现自动扫雷,创造自己的世界纪录!

时间:2023-03-26 19:14:26 Python

一起玩扫雷吧。使用Python+OpenCV实现自动扫雷,打破世界纪录,先来看看效果吧。中级-0.74秒3BV/S=60.81相信很多人早就知道有扫雷这样一款经典游戏(显卡测试)游戏(软件),也有很多人听说过中国雷神,同样是中国人的郭维佳,扫雷排名第一,世界综合排名第二,可谓是顶天立地的名字。作为一款诞生于Windows9x时代的经典游戏,扫雷至今仍保持着其古往今来的独特魅力:快节奏高精度的鼠标操作要求,快速的反应能力,刷新记录的快感。雷友们带来的独特刺激,只有扫雷才有。▍0x00在准备制作一套扫雷自动化软件之前,需要准备以下工具/软件/环境开发环境Python3环境-推荐3.6以上【更推荐Anaconda3,下面很多依赖库不需要待安装]numpy依赖库[如果有Anaconda不需要安装]PIL依赖库[如果你有Anaconda,则不需要安装]opencv-pythonwin32gui,win32api依赖库支持PythonIDE[可选,如果你能承受用文本编辑器写程序】扫雷Arbiter下载地址(扫雷必须用MS-Arbiter!)好了,我们的准备工作就完成了!开始吧~▍0x01做一件事情之前最重要的是什么?就是在脑海中为自己要做的事情建立一个循序渐进的框架。只有这样,才能保证在做这件事的过程中,尽量做到深思熟虑,最终才会有一个好的结果。我们在写程序的时候,尽量在正式开始开发之前心里有个大概的概念。对于本项目,大致的开发流程是:完成表单内容截取部分,完成矿块分割部分,完成矿块类型识别部分,完成扫雷算法。既然有了主意,那就撸起袖子干起来吧!01表单拦截其实对于这个项目来说,表单拦截是逻辑上简单但实现起来相当繁琐的部分,也是必不可少的部分。我们通过Spy++得到了如下两条信息:class_name="TMain"title_name="MinesweeperArbiter"ms_arbiter.exe的主窗体类是"TMain"ms_arbiter.exe的主窗体名称是"MinesweeperArbiter"你注意到了吗?主窗体名称后有一个空格。正是这个空间让笔者困扰了一阵子。只有加上这个空格,win32gui才能正常获取窗体的句柄。本项目使用win32gui获取窗口位置信息,具体代码如下:hwnd=win32gui.FindWindow(class_name,title_name)ifhwnd:left,top,right,bottom=win32gui.GetWindowRect(hwnd)通过上面的代码,我们得到了窗体相对于整个屏幕的位置。之后,我们需要使用PIL拦截扫雷界面的棋盘。我们需要先从PILimportImageGrab导入PIL库,然后再进行具体操作。left+=15top+=101right-=15bottom-=43rect=(left,top,right,bottom)img=ImageGrab.grab().crop(rect)聪明的你一定已经找到了一眼望去那些诡异的MagicNumbers,没错,这确实是MagicNumbers,就是我们通过一点点微调得到的整个棋盘相对于form的位置。注:这些数据仅在Windows10下测试,如果是在其他Windows系统下,不保证相对位置的正确性,因为旧版本系统的窗体边框宽度可能不同。橙色区域是我们需要的。好了,我们有了棋盘的形象。接下来就是对每个矿块的图像进行分割了~02雷块分割在进行矿块分割之前,我们需要提前了解矿块的大小和它的边界大小。经过笔者的测量,在ms_arbiter下,每个矿块的大小为16px*16px。知道矿块的大小,我们就可以切割每个矿块。首先,我们需要知道水平方向和垂直方向的地雷块数量。block_width,block_height=16,16blocks_x=int((right-left)/block_width)blocks_y=int((bottom-top)/block_height),我们创建一个二维数组来存储每一个一个矿块的图像,图像被分割,并保存在之前创建的数组中。defcrop_block(hole_img,x,y):x1,y1=x*block_width,y*block_heightx2,y2=x1+block_width,y1+block_heightreturnhole_img.crop((x1,y1,x2,y2))blocks_img=[[0foriinrange(blocks_y)]foriinrange(blocks_x)]foryinrange(blocks_y):forxinrange(blocks_x):blocks_img[x][y]=crop_block(img,x,y)将整个图像的获取和分割封装成一个库,可以随时调用~在笔者的实现中,我们将这部分封装到imageProcess.py中,其中使用了函数get_frame()以完成上述图像采集和分割过程。03地雷区块识别这部分可能是整个项目除了扫雷算法本身之外最重要的部分。作者在检测矿块时使用了比较简单的特征,效率高,可以满足要求。defanalyze_block(self,block,location):block=imageProcess.pil_to_cv(block)block_color=block[8,8]x,y=location[0],location[1]#-1:未打开#-2:打开但空白#-3:未初始化#Openedifself.equal(block_color,self.rgb_to_bgr((192,192,192))):ifnotself.equal(block[8,1],self.rgb_to_bgr((255,255,255))):self.blocks_num[x][y]=-2self.is_started=Trueelse:self.blocks_num[x][y]=-1elifself.equal(block_color,self.rgb_to_bgr((0,0,255))):self.blocks_num[x][y]=1elifself.equal(block_color,self.rgb_to_bgr((0,128,0))):self.blocks_num[x][y]=2elifself.equal(block_color,self.rgb_to_bgr((255,0,0))):self.blocks_num[x][y]=3elifself.equal(block_color,self.rgb_to_bgr((0,0,128))):self.blocks_num[x][y]=4elifself.equal(block_color,self.rgb_to_bgr((128,0,0))):self.blocks_num[x][y]=5elifself.equal(block_color,self.rgb_to_bgr((0,128,128))):self.blocks_num[x][y]=6elifself.equal(block_color,self.rgb_to_bgr((0,0,0))):ifself.equal(block[6,6],self.rgb_to_bgr((255,255,255))):#是mineself.blocks_num[x][y]=9elifself.equal(block[5,8],self.rgb_to_bgr((255,0,0))):#是flagself.blocks_num[x][y]=0else:self.blocks_num[x][y]=7elifself.equal(block_color,self.rgb_to_bgr((128,128,128))):self.blocks_num[x][y]=8else:self.blocks_num[x][y]=-3self.is_mine_form=如果self.blocks_num[x][y]==-3或不是self.blocks_num[x][y]==-1:self.is_new_start=False可以看出,我们采用了读取每个矿块中心点像素的方法来判断矿块的类型,进一步判断了标记、未被点击、被点击但空白等情况。具体颜值是作者。直接获取颜色,没有压缩截图的颜色,所以通过中心像素点和其他特征点结合判断类别就够了,效率高。在本项目中,我们在实现时使用了以下标注方式:1-8:表示数字1到89:表示地雷0:表示旗帜-1:表示未打开-2:表示打开但空白-3:表示不是任何类型的在扫雷游戏中阻止。通过这种简单、快速、有效的方式,我们成功地实现了高效的图像识别。04扫雷算法实现这可能是本文最精彩的部分。这里需要先说明一下具体的扫雷算法思路:遍历每一个已经有编号的矿块,判断其周围的九个方格中未开的矿块的数量是否与其相同自己的号码。都是地雷,标记它们。再次遍历每一个编号的地雷块,取九方格内所有未开的地雷块,去掉上次遍历标记为地雷的地雷块,记录并点击打开。如果以上方法无法继续,说明你遇到了死胡同,选择在所有当前未开的矿块中随机点击。(当然这个方法不是最优的,还有更多优秀的方案,但是实现起来比较麻烦。)基本的扫雷流程就是这样,我们自己实现吧~首先,我们需要一个九方格范围,可以找到所有方块位置的地雷方块方法。由于扫雷游戏的特殊性,棋盘四边的九个方格是没有边的,所以我们需要过滤排除可能超出边界的访问。defgenerate_kernel(k,k_width,k_height,block_location):ls=[]loc_x,loc_y=block_location[0],block_location[1]fornow_yinrange(k_height):fornow_xinrange(k_width):ifk[now_y][now_x]:rel_x,rel_y=now_x-1,now_y-1ls。append((loc_y+rel_y,loc_x+rel_x))returnlskernel_width,kernel_height=3,3#内核模式:[Row][Col]kernel=[[1,1,1],[1,1,1],[1,1,1]]#左边界ifx==0:foriinrange(kernel_height):kernel[i][0]=0#右边界ifx==self.blocks_x-1:foriinrange(kernel_height):kernel[i][kernel_width-1]=0#Topborderify==0:foriinrange(kernel_width):kernel[0][i]=0#Bottomborderify==self.blocks_y-1:foriinrange(kernel_width):kernel[kernel_height-1][i]=0#生成搜索图to_visit=generate_kernel(kernel,kernel_width,kernel_height,location)mineblock在棋盘的每条边上(在kernel中,1保留,0丢弃),然后通过generate_kernel函数生成最终坐标defcount_unopen_blocks(blocks):count=0forsingle_blockinblocks:ifself.blocks_num[single_block[1]][single_block[0]]==-1:count+=1返回countdefmark_as_mine(blocks):forsingle_blockinblocks:ifself.blocks_num[single_block[1]][single_block[0]]==-1:self.blocks_is_mine[single_block[1]][single_block[0]]=1unopen_blocks=count_unopen_blocks(to_visit)ifunopen_blocks==self.blocks_num[x][y]:mark_as_mine(to_visit)完成核心的生成后,我们就有了一个需要检测的矿块“通讯录”:to_visit。之后,我们使用count_unopen_blocks函数统计周围九宫格范围内未开启的块数,并与当前矿区块数进行比较,如果相等则通过mark_as_mine函数将所有九宫格块标记为地雷。defmark_to_click_block(blocks):forsingle_blockinblocks:#NotMineifnotself.blocks_is_mine[single_block[1]][single_block[0]]==1:#Click-ableifself.blocks_num[single_block[1]][single_block[0]]==-1:#SourceSyntax:[y][x]-Convertedifnot(single_block[1],single_block[0])inself.next_steps:self.next_steps.append((single_block[1],single_block[0]))defcount_mines(blocks):count=0forsingle_blockinblocks:ifself.blocks_is_mine[single_block[1]][single_block[0]]==1:count+=1返回countmines_count=count_mines(to_visit)ifmines_count==block:mark_to_click_block(to_visit)扫雷过程中的第二步我们也以与第一步类似的方式实现。先用和第一步一样的方法生成需要接入的矿块核心,然后生成具体的矿块位置,使用count_mines函数获取九宫格范围内所有矿块的个数,判断当前九宫格内的所有地雷方块都已被探测到。如果是,则使用mark_to_click_block函数排除九宫格中已经标记为地雷的地雷块,将剩余的安全地雷块添加到next_steps数组中。#分析块数self.iterate_blocks_image(BoomMine.analyze_block)#标记所有mineself.iterate_blocks_number(BoomMine.detect_mine)#计算点击哪里self.iterate_blocks_number(BoomMine.detect_to_click_block)ifself.在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中。感兴趣的可以去文末的GithubRepo查看。完整项目代码/GitHub地址|ArtrixTech/BoomMine最近整理了一套编程学习资料分享给大家,都是干货,包括教程视频、电子书、源码笔记、学习路线图、实战项目、面试题等。关注gzh【Python编程学习圈】免费领取,回复关键词【学习资料】即可,抓紧时间!