前言
本专栏前几篇提到OpenCV模板匹配的原理和代码实现解析,时过几年,同时伴随AI各种模型的发展,现在又遇见了类似的应用场景,基于图片模板匹配识别图片位置的UI自动化场景的一些思考,对比基于kaza特征匹配是在阅读Airtest实现源码时获取到信息,本次不做过多讲解仅对比一下优点和缺点。
文章调研问题背景
尝试解决解决 Airtest 图片模板匹配问题的有些思考,是否有其他更优解?
1、匹配慢
2、匹配失败
一、OpenCV模板匹配 简短回顾和新的猜想
1.1 识别目标 框标记
通过专栏上文获取信息,在大图中标记滑块的位置是否准确,可以本地人工先Check检查一下是否准确,调试自测用
1.2 OpenCV 模板匹配的场景方法和各自识别特点
标志参数 |
简记 |
作用 |
TM_SQDIFF |
0 |
平方差匹配法 |
TM_SQDIFF_NORMED |
1 |
归一化平方差匹配法 |
TM_CCORR |
2 |
相关匹配法 |
TM_CCORR_NORMED |
3 |
归一化相关匹配法 |
TM_CCOEFF |
4 |
系数匹配法 |
TM_CCOEFF_NORMED |
5 |
归一化相关系数匹配法 |
1.2.1 以下表格对比所有方法的特性,便于快速参考
标志参数 | 简记 | 最佳匹配值 | 光照敏感性 | 典型应用场景 |
---|---|---|---|---|
TM_SQDIFF | 0 | 最小值 (0) | 高敏感 | 光照稳定的像素级匹配 |
TM_SQDIFF_NORMED | 1 | 接近 0 | 中等敏感 | 需归一化的平方差场景 |
TM_CCORR | 2 | 最大值 | 不敏感 (对偏移敏感) | 亮度差异小的通用匹配 |
TM_CCORR_NORMED | 3 | 接近 1 | 低敏感 | 复杂背景的物体识别 |
TM_CCOEFF | 4 | 正值最大 | 低敏感 | 光照不均的场景 |
TM_CCOEFF_NORMED | 5 | 1 (最大值) | 不敏感 | 高鲁棒性需求,如变化光照 |
1.2.2 使用建议
算法选择:随着方法从简单(如TM_SQDIFF)到复杂(如TM_CCOEFF_NORMED),计算开销增加,但匹配精度和鲁棒性提高。优先选择归一化版本以提高泛化性。
局限性::模板匹配仅支持平移,对旋转或缩放无效;实际应用中需结合其他技术处理变形.
性能优化: 在OpenCV中,使用matchTemplate函数并指定method参数,通过minMaxLoc获取最佳匹配位置。
1.3 OpenCV TM_CCOEFF_NORMED 归一化相关系数匹配法 验证预期点击图片在当前界面中的位置
自爆问题:取的相似度前10(可自定义),未去重
基于Airtest框架UI自动化识别图片位置的流程解决方法和思考
前提
1、模板截图,应该是在pycharm原图大小格式打开的,测试资源
2、原图就是手机截图
3、取的相似度前10,还没有去重
4、方法就是自带的图片模板匹配, 使用归一化相关系数法进行模板匹配
PS:
1、可能问题出在模板匹配,人工截图大小保存的问题
2、其他步骤应该都是通用的 获取手机当前屏幕图(通用耗时)、模板图、模板匹配算法(通用耗时)
既然下图示例能找到图片的位置并框,从技术层面是可以识别点击图片,至于是否经得起不同项目的考验和推敲未知,只是阐述一种解决方案,那就能找到点击坐标的中心点,后续均未处理需要自行处理并优化。
1.3.1 图例1 匹配XX页面某一个按钮图片
识别速度 0.118秒
1.3.2 图例2 匹配XX页面某一个按钮图片
识别速度 0.156秒
1.3.3 图例3 匹配XX页面某一个按钮图片
0.0936秒
1.3.4 参考部分调试代码
import cv2
import numpy as np
import os
def get_top_matches(res, top_k=5):
flat = res.flatten()
indices = np.argpartition(flat, -top_k)[-top_k:]
indices = indices[np.argsort(-flat[indices])]
return [
(res.min(), flat[i],
np.unravel_index(res.argmin(), res.shape),
np.unravel_index(i, res.shape))
for i in indices
]
def getImageX(bj_rgb_path, hk_rgb_path, top_k=1):
# 读取图像时添加尺寸检查
bj_rgb = cv2.imread(bj_rgb_path)
if bj_rgb is None:
raise FileNotFoundError(f"无法加载大图: {bj_rgb_path}")
bj_gray = cv2.cvtColor(bj_rgb, cv2.COLOR_BGR2GRAY)
hk_rgb = cv2.imread(hk_rgb_path, 0)
if hk_rgb is None:
raise FileNotFoundError(f"无法加载模板图: {hk_rgb_path}")
res = cv2.matchTemplate(bj_gray, hk_rgb, cv2.TM_CCOEFF_NORMED)
valid_matches = []
matches = get_top_matches(res, top_k=top_k)
for i, (min_val, max_val, min_loc, max_loc) in enumerate(matches):
# 转换为实际图像坐标(考虑模板尺寸)
h, w = hk_rgb.shape
pt1 = (max_loc[1], max_loc[0]) # OpenCV的(x,y)格式
pt2 = (pt1[0] + w, pt1[1] + h)
# 边界检查
if pt2[0] > bj_rgb.shape[1] or pt2[1] > bj_rgb.shape[0]:
print(f"警告:匹配点{i + 1}超出图像边界")
continue
print(f"匹配点{i + 1}: 相似度={max_val:.3f}, 位置={pt1}->{pt2}")
valid_matches.append((pt1, pt2))
return valid_matches
def draw_match_boxes(image_path, match_points, save_name="result.jpg"):
img = cv2.imread(image_path)
if img is None:
raise FileNotFoundError(f"无法加载图像: {image_path}")
for i, (pt1, pt2) in enumerate(match_points):
# 确保坐标在图像范围内
pt1 = (max(0, min(pt1[0], img.shape[1] - 1)),
max(0, min(pt1[1], img.shape[0] - 1)))
pt2 = (max(0, min(pt2[0], img.shape[1] - 1)),
max(0, min(pt2[1], img.shape[0] - 1)))
cv2.rectangle(img, pt1, pt2, (0, 0, 255), 2)
cv2.putText(img, str(i + 1), (pt1[0] + 5, pt1[1] + 20), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)
cv2.imwrite(save_name, img)
print(f"结果保存至: {os.path.abspath(save_name)}")
# 使用示例
if __name__ == "__main__":
import time
t1 = time.time()
# 获取匹配点(自动处理边界)
matches = getImageX(bj_rgb_path='1-1.png', hk_rgb_path='1-6.png', top_k=4)
# 绘制结果
if isinstance(matches, list): # 多匹配点模式
draw_match_boxes('1-1.png', matches)
else: # 单匹配点模式
print(f"最佳匹配X坐标: {matches}")
t2 = time.time()
print(t2 - t1)
二、阅读Airtest源码,浅析基于kaze进行图像识别,只筛选出最优区域
2.1 Airtest 测试代码简短分析,方法调用顺序
通过点击运行脚本中的方法,ctrl+鼠标单击,一步一步debug看方法调用顺序推理得出
1、touch(Template(f"{ElementsRepo}/index/查找酒店.png"))
2、Template函数类中的图片识别方法
2.1 实际调研的find_best_result找到最优解的方法
3、find_best_result最优解的方法的具体实现
2.2 find_best_result 方法浅析
cv.py 代码片段
'''
基于kaze进行图像识别,只筛选出最优区域,对应具体方法名称
'''
MATCHING_METHODS = {
"tpl": TemplateMatching,
"mstpl": MultiScaleTemplateMatchingPre,
"gmstpl": MultiScaleTemplateMatching,
"kaze": KAZEMatching,
"brisk": BRISKMatching,
"akaze": AKAZEMatching,
"orb": ORBMatching,
"sift": SIFTMatching,
"surf": SURFMatching,
"brief": BRIEFMatching,
}
class Template(object):
"""
picture as touch/swipe/wait/exists target and extra info for cv match
filename: pic filename
target_pos: ret which pos in the pic
record_pos: pos in screen when recording
resolution: screen resolution when recording
rgb: 识别结果是否使用rgb三通道进行校验.
scale_max: 多尺度模板匹配最大范围.
scale_step: 多尺度模板匹配搜索步长.
"""
@logwrap
def _cv_match(self, screen):
# in case image file not exist in current directory:
ori_image = self._imread()
image = self._resize_image(ori_image, screen, ST.RESIZE_METHOD)
ret = None
for method in ST.CVSTRATEGY:
# get function definition and execute:
func = MATCHING_METHODS.get(method, None)
if func is None:
raise InvalidMatchingMethodError("Undefined method in CVSTRATEGY: '%s', try 'kaze'/'brisk'/'akaze'/'orb'/'surf'/'sift'/'brief' instead." % method)
else:
if method in ["mstpl", "gmstpl"]:
ret = self._try_match(func, ori_image, screen, threshold=self.threshold, rgb=self.rgb, record_pos=self.record_pos,
resolution=self.resolution, scale_max=self.scale_max, scale_step=self.scale_step)
else:
ret = self._try_match(func, image, screen, threshold=self.threshold, rgb=self.rgb)
if ret:
break
return ret
@staticmethod
def _try_match(func, *args, **kwargs):
G.LOGGING.debug("try match with %s" % func.__name__)
try:
ret = func(*args, **kwargs).find_best_result()
except aircv.NoModuleError as err:
G.LOGGING.warning("'surf'/'sift'/'brief' is in opencv-contrib module. You can use 'tpl'/'kaze'/'brisk'/'akaze'/'orb' in CVSTRATEGY, or reinstall opencv with the contrib module.")
return None
except aircv.BaseError as err:
G.LOGGING.debug(repr(err))
return None
else:
return ret
settings.py 代码片段
cv.py 中 ST.CVSTRATEGY 参数,Airtest中图片特征匹配的4个方法先后顺序
CVSTRATEGY = ["mstpl", "tpl", "sift", "brisk"]
template_matching.py 代码片段
@print_run_time
def find_best_result(self):
"""基于kaze进行图像识别,只筛选出最优区域."""
"""函数功能:找到最优结果."""
# 第一步:校验图像输入
check_source_larger_than_search(self.im_source, self.im_search)
# 第二步:计算模板匹配的结果矩阵res
res = self._get_template_result_matrix()
# 第三步:依次获取匹配结果
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
h, w = self.im_search.shape[:2]
# 求取可信度:
confidence = self._get_confidence_from_matrix(max_loc, max_val, w, h)
# 求取识别位置: 目标中心 + 目标区域:
middle_point, rectangle = self._get_target_rectangle(max_loc, w, h)
best_match = generate_result(middle_point, rectangle, confidence)
LOGGING.debug("[%s] threshold=%s, result=%s" % (self.METHOD_NAME, self.threshold, best_match))
return best_match if confidence >= self.threshold else None
2.3 简短小节
从阅读源码中了解到Airtest是基于kaza优先使用"mstpl": MultiScaleTemplateMatchingPre,方法进行图片特征匹配
2.3.1 MultiScaleTemplateMatchingPre 浅析
AKAZE(Accelerated-KAZE)图片匹配结合多尺度模板匹配(MultiScaleTemplateMatchingPre)的优点和缺点可总结如下:
优点
尺度不变性:通过非线性扩散滤波构建图像金字塔,实现多尺度特征检测,适应不同分辨率的目标匹配。
旋转鲁棒性:采用M-LDB描述子,对图像旋转和视角变化具有较强稳定性。
高效性:相比传统模板匹配,AKAZE通过加速非线性扩散优化计算效率。
动态场景适配:支持掩模处理和加权匹配,减少旋转后黑边区域的干扰。
缺点
计算复杂度高:多尺度金字塔构建和特征描述生成需较多计算资源。
光照敏感:与模板匹配类似,对光照变化仍有一定敏感性,需额外预处理。
参数调优依赖:需合理设置尺度层级和旋转角度步长,否则可能影响匹配精度。
该方法综合了特征匹配的鲁棒性和模板匹配的直观性,适合工业检测等动态场景。
三、Airtest中基于kaza图片匹配失败的原因分析,对比OpenCV可视化效果
3.1 Airtest中基于kaza图片匹配失败识别的场景
原因分析说明:手机屏幕截图的分辨率是原图格式大小,需要匹配的图片是不同人操作截图分辨率和原图大小图片区域不一致,从报告中看args参数未匹配到该图片,且未显示相似度
3.1.1 识别 失败案例
3.1.2 识别 成功案例
3.2 使用相同的图片采用OpenCV TM_CCOEFF_NORMED 归一化相关系数匹配法 对比
可以看出OpenCV能匹配,但是小图在大图中的位置偏小,且存在识别错误。相比kaza无识别结果有概率提高成功率。
3.2.1 对比展示效果,红框数字标记
3.3.2 图片相似度结果展示
匹配点1: 相似度=0.522, 位置=(np.int64(983), np.int64(2074))->(np.int64(1032), np.int64(2123))
匹配点2: 相似度=0.522, 位置=(np.int64(983), np.int64(707))->(np.int64(1032), np.int64(756))
匹配点3: 相似度=0.521, 位置=(np.int64(847), np.int64(2070))->(np.int64(896), np.int64(2119))
匹配点4: 相似度=0.521, 位置=(np.int64(848), np.int64(2070))->(np.int64(897), np.int64(2119))
结果保存至: D:\code_path\Python\KillBuy\testPro\result.jpg
0.11297798156738281
四、OpenCV 和 kaza 图片匹配简单对比分析
还未深入研究,其他参考公开资料或AI大语言模型问答吧,合适的场景选择合适的工具,优缺点根据项目需要平衡。