从零开始用python实现部落冲突自动化脚本【第2篇】利用openCV模板匹配实现图像识别

前篇文章中,我们介绍了最简单的一种自动化方法——记录坐标,按顺序点击。但是问题也显而易见:如果,坐标会变动呢?又如果,某对象的坐标虽然不会变动,但我们不确定它什么时候才会出现呢?本篇我们要介绍的是一种利用OpenCV的模板匹配方法,实现对固定模板图片匹配,从而确认屏幕上是否存在和模板图片相同的部分,并得到其所在的坐标。模板匹配方法相较于深度学习方案,一个显著的优点是轻量、易上手、无需训练,我们只要明白其原理,拿来即用(你甚至完全可以让AI封装好,无需了解原理直接使用


matchTemplate方法简介

要在Python中使用OpenCV,我们先安装OpenCV:

pip install opencv-python

之后在程序中导入就可以:

import cv2

 在PyCharm中,使用鼠标中键点击该函数,我们可以快速索引到库文件中函数的定义:

def matchTemplate(image: UMat, templ: UMat, method: int, result: UMat | None = ..., mask: UMat | None = ...) -> UMat: ...
  • 📝注:虽然函数签名里有 resultmask,但在 Python 中使用时我们只需传入前 3 个参数,后两个一般由 OpenCV 内部处理,忽略即可。mask参数可以实现更高级的掩码功能,由于暂时没有用到,故不做介绍。

matchTemplate方法需要传入三个参数,第一个参数image为原图,即你要在上面进行模板匹配的源图像;第二个参数templ为模板图;第三个参数method是匹配方式,OpenCV提供了如下几种方法:

方法名全称匹配值意义推荐指数说明
cv2.TM_SQDIFF平方差匹配越小越相似对亮度敏感,容易误判
cv2.TM_SQDIFF_NORMED归一化平方差越小越相似同上但归一化
cv2.TM_CCORR相关匹配越大越相似⭐⭐简单粗暴,效果一般
cv2.TM_CCORR_NORMED归一化相关匹配越大越相似⭐⭐比上一个更稳定
cv2.TM_CCOEFF相关系数匹配越大越相似⭐⭐⭐ 
cv2.TM_CCOEFF_NORMED归一化相关系数越大越相似⭐⭐⭐⭐⭐最常用、鲁棒性最好,亮度变化也能忍

推荐初学者默认使用 cv2.TM_CCOEFF_NORMED,在多数场景下稳定且效果良好。

有兴趣的读者可以进一步了解这些方法背后的公式。例如我们要使用的TM_CCOEFF_NORMED,它的计算公式为

 

当然,在实践中,我们更关心的是如何使用。接下来从三个参数讲起。


如何使用matchTemplate?

image参数

你要在这张图上搜索模板图的位置。有以下几点要求:

  • 必须是 单通道灰度图np.ndarray 类型,dtype = uint8

  • 可以是彩色图,但你得手动转换为灰度图再传进去。例如

    img = cv2.imread("screenshot.png")
    img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

 templ参数

模板图像,要求如下:

  • 同样必须是 灰度图(不能是彩色图)

  • 类型必须和 image 相同的数据类型,通常都是 np.uint

  • 要使用有足够多特征的图像,同时不要有多余的背景和噪声影响到模板图像的纯净度。这一点我们在后续举例中会讲解。

method参数 

我们就使用 cv2.TM_CCOEFF_NORMED。

返回值:result 匹配结果矩阵

 cv2.matchTemplate() 会返回一个二维的 numpy.ndarray,我们可以叫它 resultres,它的每个元素代表模板图左上角放在原图该位置时的匹配分数

你可以把它想象成是:模板在原图左上角顶点开始,从左往右,再从上往下“滑动”过去的过程中,每一个位置都算一个“相似度打分”,所有分数就拼成了这个二维矩阵。 

  • result 的每个点,对应原图上一个可能的模板左上角位置

综上,我们可以例如这样调用matchTemplate方法:

result = cv2.matchTemplate(img_gray, template, cv2.TM_CCOEFF_NORMED)

 举个栗子,我们还是要匹配部落战的图标,这次是有进攻机会的图标,它会有点点不一样。首先看全屏截图:

……

……

……

唔,刚好手上没有正好有部落战进攻机会的账号,没法截图,不过没关系啦!都是小事,把上次的图偷过来,再放我之前截好的模板图像好啦。

 需要锚定的是图中这个位置的图标。

我们截图。在1920*1080分辨率下,截出来的模板图像应该是这样:

请注意看,这里我只截了这一小部分,甚至右上角都没截全,但是这个特征已经足够区别于没有进攻机会的按钮(在后续魔改matchTemplate方法的img_match函数中,你只需要把阈值调高一些就是啦)。没有多截的另一个原因是,由于躲开了背景的无关像素,模板图像就能确保准确性,从而确保模板匹配的成功率。模板图像截取的重点和原则是:

  • 宁可少截,也不要截取到无关像素点;同时要确保截取的图像的特征能确保它在屏幕上是唯一的(除非,你真的遇到了有多个相同图标的场景,不过在这个游戏里我目前还没遇到过)。

当然,注意力惊人的读者应该注意到了我特意说明该模板图像是在1080p下截取的。也就是说,假设一个模板图像在1920*1080分辨率下是100*100像素,那在3840*2160分辨率下它应该是200*200像素(横竖分辨率都翻倍)!这可如何是好?


resize方法:针对不同分辨率的标准化

如果我们只想截一次图,但是能在不同分辨率下都能跑,那我们需要的就是cv2.resize方法(我们在最早技术总览的序篇中也有所提及,没有读过的读者可以自行补上)。OpenCV 提供了 resize 函数来改变图像大小,我们照例先看函数签名:

def resize(src: UMat, dsize: cv2.typing.Size | None, dst: UMat | None = ..., fx: float = ..., fy: float = ..., interpolation: int = ...) -> UMat: ...

其中:

  • src:原图像

  • dsize:目标尺寸,格式为 (width, height)

  • interpolation(可选参数):插值算法,决定了缩放的“画质”,默认使用 cv2.INTER_LINEAR 就够用了,平衡速度与效果,也可以不指明。

 resize方法实际上就是帮助我们把源图像插值为指定分辨率的图像,因此我们可以使用它解决分辨率不同带来的问题。还是如上文的例子,一个截图大图,一个模板小图,我们要把哪个拿来resize?

我选用的是将屏幕截图resize成1920*1080,因为我们知道模板图像是在某个分辨率下的截图(我都是在1080p下截取的),而我们不知道模板图像在某个分辨率下实际大小是多少,如果用截图分辨率除以实际分辨率,再乘上模板图像大小得到实际大小,那就太麻烦了!而且也不能保证正确。

有了resize以后,我们就能确保在不同分辨率下,程序大概率也能运行正常了。


截图的实现

最后我们再简要介绍一下在Python中实现屏幕截图的方法。

  1. pyautogui实现
  2. pillow库实现 

 可以直接使用pyautogui.screenshot函数实现截图操作,代码如下:

import pyautogui

screenshot = pyautogui.screenshot()
screenshot.save("screenshot.png")

或者pillow库(即PIL),可以用它内置的 ImageGrab 模块:

from PIL import ImageGrab

img = ImageGrab.grab()
img.save("screenshot.png")

由于这里是全屏的截图,因此不传入任何参数,两者也是完全等价的。但在后续需要截取指定区域时,两者的参数格式会稍有不同,需要留意。不过这里还没用到,就先不介绍了。别问我怎么知道的,因为我就踩过坑(哭)。


可用的模板匹配函数:把所有功能组合起来!

 我们只需要把上述所有功能组合起来,就能搭建出一个可用的模板匹配函数了——实际上你可以看出,matchTemplate的返回值并不是该模板图像出现的坐标或范围。

# img_match.py

import cv2
import numpy as np
import pyautogui
import time


def img_match(template_path, threshold=0.75, mode='point'):
    """
    截图,尝试匹配模板图像,失败return False,成功return坐标
    默认模式 'point': 返回模板图像的中心点坐标
    'range' 模式: 返回模板图像的左上角坐标和右下角坐标
    """
    start_time = time.perf_counter()
    # 截图
    screenshot = pyautogui.screenshot()

    # 读取模板图像,并转换为灰度
    template = cv2.imread(template_path, 0)

    # 将PIL图像转换为OpenCV格式
    screenshot = np.array(screenshot)
    screenshot = cv2.cvtColor(screenshot, cv2.COLOR_RGB2BGR)

    # 获取屏幕截图的尺寸
    screenshot_height, screenshot_width = screenshot.shape[:2]

    # 将屏幕截图调整为1920x1080分辨率
    target_width = 1920
    target_height = 1080
    resized_screenshot = cv2.resize(screenshot, (target_width, target_height))

    # 将调整后的截图转换为灰度
    gray_screenshot = cv2.cvtColor(resized_screenshot, cv2.COLOR_BGR2GRAY)

    # 调用matchTemplate函数
    result = cv2.matchTemplate(gray_screenshot, template, cv2.TM_CCOEFF_NORMED)

    # 获取最佳匹配位置
    min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)

    # 检查匹配结果是否足够好
    if max_val < threshold:
        print(f'\033[31m失败 {template_path}, 系数{max_val}, 用时 {time.perf_counter() - start_time}\033[0m')
        return False  # 如果匹配结果不够好,返回False

    if mode == 'point':
        # 计算匹配区域的中心点坐标
        center_x = max_loc[0] + template.shape[1] // 2
        center_y = max_loc[1] + template.shape[0] // 2

        # 将坐标转换回原始截图的分辨率
        original_center_x = int((center_x / target_width) * screenshot_width)
        original_center_y = int((center_y / target_height) * screenshot_height)

        # 返回中心点坐标
        print(f'成功 {template_path}, 系数{max_val}, 用时 {time.perf_counter() - start_time}')
        return original_center_x, original_center_y

    elif mode == 'range':
        # 计算匹配区域的坐标
        start_x = max_loc[0]
        start_y = max_loc[1]
        end_x = max_loc[0] + template.shape[1]
        end_y = max_loc[1] + template.shape[0]

        # 将坐标转换回原始截图的分辨率
        original_start_x = int((start_x / target_width) * screenshot_width)
        original_start_y = int((start_y / target_height) * screenshot_height)
        original_end_x = int((end_x / target_width) * screenshot_width)
        original_end_y = int((end_y / target_height) * screenshot_height)

        # 返回图标所在区域的坐标(左上角和右下角)
        return (original_start_x, original_start_y), (original_end_x, original_end_y)

所有模板图像都要在指定分辨率下截取,这段代码里我给的是1080p。建议把截取好的模板图像分门别类地存放在特定文件夹中,便于查找。在使用时,只需调用img_match并传入模板图像路径,就可以了。

比如我们要在屏幕上匹配有进攻机会的部落战打开按钮(就是前面的示例),可以这样写(首先你要有截好的模板图像!):

from img_match import img_match

# "img/clan_war/open_clan_war.png"是我存储该模板图像的相对路径
clan_war_button = img_match("img/clan_war/open_clan_war.png", threshold=0.95)
if clan_war_button:
    pyautogui.click(clan_war_button)

这样就能在屏幕上匹配并点击该按钮了。


在这一节中,我们完整地从模板匹配函数开始,到实现一个可以直接调用的匹配函数的完整流程。后续的章节中,我们会介绍一些游戏内具体行为的实现(比如进攻中的“一字划”,或者进攻中的资源识别,还有流派的搭配),并在我基于PaddleOCR开发完成好自动升级的模块后,也会逐一介绍原理和实现。教程不定期更新,点个关注不迷路,谢谢(୨୧• ᴗ •͈)◞︎ᶫᵒᵛᵉ ♡

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值