本文所述的方法都是基于前几章的后台点击,因此同样需要绑定窗口句柄。
Python自动化(6)——图像模块
识色
定点比色
def cv2CompareColorOneMatch(self, x, y, hexColor, _similar=0, border=None):
startX = 0
startY = 0
similar = _similar + self.colorOffset
if border:
startX = border[0]
startY = border[1]
color = self.Hex2RGB(hexColor)
screenQImg = self.screen.captureScreen(None, border)
if int(x)-startX <= 0:
print('cv2CompareColorOneMatch error x: '+str(x)+', startX: '+str(startX))
if int(y)-startY <= 0:
print('cv2CompareColorOneMatch error y: '+str(y)+', startY: '+str(startY))
res = QColor(screenQImg.pixel(int(x)-startX, int(y)-startY)).getRgb()
print('cv2CompareColorOneMatch x: '+str(x)+', y: '+str(y)+', re: '+str(res))
if abs(res[0] - color[0]) < similar and abs(res[1] - color[1]) < similar and abs(res[2] - color[2]) < similar:
return True
else:
return False
参数:
前两个传输是绑定的窗口的x,y坐标
hexColor:16进制的色值(传字符串,例如:“#fffbeb”)
_similar:色值偏移值,默认为0,一般会传3~5
border:截图范围,默认截全屏。截图截少一点,(理论上)性能好一点,使用时一般只会截那个点周围的十来个像素
返回值:如果截图判断的点与传进来的色值相减,RGB每个值都在色值偏移范围内,返回True,否则返回False
其中,self.Hex2RGB是将16进制色值转换为RGB值的方法,可以在最下面的全部代码看到。
self.colorOffset是全局变量,用于设置全局的色值偏差。因为某些屏幕会有色差,所以需要这个设置
定点比色的核心代码是通过QImage类的pixel方法获取到对应的像素点数据,然后转换为QColor对象,再通过QColor对象的getRgb方法获取到对应像素点的RGB色值,然后再与传进来的参数对比,得出结果。
多点比色
def cv2CompareColorMoreMatch(self, lists, _similar=0, border=None, screenQImg=None, isIgnoreBorder=False):
if screenQImg == None:
screenQImg = self.screen.captureScreen(None, border)
startX = 0
startY = 0
similar = _similar + self.colorOffset
if not isIgnoreBorder and border:
startX = border[0]
startY = border[1]
# print('cv2CompareColorMoreMatch')
for x, y, hexColor in lists:
color = self.Hex2RGB(hexColor)
if int(x)-startX <= 0:
print('cv2CompareColorOneMatch error x: '+str(x)+', startX: '+str(startX))
if int(y)-startY <= 0:
print('cv2CompareColorOneMatch error y: '+str(y)+', startY: '+str(startY))
res = QColor(screenQImg.pixel(int(x)-startX, int(y)-startY)).getRgb()
if abs(res[0] - color[0]) > similar or abs(res[1] - color[1]) > similar or abs(res[2] - color[2]) > similar:
return False
return True
参数:
lists:需要比较色值点的列表,例如:[[998,262,’#fffbeb’], [999,329,’#fffbeb’]]
_similar:色值偏移值,同定点比色
border:截图范围,默认截全屏。同定点比色
screenQImg:截的图片,格式是QImage,默认为空,为空时会根据border截图
isIgnoreBorder:是否忽略截图范围,默认为false。当已有一张全屏图的时候,可以用此参数。例如:graph.cv2CompareColorMoreMatch(pointList,5,border,screenshot,screenshot!=None)
这样就是如果有全屏图就忽略border,否则根据border来截图
多点比色实际上只是支持了多个点对比,核心代码同定点比色。
找色
单点找色
def cv2FindColor(self, hexColor, border=None):
color = list(self.Hex2RGB(hexColor))
screenImg = self.screen.captureScreen(None, border)
array = numpy.array(Image.fromqimage(screenImg))
res = numpy.argwhere(numpy.all(array == color, axis=2)).tolist()
print('cv2FindColor res: '+str(res))
return res
参数:
hexColor:字符串,16进制色值(带#号)
border:截屏范围,默认为全屏
返回值:返回全部色值相同位置的数组
单点找色的核心逻辑,其实就是先将QImage转换为PIL库的Iamge对象,然后通过numpy库的array方法将Image转换为数组以便进行数值操作。
接着使用numpy.all方法比较numpy数组中的每个像素值与指定的RGB色值,返回一个bool数组,表示哪些像素匹配指定颜色。
最后使用numpy.argwhere方法,返回bool数组中值为True的索引,然后通过tolist方法将numpy数组转换为python列表。
一般来说,这个方法比较少用,限制比较多
多点找色
def cv2FindColors(self, hexColorListStr, border=None):
screenImg = self.screen.captureScreen(None, border)
array = numpy.array(Image.fromqimage(screenImg))
startX = 0
startY = 0
w = None
h = None
if border == None:
left, top, right, bottom = win32gui.GetWindowRect(self.hwnd)
w = right-left
h = bottom-top
else:
startX = border[0]
startY = border[1]
w = border[2]-border[0]
h = border[3]-border[1]
rgby = []
ps = []
a = 0
firstXY = []
res = numpy.empty([0, 2])
hexColorStr = hexColorListStr.split(',')
for i in hexColorStr:
rgb_y = i[-13:]
r = int(rgb_y[0:2], 16)
g = int(rgb_y[2:4], 16)
b = int(rgb_y[4:6], 16)
y = int(rgb_y[-2:])
rgby.append([r,g,b,y])
for i in range(1, len(hexColorStr)):
ps.append([int(hexColorStr[i].split('|')[0]), int(hexColorStr[i].split('|')[1])])
for i in rgby:
result = numpy.logical_and(abs(array[:, :, 0:1] - i[0]) < i[3], abs(array[:, :, 1:2] - i[1]) < i[3], abs(array[:, :, 2:3] - i[2]) < i[3])
results = numpy.argwhere(numpy.all(result == True, axis=2)).tolist()
if a == 0:
firstXY = copy.deepcopy(results)
else:
nextnextXY = copy.deepcopy(results)
for index in nextnextXY:
index[0] = int(index[0]) - ps[a - 1][1]
index[1] = int(index[1]) - ps[a - 1][0]
q = set([tuple(t) for t in firstXY])
w = set([tuple(t) for t in nextnextXY])
matched = numpy.array(list(q.intersection(w)))
if len(matched)==0:
return -1,-1
res = numpy.append(res, matched, axis=0)
a += 1
res = res.tolist()
for i in res:
if res.count(i) == len(hexColorStr) - 1:
print('cv2FindColors res: '+str(res))
return i[1] + startX, i[0] + startY
print('cv2FindColors not find')
return -1,-1
多点找色其实就是大漠插件里的多点找色实现的,其核心还是上述的找色逻辑,这里不再赘述。
识图
单模版匹配
def cv2OneMatchFindImage(self, rect, temp, qimg=None, similar=0.85):
img = None
if qimg:
img = cv2.cvtColor(numpy.asarray(Image.fromqimage(qimg)),cv2.COLOR_RGB2BGR)
else:
img = cv2.cvtColor(numpy.asarray(Image.fromqimage(self.screen.captureScreen(None,rect))),cv2.COLOR_RGB2BGR)
template = cv2.cvtColor(numpy.asarray(temp),cv2.COLOR_RGB2BGR)
h, w = template.shape[:2]
# 匹配模板
res = cv2.matchTemplate(img, template, cv2.TM_CCOEFF_NORMED)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
if max_val >= similar:
# 计算矩形左边
top_left = max_loc
bottom_right = (top_left[0] + w, top_left[1] + h)
# 返回rect数组,参数分别是topLeft,topRight,bottomLeft,bottomRight,中心点x,中心点y
rect = (top_left[0], top_left[1], top_left[0]+w, top_left[1]+h, int(int(2*top_left[0]+w)/2), int(int(2*top_left[1]+h)/2))
print('cv2OneMatchFindImage rect: '+str(rect))
print('cv2OneMatchFindImage max_val: '+str(max_val))
return rect
else:
return None
参数:
rect:截图范围,为空截全屏
temp:被查找的图片,小图,PIL库的Image对象
qimg:大图,在这张图上找temp那张图,QImage对象
similar:相似度,默认0.85
模板匹配实际上就是在一张大图中找小图。其核心是基于OpenCV库的cv2.matchTemplate方法。
首先,将两张图片的颜色空间都从RGB转换为BGR,OpenCV使用BGR作为默认颜色空间,然后获取模板图像的高度h和宽度w。
然后通过cv2.matchTemplate方法进行模板匹配,在大图中寻找小图temp的位置,并返回一个二维数组,表示每个位置的匹配结果(此方法有多个不同的匹配方式,试了一下大差不差吧,没有什么最准的)。接着通过cv2.minMaxLoc方法找到最小值和最大值及其对应的位置:
min_val:最小匹配值
max_val:最大匹配值
min_loc:最小值的位置
max_loc:最大值的位置
如果匹配结果的最大匹配值满足相似度要求,则计算顶点和中心点的位置并返回。否则返回空(None)。
注意:单模板匹配只会返回最匹配的一个结果,多模板匹配会返回全部满足相似度要求的结果。
另外,这里要说明一下模板匹配的实现以及问题:
实际使用的情况中,会有时候得不到正确的结果。因此研究了一下内部逻辑,这里简单说一下。
首先看一下OpenCV文档:https://docs.opencv.org/3.4/de/da9/tutorial_template_matching.html
(文档是英文,这里为了方便翻译为中文截图)
匹配方法有6种:
TM_SQDIFF:平方差匹配法
TM_SQDIFF_NORMED:归一化平方差匹配法
TM_CCORR:相关匹配法
TM_CCORR_NORMED:归一化相关匹配法
TM_CCOEFF:系数匹配法
TM_CCOEFF_NORMED:归一化相关系数匹配法
这里以TM_CCOEFF_NORMED归一化相关系数匹配法为例,公式计算过程详解:
假设有一张大图:
以及一张小图:
然后写一个简单的代码进行目标匹配并显示结果
代码:
import cv2
import numpy as np
import matplotlib.pyplot as plt
def match_and_display(image_path, template_path, method, similarity_threshold=0.9):
img = cv2.imread(image_path, cv2.IMREAD_COLOR)
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
template = cv2.imread(template_path, cv2.IMREAD_GRAYSCALE)
h, w = template.shape
res = cv2.matchTemplate(img_gray, template, method)
loc = np.where(res >= similarity_threshold)
# 在目标图像上绘制匹配区域的矩形框
for pt in zip(*loc[::-1]):
top_left = pt
bottom_right = (pt[0] + w, pt[1] + h)
cv2.rectangle(img, top_left, bottom_right, (0, 255, 0), 2)
# 使用 Matplotlib 显示结果图像
plt.figure(figsize=(6, 6))
plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
plt.title('Matched Results')
plt.axis('off')
plt.show()
示例调用
image_path = './source.png'
template_path = './temp.png'
method = cv2.TM_CCOEFF_NORMED
match_and_display(image_path, template_path, method, similarity_threshold=0.95)
结果:
TM_SQDIFF(平方差匹配法):
TM_SQDIFF_NORMED(归一化平方差匹配法):
TM_CCORR(相关匹配法):
TM_CCORR_NORMED(归一化相关匹配法):
TM_CCOEFF:系数匹配法):
TM_CCOEFF_NORMED:归一化相关系数匹配法)
从结果可以看到,很多结果都把大图中两个相似的点都识别出来了,甚至还有的匹配方法识别失败了,TM_CCOEFF_NORMED匹配方法看起来是对了,不过当我把相似度降低到0.9时,一样会把大图中左上角的也匹配进结果中:
从官方文档可以知道,cv2进行模板匹配时,是以模板大小的搜索框依次遍历整张大图的。假设小图宽高为(w,h),大图宽高为(W,H),那么遍历时就绪遍历(W-w+1)次,每列需要遍历(H-h+1)次。
以下列的矩阵为例:
假设小图的矩阵为:
根据公式:
对比公式得出,完全匹配会得到1,完全负相关匹配会得到-1,完全不匹配会得到0
假设匹配的是第一个点,首先两边同时减去各自的均值,得到公式中的T ‘和I’:
=》
=》
然后求两个矩阵的内积,以及两个矩阵内元素平方和的平方的乘积再开根号:
result = 6/7.7459 = 0.7746
类似的,我们可以得出,当模板匹配到下面两个矩阵的时候,得出的值也是很接近1的
=》
=》
result = 1(完全匹配)
=》
=》
result = -5/5 = -1(完全不匹配)
那么,为什么上述的结果中,当相似度设置为0.9时,会把完全负相关的那一块也匹配到呢。
经过我的计算,当矩阵为
0 0
4 3 时,得到的结果:
result = 6.5/7.9843 = 0.8140
从结果可以看出,完全负相关周围的矩阵,其实还是有可能匹配到相似度比较高的结果,因此,cv2的模板匹配是有可能不准的。
不过,一般来说,只要取最匹配的值,一般来说结果还是可靠的。
但是,通过这次的探究,使用模板匹配时建议设置的值不低于0.65,这个是我认为比较安全的值,因为按照模板匹配的算法,可能不相关的矩阵也能算出来有0.5的相似度甚至更高,总之使用时不建议相似度设置得太低
多模板匹配
def cv2MoreMatch(self, imagePath, tempImgPath, similar=0.9):
img = cv2.imread(imagePath)
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
template = cv2.imread(tempImgPath, 0)
h, w = template.shape[:2]
res = cv2.matchTemplate(img_gray, template, cv2.TM_CCOEFF_NORMED)
# numpy.where返回的坐标值(x,y)是(h,w),注意h,w的顺序
loc = numpy.where(res >= similar)
rects = []
for pt in zip(*loc[::-1]):
top_left = pt
bottom_right = (pt[0] + w, pt[1] + h)
# 返回rect数组,参数分别是topLeft,topRight,bottomLeft,bottomRight,中心点x,中心点y
rect = (top_left[0], top_left[1], top_left[0]+w, top_left[1]+h, int(int(2*top_left[0]+w)/2), int(int(2*top_left[1]+h)/2))
rects.append(rect)
print('cv2MoreMatch rect: '+str(rect))
return rects
多模板匹配的参数其实与单模板匹配相同,也是传入大图、小图以及相似度。
不同的是多模板匹配使用了numpy.where方法筛选出符合相似度的结果,并返回的是一个数组。
前台找图
def pyAutoGUIMatch(self, imagePath, rect=None, similar=0.9, grayscale=False):
rectInWindow = None
if rect == None:
left, top, right, bottom = win32gui.GetWindowRect(self.hwnd)
rectInWindow = (left, top, right-left, bottom-top)
print('rectInWindow: '+str(rectInWindow))
else:
rectInWindow = (rect[0], rect[1], rect[2]-rect[0], rect[3]-rect[1])
pos = pyautogui.locateOnScreen(imagePath, region=rectInWindow, confidence=similar, grayscale=grayscale)
print('pyAutoGUIMatch pos: '+str(pos))
return pos
前台找图是通过pyautogui.locateOnScreen方法实现的,需要注意的是,如果自己的电脑连接了多个屏幕时,此方法无法在第二个屏幕上截图,如果传入的x1值大于屏幕的宽度,会导致报错needle dimension(s) exceed the haystack image or region dimensions
完整代码
#! /usr/bin env python3
# -*- coding:utf-8 -*-
# 图形处理模块
import numpy
import cv2
import pyautogui
import win32gui
from Screen import Screen
from PyQt5.QtGui import QColor
from PIL import Image
import copy
class Graph():
def __init__(self):
self.screen = Screen()
self.colorOffset = 0
print('Graph init')
def bind(self, hwnd):
self.hwnd = hwnd
self.screen.bind(hwnd)
def setColorOffset(self, colorOffset):
self.colorOffset = colorOffset
# 图形处理方法1——使用cv2(默认) ############################################
# 单个模板匹配
def cv2OneMatch(self, imagePath, tempImgPath):
img = cv2.imread(imagePath)
template = cv2.imread(tempImgPath)
h, w = template.shape[:2]
# 匹配模板
res = cv2.matchTemplate(img, template, cv2.TM_CCOEFF_NORMED)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
# 计算矩形左边
top_left = max_loc
bottom_right = (top_left[0] + w, top_left[1] + h)
# 返回rect数组,参数分别是topLeft,topRight,bottomLeft,bottomRight,中心点x,中心点y
rect = (top_left[0], top_left[1], top_left[0]+w, top_left[1]+h, int(int(2*top_left[0]+w)/2), int(int(2*top_left[1]+h)/2))
print('cv2OneMatch rect: '+str(rect))
return rect
# 单个模板匹配
# @rect 需要被截图的范围(left, right, top, bottom),为空则全窗口截图
# @temp 小图,PIL.Image格式
# @qimg
def cv2OneMatchFindImage(self, rect, temp, qimg=None, similar=0.85):
img = None
if qimg:
img = cv2.cvtColor(numpy.asarray(Image.fromqimage(qimg)),cv2.COLOR_RGB2BGR)
else:
img = cv2.cvtColor(numpy.asarray(Image.fromqimage(self.screen.captureScreen(None,rect))),cv2.COLOR_RGB2BGR)
template = cv2.cvtColor(numpy.asarray(temp),cv2.COLOR_RGB2BGR)
h, w = template.shape[:2]
# 匹配模板
res = cv2.matchTemplate(img, template, cv2.TM_CCOEFF_NORMED)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
if max_val >= similar:
# 计算矩形左边
top_left = max_loc
bottom_right = (top_left[0] + w, top_left[1] + h)
# 返回rect数组,参数分别是topLeft,topRight,bottomLeft,bottomRight,中心点x,中心点y
rect = (top_left[0], top_left[1], top_left[0]+w, top_left[1]+h, int(int(2*top_left[0]+w)/2), int(int(2*top_left[1]+h)/2))
print('cv2OneMatchFindImage rect: '+str(rect))
print('cv2OneMatchFindImage max_val: '+str(max_val))
return rect
else:
return None
# 多个模板匹配
def cv2MoreMatch(self, imagePath, tempImgPath, similar=0.9):
img = cv2.imread(imagePath)
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
template = cv2.imread(tempImgPath, 0)
h, w = template.shape[:2]
res = cv2.matchTemplate(img_gray, template, cv2.TM_CCOEFF_NORMED)
# numpy.where返回的坐标值(x,y)是(h,w),注意h,w的顺序
loc = numpy.where(res >= similar)
rects = []
for pt in zip(*loc[::-1]):
top_left = pt
bottom_right = (pt[0] + w, pt[1] + h)
# 返回rect数组,参数分别是topLeft,topRight,bottomLeft,bottomRight,中心点x,中心点y
rect = (top_left[0], top_left[1], top_left[0]+w, top_left[1]+h, int(int(2*top_left[0]+w)/2), int(int(2*top_left[1]+h)/2))
rects.append(rect)
print('cv2MoreMatch rect: '+str(rect))
return rects
def Hex2RGB(self, hex):
r = int(hex[1:3], 16)
g = int(hex[3:5], 16)
b = int(hex[5:7], 16)
return r, g, b
# 定点比色
def cv2CompareColorOneMatch(self, x, y, hexColor, _similar=0, border=None):
startX = 0
startY = 0
similar = _similar + self.colorOffset
if border:
startX = border[0]
startY = border[1]
color = self.Hex2RGB(hexColor)
screenQImg = self.screen.captureScreen(None, border)
if int(x)-startX <= 0:
print('cv2CompareColorOneMatch error x: '+str(x)+', startX: '+str(startX))
if int(y)-startY <= 0:
print('cv2CompareColorOneMatch error y: '+str(y)+', startY: '+str(startY))
res = QColor(screenQImg.pixel(int(x)-startX, int(y)-startY)).getRgb()
print('cv2CompareColorOneMatch x: '+str(x)+', y: '+str(y)+', re: '+str(res))
if abs(res[0] - color[0]) < similar and abs(res[1] - color[1]) < similar and abs(res[2] - color[2]) < similar:
return True
else:
return False
# 多点比色
def cv2CompareColorMoreMatch(self, lists, _similar=0, border=None, screenQImg=None, isIgnoreBorder=False):
if screenQImg == None:
screenQImg = self.screen.captureScreen(None, border)
startX = 0
startY = 0
similar = _similar + self.colorOffset
if not isIgnoreBorder and border:
startX = border[0]
startY = border[1]
# print('cv2CompareColorMoreMatch')
for x, y, hexColor in lists:
color = self.Hex2RGB(hexColor)
if int(x)
完整自动化工程代码:https://gitee.com/chj-self/PythonRobotization
大佬们找到问题欢迎拍砖~