Mac在Python项目中通过opencv模版匹配定位不到图片

起因

原本一行代码的事情,但是在Mac上总能出现意外,如下

box = pyautogui.locateOnScreen('obsidian.png')  
print(box)  
pyautogui.moveTo(box[0],box[1])

上面的代码用来定位图片在屏幕中的位置,然后移动鼠标到定位到的屏幕位置坐标。

意外的结果如下:
在这里插入图片描述

排除的情况:

  • 图片相对路径是一定没错的(左边目录结构同级)
  • 截图真实存在屏幕中

看了很多人(window用户)定位图片简简单单一行代码搞定,为什么到我这里就无限报错呢,所以模版匹配(定位图片)到底是怎么回事?

理解pyautogui定位原理

locateOnScreen源码
在这里插入图片描述

Note:(本质只有两件事情)

  • screenshot(region=None)截屏
  • locate携带截屏数据和目标图片数据以及多值字典定位图片

截屏源码
在这里插入图片描述

因为pillow版本大于6并且没有指定region所以pyautogui截屏本质是调用底层库ImageGrab得到的。

# pyautogui 截屏
screenshotIm = screenshot(region=None)# 本质等于
im = ImageGrab.grab()

定位源码
在这里插入图片描述

Note:

  • 默认就是底层调用opencv进行定位
  • 如果没有指定grayscale,默认就是灰度图
  • 核心定位方法为cv2.matchTemplateopencv的模版匹配

所以pyautogui定位图片的调用本质是opencv的模版匹配那么opencv的模版匹配原理是什么呢?

理解opencv模版匹配

理解计算机中的图片

我们知道颜色是由三原色RGB(红色Red,绿色Green,蓝色Blue)组成,一张图片就是由给定的像素点组成,通常我们说图片大小为100*100就是横着存在100个像素点,竖着存在100个像素点,而每个像素点记录一个RGB值,当像素越密集,我们看到的图片就越清晰。

在这里插入图片描述

因此对于彩色图而言,如果图为100*100意味着存在矩阵R[],矩阵G[],矩阵B[]大小为100行100列,分别记录每个像素点的二进制表示,也就是0~255。

在这里插入图片描述

由于二进制可以进行十六进制转换,因此一个像素点通常也可以表示为两种模式:

  • RGB:比如RGB(255, 255, 255)
  • 十六进制:比如0XFFFFFF

如果图片是灰度模式,则不需要三个通道RGB记录,只需要亮度即可,也就是说在灰度图中,记录一张图片的信息只需要每个像素点的亮度值表示即可!即一个二维数组

在这里插入图片描述

总结:在计算机中图片由很多的像素点组成,其中每个像素点在计算机中以二进制形式进行存储,可以是彩色模式,也可以是灰度模式。

理解模版匹配

给定一个图片,如何在屏幕中(源图)中定位给定图片的位置呢?这就是模版匹配。

我们先看一下屏幕的表示

0,0       X increases -->
+---------------------------+
|                           | Y increases
|                           |     |
|   1920 x 1080 screen      |     |
|                           |     V
|                           |
|                           |
+---------------------------+ 1919, 1079

在屏幕中我们以左上角为坐标轴原点(0, 0),分辨率(1920*1080)表示在横轴X上存在1920个像素点(pixel),在纵轴Y上存在1080个像素点。

那我要查找某个图片,图片肯定涵盖多个像素点,比如我们截屏(100*100),我们称这个为盒子(box),匹配一张图片最起码需要两张大小相同的图进行对比,那我们就需要在屏幕上截取大小相同的图然后进行像素点的差值比较,最后计算整个像素的差异,就可以得到匹配的相关性数据了。

比如原图大小(100*100)即10000个像素点,需要查找的图(10*10)即100个像素点,那么就需要从坐标原点(0,0)截取长宽为10的一个盒子和原图进行比较,怎么比较呢?就是每个像素点依次做差值,这样就可以判断像素颜色是否相近,最后得到100个像素点的差值,最后进行计算得到一个相关性系数,范围(0~1)。
在这里插入图片描述

如果原图大小为(W, H)需要查找的目标图大小为(w, h),那么在X轴方向就需要移动(W-w+1)次,也就是这么多像素点,同理Y轴方向为(H-h+1)次,所以针对上面的情况,进行匹配将会得到结果大小为91*91的数组结果,其中每个元素代表区块的匹配程度,显然我们只需要最接近的那个区块,也就是结果中R[0,0,…,0,1]中最后的一个数字。

代码验证
在这里插入图片描述

Note:

  • 图片对比需要保证两张图片大小相同,即像素点一样才具有可比性。
  • 因此原图中查找的基本思路就是从原点开始逐个像素点移动得到长宽一样的图进行比较。
  • 匹配的思路是每个像素点的差值大小记为相关性,在每个位置都能得到一个相关系数,因此匹配的结果大小一定是(W-w+1)*(H-h+1)

