使用Python+opencv实现自动扫雷

大家好,相信许多人很早就知道有扫雷这么一款经典的游戏,更是有不少人曾听说过中国雷圣,也是中国扫雷第一、世界综合排名第二的郭蔚嘉的顶顶大名。扫雷作为一款在Windows9x时代就已经诞生的经典游戏,从过去到现在依然都有着它独特的魅力:快节奏高精准的鼠标操作要求、快速的反应能力、刷新纪录的快感,这些都是扫雷给雷友们带来的、只属于扫雷的独一无二的兴奋点。

本文将介绍使用Python+OpenCV实现自动扫雷。对于自动扫雷而言,大致的开发过程是这样的:完成窗体内容截取部分,雷块分割,实现雷块类型识别,进而构建扫雷算法。

1.窗体截取

窗体截取是一个逻辑上简单,实现起来却相当麻烦的部分,而且还是必不可少的部分。通过Spy++得到了以下两点信息:

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

  • ms_arbiter.exe的主窗体名称为"Minesweeper Arbiter "

采用win32gui来获取窗体的位置信息,具体代码如下: 

hwnd = win32gui.FindWindow(class_name, title_name)
if hwnd:
left, top, right, bottom = win32gui.GetWindowRect(hwnd)

通过以上代码,得到了窗体相对于整块屏幕的位置,之后通过PIL来进行扫雷界面的棋盘截取。

from PIL import ImageGrableft += 15
top += 101
right -= 15
bottom -= 43rect = (left, top, right, bottom)
img = ImageGrab.grab().crop(rect)

这些数据仅在Windows10下测试通过,如果在别的Windows系统下,不保证相对位置的正确性,因为老版本的系统可能有不同宽度的窗体边框。

图片

2.雷块分割 

在进行雷块分割之前,事先需要了解雷块的尺寸以及它的边框大小,在ms_arbiter下,每一个雷块的尺寸为16px*16px。知道雷块尺寸,就可以进行每一个雷块的裁剪。

block_width, block_height = 16, 16blocks_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_heightx2, 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()用于完成上述的图像获取、分割过程。

3.雷块识别

这一部分可能是整个项目里除了扫雷算法本身之外最重要的部分,在进行雷块检测的时候采用了比较简单的特征,高效并且可以满足要求。 

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] = -1elif self.equal(block_color, self.rgb_to_bgr((0, 0, 255))):
self.blocks_num[x][y] = 1elif self.equal(block_color, self.rgb_to_bgr((0, 128, 0))):
self.blocks_num[x][y] = 2elif self.equal(block_color, self.rgb_to_bgr((255, 0, 0))):
self.blocks_num[x][y] = 3elif self.equal(block_color, self.rgb_to_bgr((0, 0, 128))):
self.blocks_num[x][y] = 4elif self.equal(block_color, self.rgb_to_bgr((128, 0, 0))):
self.blocks_num[x][y] = 5elif self.equal(block_color, self.rgb_to_bgr((0, 128, 128))):
self.blocks_num[x][y] = 6elif 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] = 9elif 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] = 7elif 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 = Falseif 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:表示不是扫雷游戏中的任何方块类型

4.扫雷算法实现

首先需要一个能够找出一个雷块的九宫格范围的所有方块位置的方法,因为扫雷游戏的特殊性,在棋盘的四边是没有九宫格的边缘部分的,所以要筛选来排除掉可能超过边界的访问。

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 - 1ls.append((loc_y + rel_y, loc_x + rel_x))
return lskernel_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 mapto_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 countdef 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]] = 1unopen_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 countmines_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的作用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/1179.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

汽车4S集团数据分析

派可数据分析--汽车4S集团。 派可数据汽车4S集团数据分析概述。派可数据汽车4S集团分析主题全面涵盖行业内各板块业务分析,具体包括:保险业务分析、客户关系分析、汽车保养情况分析、售后维修主题分析、整车销售分析、整车库存分析、装具销售分析、配件…

dbearver达梦连接

1、新建达梦驱动 新建驱动管理器 点击“数据库”,选择“驱动管理器” 配置 点击“新建”,出现配置界面 类名:dm.jdbc.driver.DmDriver #固定值,不能修改URL模板:jdbc:dm://{host}/DMHR #配置要连接的数据库信息默认…

【VIC水文模型】准备工作:平台软件安装

VIC水文模型所需平台软件安装 1 Arcgis安装2 Cygwin安装(Linux系统)3 Matlab/R/Fortran的安装Notepad 4 VIC模型程序代码获取参考 由于VIC模型的编程语言为C语言,交互方式为控制台输指令,需要在Linux系统上运行。Windows 上使用 …

Https网站接口被黑被恶意调取

背景: 维护的一个网站最近短信接口被黑,发送大量短信。起初以为是在网站内部操作,优化了发送短信前的操作,如添加图形验证码,屏蔽国外IP等。但后续还存在被调取情况,定位排查到是该接口在外部被恶意调取。 …

免费使用ChatGPT 4.0 和 文心一言 4.0

前言 今天给大家分享如何免费使用ChatGPT4.0 和 文心一言 4.0,废话就不多说了,我们直接入正题。 ChatGPT 4.0 先来看看如何免费使用ChatGPT 4.0 进入Coze登录 https://www.coze.com 选择大圣-GPT-4 文心一言 4.0 通过文心智能体平台,就…