为什么100*100的原图查找10*10X轴到90就可以了?*

因为再移动就出原图边界了,这样得到的原图块大小和目标图不一致,无法比较! 因为我们得到这些数据后,只需要记录左上角坐标(90, 90)即可。

opencv模版匹配方法的使用

文档链接

基本定义

cv.matchTemplate(image, templ, method[, result[, mask]]) ->result

其中参数:

  • image:表示原图数据,如果彩色则是三维数组,如果灰度图则为二维数组
  • templ:需要查找的图数据,同上,图片需要数组表示
  • method:模版匹配的算法,就是做差值时候怎么得到相关系数的算法
    • cv.TM_SQDIFF
    • cv.TM_SQDIFF_NORMED
    • cv.TM_CCORR
    • cv.TM_CCORR_NORMED
    • cv.TM_CCOEFF
    • cv.TM_CCOEFF_NORMED (pyautogui底层默认的算法)

使用

# 屏幕截屏, 默认是RGBA模式,也可以直接用pyautogui.screenshot()底层就是下面代码
screen = ImageGrab.grab()  
# 加载图片为数组数据,指定灰度图,也可通过 Image.open('obsidian.png') 加载图片,但是同上是图片对象,非数组RGBA模式  
target_img = cv2.imread('obsidian.png', cv2.IMREAD_GRAYSCALE)  # 将屏幕图片转为数组,并且模式为灰度图  
screen_img = cv2.cvtColor(np.array(screen), cv2.COLOR_RGBA2GRAY)  result = cv2.matchTemplate(screen_img, target_img, cv2.TM_CCOEFF_NORMED)  
# 从所有的相关系数结果中找到最大最小值,以及坐在的坐标位置  
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)  print(min_val, max_val, min_loc, max_loc) # 结果
# -0.6216521859169006 0.637797474861145 (37, 10) (22, 102)

可以看到最大相关系数只有0.63,这个图片是我的图标默认存在的,这里是存在问题的

分析问题

分辨率对查找的影响

要知道我们使用pyautogui的目的是定位图片,获取图片在屏幕中所在的坐标位置,这里涉及三个东西:

  • 目标图
  • 原图(屏幕截图)
  • 屏幕坐标位置

已知我的屏幕大小为1440*900,那么给定原图为2880*1800,那么我查找目标图时候怎么得到在屏幕中的坐标位置?比如目标刚好在右下角,难道得到坐标(2800,1720)?显然这么走鼠标都要点击到屏幕外边去了。

在这里插入图片描述

# 从所有的相关系数结果中找到最大最小值,以及坐在的坐标位置  
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)

这行代码返回的max_loc对应上图就是(20,12)显然这是超出屏幕大小的,因此使用这个坐标点击屏幕肯定不行,一种思路是等比例计算在屏幕中的位置,即上图的坐标(10,6)

再看下面的问题
在这里插入图片描述

按照理论,查找的图片应该位于屏幕之外的位置,可是这里得到的结果却是完全错的,并且最大相关系数也才0.63,这是因为,原图比例等比例放大了一倍,但是查找的目标图并没有,这意味着,在计算机中存储的数值完全是不相关的!

我们可以通过放大目标图去匹配如下:

target_img = cv2.resize(target_img, (width*2, height*2))

结果如下:
在这里插入图片描述

可以看到相关系数和坐标都看似正常了,通过pyautogui移动鼠标位置基本正确。

解决问题

上面我们已经知道,截屏的尺寸问题会影响对图片的查找,这里的本质其实是:我的目标图要在原图的基础上截取! 如果我不使用Image.grab()截屏而是直接手动截图存储然后查找就可以了。

方式一:手动截屏

# 加载目标图为数组数据  
target_img = cv2.imread('obsidian.png', cv2.IMREAD_GRAYSCALE)  
# 加载原图为数组数据  
screen_img = cv2.imread('screenshot.png', cv2.IMREAD_GRAYSCALE)  result = cv2.matchTemplate(target_img, screen_img, cv2.TM_CCOEFF_NORMED)  
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)  
print(min_val, max_val, min_loc, max_loc)

结果图
在这里插入图片描述

可以看到最大相关系数为0.89,坐标也基本正确。

方式二:调整截屏大小

# 加载目标图为数组数据  
target_img = cv2.imread('obsidian.png', cv2.IMREAD_GRAYSCALE)  
# 截屏默认为RGBA  
screen = ImageGrab.grab()  
# numpy将图片转为数组数据  
screen_tmp = np.array(screen)  
# 将RGBA通道转为灰度图  
screen_img = cv2.cvtColor(screen_tmp, cv2.COLOR_RGBA2GRAY)  
print("截屏大小:" + str(screen_img.shape))  
# 调整截屏大小为屏幕分辨率大小  
width, height = pyautogui.size()  
screen_img = cv2.resize(screen_img, (width, height))  
print("调整后截屏大小:" + str(screen_img.shape))  result = cv2.matchTemplate(screen_img, target_img, cv2.TM_CCOEFF_NORMED)  
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)  
print(min_val, max_val, min_loc, max_loc)

结果图
在这里插入图片描述

Note:

  • 一定要确保通道转换正确,RGBA转GRAY
  • 查看截图的图片模式可以通过Image.grab().mode查看

方式三:调整目标图大小

# 加载目标图为数组数据  
target_img = cv2.imread('obsidian.png', cv2.IMREAD_GRAYSCALE)  # 截屏默认为RGBA  
screen = ImageGrab.grab()  
# numpy将图片转为数组数据  
screen_tmp = np.array(screen)  
# 将RGBA通道转为灰度图  
screen_img = cv2.cvtColor(screen_tmp, cv2.COLOR_RGBA2GRAY)  # 等比例调整目标图大小  
width_screen, height_screen = pyautogui.size()  
height_original, width_original = screen_img.shape[:2]  
# 计算调整比例  
rate_width = width_original // width_screen  
rate_height = height_original // height_screen  
print(rate_width, rate_height)  height, width = target_img.shape[:2]  
target_img = cv2.resize(target_img, (width * rate_width, height * rate_height))  result = cv2.matchTemplate(screen_img, target_img, cv2.TM_CCOEFF_NORMED)  
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)  # 坐标等比例换算  
min_loc = min_loc[0] // rate_width, min_loc[1] // rate_width  
max_loc = max_loc[0] // rate_height, max_loc[1] // rate_height  
print(min_val, max_val, min_loc, max_loc)

结果图
在这里插入图片描述


总结: 出现问题的根因在于底层库截屏得到的屏幕图片分辨率和屏幕不一致导致的,解决办法也就是根据这个思路进行,推荐resize调整截屏大小,然后再进行定位图片就可以了。

关于为什么出现Mac截图分辨率放大一倍的解释,可以参考这条issue

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

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

相关文章

实战OpenCV之像素操作

基础入门 在OpenCV中,像素是最基本的操作单位。图像可以视为一个三维数组,其中第三维表示颜色通道。图像数据在内存中以连续或几乎连续的方式存储,对于多通道图像(比如:BGR图像),每个像素的各通…

EmguCV学习笔记 VB.Net 11.5 目标检测

版权声明:本文为博主原创文章,转载请在显著位置标明本文出处以及作者网名,未经作者允许不得用于商业目的。 EmguCV是一个基于OpenCV的开源免费的跨平台计算机视觉库,它向C#和VB.NET开发者提供了OpenCV库的大部分功能。 教程VB.net版本请访问…

初识时序数据库InfluxDB

最近项目开发中,需要记录时间序列的日志信息,InfluxDB 刚好契合。于是准备研究一下,发现已经有整理很好的文档,以下两篇觉得很好,入门开发可以参考一下。 因为项目是用C#开发的,因此,简单介绍一下C#开发中,InfluxDB的API使用。 1.简介 InfluxDB是一个由InfluxData开发…

《食品安全导刊》是什么级别的期刊?是正规期刊吗?能评职称吗?

问题解答 问:《食品安全导刊》是不是核心期刊? 答:不是,是知网收录的正规学术期刊。 问:《食品安全导刊》级别? 答:国家级。主管单位: 中国商业联合会 主办单…

ONLYOFFICE8.0部署集成(vue+java)并配置存储为minio

文章目录 前言一、使用docker安装onlyoffice8安装使用DockerDesktop方式命令行方式使用 HTTPS 运行展示 二、项目集成前端集成-vue3html方式后端集成-java 三、onlyoffice基础原理四、配置存储为minio1.onlyoffice配置文件挂载问题2.配置存储为minio3.验证切换minio存储是否生效…

生成式AI介绍

生成式AI介绍 生成式AI(Generative AI)是人工智能领域的一种技术,能够通过学习现有数据来生成新的内容。不同于传统的人工智能模型只进行分类、回归等分析任务,生成式AI具备创作能力,能够生成文本、图像、音频甚至视频…

基于spring拦截器实现博客项目的强制登录功能(四)

6. 强制登录 当⽤⼾访问 博客列表和博客详情⻚ 时, 如果⽤⼾当前尚未登陆, 就⾃动跳转到登陆⻚⾯. 我们可以采⽤拦截器来完成, token通常由前端放在header中, 我们从header中获取token, 并校验 token是否合法 6.1 添加拦截器 package com.example.spring_blog_24_9_8.config;…