Java 笔记 03:Java 基础知识,使用 IDEA 创建 Java 项目、设置注释颜色,以及自动生成 JavaDoc

一、前言 记录时间 [2024-04-21] 系列文章简摘: Java 笔记 01:Java 概述,MarkDown 常用语法整理 Java 笔记 02:Java 开发环境的搭建,IDEA / Notepad / JDK 安装及环境配置,编写第一个 Java 程序 本文讲述了…

OJ:数字三角形(搜索)

🎁个人主页:我们的五年 🔍系列专栏:每日一练 🌷追光的人,终会万丈光芒 🌷1.问题描述: ⛳️题目描述: 示出了一个数字三角形。 请编一个程序计算从顶至底的某处的一条路…

对接浦发银行支付(六)-- 请求退款接口与查询退款结果接口

一、概述 本文介绍浦发银行支付的请求退款和查询退款结果两个接口,浦发银行的退款流水号是以5901开头。发起退款的时候,浦发银行返回浦发银行退款流水号给我们(这里的我们是指对接浦发银行支付的一方,于浦发银行而言,…

面向对象设计模式之概念

设计模式系列的观点结合了《HeadFirst设计模式》(中文版)以及《设计模式:可复用面向对象软件的基础》两本书的知识,以及Sunny(刘伟)的博客 《HeadFirst设计模式》(中文版): 百度网盘链接:https://pan.baidu.com/s/1osvnUGZZREm8Jb…

「GO基础」变量

💝💝💝欢迎莅临我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

python-自动化篇-终极工具-用GUI自动控制键盘和鼠标-pyautogui-键盘

文章目录 键盘键盘——记忆宫殿入门——通过键盘发送一个字符串——typewrite()常规——键名——typewrite()常规——按下键盘——keyDown()常规——释放键盘——keyUp()升级——热键组合——hotkey() 键盘 pyautogui也有一些函数向计算机发送虚拟按键,让你能够填充…

【介绍下WebStorm开发插件】

🎥博主:程序员不想YY啊 💫CSDN优质创作者,CSDN实力新星,CSDN博客专家 🤗点赞🎈收藏⭐再看💫养成习惯 ✨希望本文对您有所裨益,如有不足之处,欢迎在评论区提出…

深入了解PBKDF2:密码学中的关键推导函数

title: 深入了解PBKDF2:密码学中的关键推导函数 date: 2024/4/20 20:37:35 updated: 2024/4/20 20:37:35 tags: 密码学对称加密哈希函数KDFPBKDF2安全密钥派生 第一章:密码学基础 对称加密和哈希函数 对称加密:对称加密是一种加密技术&…

[阅读笔记15][Orca]Progressive Learning from Complex Explanation Traces of GPT-4

接下来是微软的Orca这篇论文,23年6月挂到了arxiv上。 目前利用大模型输出来训练小模型的研究都是在模仿,它们倾向于学习大模型的风格而不是它们的推理过程,这导致这些小模型的质量不高。Orca是一个有13B参数的小模型,它可以学习到…

Java中的四种引用类型

6.Java中的引用类型 1.强引用 一个对象A被局部变量、静态变量引用了就产生了强引用。因为局部变量、静态变量都是被GC Root对象关联上的,所以被引用的对象A,就在GC Root的引用链上了。只要这一层关系存在,对象A就不会被垃圾回收器回收。所以只…

计算机视觉——OpenCV Python位运算与图像掩码

概述 位运算与图像掩码的结合允许对图像的特定区域进行精确的操作。通过使用位运算(如AND、OR、XOR和NOT),可以基于掩码的选择性地修改图像数据。位运算与图像掩码结合使用的一些关键点和应用场景: 选择性修改: 通过位…

内网云盘如何内网穿透实现公网访问

云盘是一种专业的互联网存储工具,是互联网云技术的产物,它通过互联网为企业和个人提供信息的存储、读取、下载等服务,具有安全稳定、海量存储的特点。随着企业信息化发展,云盘系统需求不断扩大,相关系统软件被广泛应用…

通用大模型研究重点之五:llama family

LLAMA Family decoder-only类型 LLaMA(Large Language Model AI)在4月18日公布旗下最大模型LLAMA3,参数高达4000亿。目前meta已经开源了80亿和700亿版本模型,主要升级是多模态、长文本方面工作。 模型特点:采用标准的…

IoC 思想简单而深邃

一、序言 本文跟大家聊聊 IoC 这一简单而深邃的思想。 二、依赖倒置原则 软件工程理论中共有六大设计原则: 单一职责原则:不存在多于一个的因素导致类的状态发生变更,即一个类只负责一项单一的职责。里氏替换原则:基类出现的地…

VSCode搭建内核源码阅读开发环境

0. 参考链接 使用VSCode进行linux内核代码阅读和开发_vscode阅读linux内核-CSDN博客 1. 搭建Linux内核源码阅读环境 现状,Linux内核源码比较庞大文件非常多,其中又包含的众多的宏定义开关配置选项,这使得阅读内核源代码称为一件头疼的事。 …