性能测试-jmeter的控制器(十六)

一、if控制器 需求:使用“用户自定义变量”定义name变量,值可以是“baidu”或“itcast”,使用变量值,控制是否访问对应网站。 1、步骤: 在测试计划中添加用户定义的变量name,取值可为baidu或itcast添加两个http请求&#xff1a…

misc音频隐写

一、MP3隐写 (1)题解:下载附件之后是一个mp3的音频文件;并且题目提示keysyclovergeek;所以直接使用MP3stego对音频文件进行解密;mp3stego工具是音频数据分析与隐写工具 (2)mp3stego工具的使用:…

CSS实现前端布局更巧妙的方案!在 flex 布局中通过使用 margin 实现水平垂直居中以及其他常见的前端布局

在前端开发中,实现水平垂直居中一直是个热门话题。随着 CSS Flexbox 布局的普及,开发者们开始更多地使用 justify-content 和 align-items 这两个属性来解决这个问题。 然而,还有一种更加简洁、灵活的方式——使用 margin: auto; 来实现居中以…

大数据之Flink(二)

4、部署模式 flink部署模式: 会话模式(Session Mode)单作业模式(Per-Job Mode)应用模式(Application Mode) 区别在于集群的生命周期以及资源的分配方式;以及应用的main方法到底在…

Vue3使用vue-qrcode-reader实现扫码绑定设备功能

需求描述 移动端进入网站后,登录网站进入设备管理界面。点击添加设备,可以选择直接添加或者扫一扫。点击扫一扫进行扫描二维码获取设备序列号自动填充到添加设备界面的序列号输入框中。然后点击完成进行设备绑定。 安装vue-qrcode-reader 这里使用的版…

2024.9.11 作业

绘制组件制作时钟 代码&#xff1a; /*******************************************/ 文件名&#xff1a;widget.h /*******************************************/ #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QPaintEvent> #include &l…

MAX3483ESA+T具有±15kV ESD保护的+3.3V、低功耗收发器,适用于RS-485和RS-422通信

MAX3483ESAT具有15kV ESD保护的3.3V、低功耗收发器&#xff0c;适用于RS-485和RS-422通信。每个器件包含一个驱动器和一个接收器。MAX3483ESAT具有限摆率驱动器&#xff0c;可充分降低EMI并减少因电缆端接不当引起的反射&#xff0c;从而实现数据速率高达250kbps的无误差数据传…

【中间件】-容器编排平台Kubernetes简介

目录 什么是K8s 为什么需要K8s 什么是容器(Contianer) K8s能做什么&#xff1f; K8s的架构原理 控制平面(Control plane) kube-apiserver etcd kube-scheduler kube-controller-manager cloud-controller-manager 小结 节点组件(Node) container runtime Pod kubelet ku…

AnyChart 数据可视化框架

AnyChart 数据可视化框架 AnyChart 是一个灵活的 JavaScript&#xff08;HTML5、SVG、VML&#xff09;图表框架&#xff0c;适合任何需要数据可视化的解决方案。 目录 下载并安装开始插件将 AnyChart 与 TypeScript 结合使用将 AnyChart 与 ECMAScript 6 结合使用技术集成贡献…

Anolis OS 7.9(龙蜥操作系统)上Oracle12C Release 2 (12.2)打补丁

本文的oracle使用的是单实例环境 一、打补丁前环境准备 1、确保make, ar, ld,和 nm四个可执行命令在$PATH中 export PATH$PATH:/bin2、查看已装的Oracle的OPatch版本 #切换到oracle用户 su - oracle#进入到数据库的安装目录下的opatch目录 cd /ora01/app/oracle/product/12…

JS_函数声明

JS中的方法,多称为函数,函数的声明语法和JAVA中有较大区别 函数说明 函数没有权限控制符不用声明函数的返回值类型,需要返回在函数体中直接return即可,也无需void关键字参数列表中,无需数据类型调用函数时,实参和形参的个数可以不一致声明函数时需要用function关键字函数没有…

github actions CICD简单使用案例

参考&#xff1a; https://developer.aliyun.com/article/1540773 https://github.com/ViggoZ/producthunt-daily-hot/blob/main/.github/workflows/generate_markdown.yml 1、创建github项目 目录&#xff1a; .github/workflows/fetch-news.yml actions执行yaml&#xff08;…

C语言 | Leetcode C语言题解之第397题整数替换

题目&#xff1a; 题解&#xff1a; //第一种动态规划:超时 // class Solution { // public: // int integerReplacement(int n) { // vector<int>dp(n1,0); // dp[1]0; // for(int i2;i<n;i){ // if(i%20){ // …