Airtest 技术:Template模版详解
Airtest是一款跨平台UI自动化测试框架,支持Android、iOS、Windows和Web应用。其核心是基于图像识别技术,无需依赖UI元素定位,通过截图匹配实现自动化操作。框架集成了多种图像识别算法(模板匹配、多尺度匹配、特征点匹配等),并支持动态分辨率适配机制,确保跨设备兼容性。实际应用涵盖移动应用测试、游戏自动化、跨设备兼容性测试等场景。最佳实践包括算法策略优化、阈值调优、错误处理机制等
目录
Airtest 简介
Airtest 是网易开源的一款跨平台的 UI 自动化测试框架,支持 Android、iOS、Windows 和 Web 应用。它最大的特点是基于图像识别的自动化,无需依赖 UI 元素定位,通过截图匹配即可实现自动化操作。
核心优势
- 跨平台支持:一套代码可运行在 Android、iOS、Windows 等多个平台
- 图像识别驱动:不依赖 UI 结构,适合游戏、原生应用等场景
- 简单易用:Python API 简洁,学习成本低
- 丰富的算法支持:集成多种图像匹配算法,适应不同场景
适用场景
- 移动应用自动化测试:Android/iOS 应用的 UI 测试
- 游戏自动化:无法通过元素定位的游戏场景
- 跨设备兼容性测试:不同分辨率、不同机型的适配测试
- UI 回归测试:界面变更后的自动化验证
核心原理深度解析
1. 图像识别流程
Airtest 的图像识别遵循以下流程:
截图 → 模板加载 → 分辨率适配 → 多算法匹配 → 结果筛选 → 返回坐标
代码示例:基础模板匹配
from airtest.core.api import *
from airtest.core.cv import Template
# 创建模板对象
template = Template("button.png", threshold=0.8)
# 在屏幕上查找
result = exists(template)
if result:
touch(result) # 点击找到的位置
2. Template 对象详解
Template 是 Airtest 的核心类,封装了图像匹配的所有信息:
class Template:
def __init__(
self,
filename, # 模板图片路径
threshold=None, # 匹配阈值 [0, 1]
target_pos=TargetPos.MID, # 点击位置(左上/中/右下等)
record_pos=None, # 录制时的相对位置
resolution=(), # 录制时的屏幕分辨率
rgb=False, # 是否使用 RGB 三通道校验
scale_max=800, # 多尺度匹配最大范围
scale_step=0.005 # 多尺度匹配步长
):
...
参数说明
- threshold:匹配置信度阈值,范围 0-1。值越高要求越严格,默认 0.7
- target_pos:点击位置,可选
TargetPos.MID(中心)、TargetPos.LEFTTOP(左上)等 - record_pos:录制时的相对位置,用于跨分辨率匹配时缩小搜索范围
- resolution:录制时的屏幕分辨率,用于分辨率适配
- rgb:是否启用 RGB 三通道校验,提高匹配准确性但速度较慢
图像识别算法详解
Airtest 集成了多种图像识别算法,按优先级顺序尝试,直到找到匹配结果或超时。
1. 模板匹配(Template Matching)
算法名称:"tpl"底层实现:OpenCV 的 cv2.matchTemplate()匹配方法:TM_CCOEFF_NORMED(归一化相关系数)
原理
模板匹配通过滑动窗口的方式,在源图像中寻找与模板图像最相似的区域。计算每个位置的相似度,返回置信度最高的位置。
# 内部实现简化版
def template_matching(im_source, im_search, threshold=0.8):
# 转换为灰度图
source_gray = cv2.cvtColor(im_source, cv2.COLOR_BGR2GRAY)
search_gray = cv2.cvtColor(im_search, cv2.COLOR_BGR2GRAY)
# 模板匹配
result = cv2.matchTemplate(source_gray, search_gray, cv2.TM_CCOEFF_NORMED)
# 找到最佳匹配位置
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
# 返回结果(如果置信度 >= threshold)
if max_val >= threshold:
return {
'result': max_loc,
'confidence': max_val,
'rectangle': get_rectangle(max_loc, im_search.shape)
}
return None
特点
- ✅ 速度快:算法简单,执行效率高
- ✅ 一定有结果:即使匹配度很低,也会返回最佳匹配位置
- ❌ 无法跨分辨率:模板和屏幕分辨率必须一致或接近
- ❌ 对光照敏感:光照变化会影响匹配效果
适用场景
- 相同分辨率的设备
- 对速度要求高的场景
- 界面元素相对固定的应用
2. 多尺度模板匹配(Multi-Scale Template Matching)
算法名称:"mstpl" 或 "gmstpl"原理:在多个缩放比例下进行模板匹配
实现逻辑
def multi_scale_matching(im_source, im_search, threshold=0.8):
best_result = None
best_confidence = 0
# 尝试不同的缩放比例
for scale in range(scale_min, scale_max, scale_step):
# 缩放模板图片
scaled_search = cv2.resize(im_search, None, fx=scale, fy=scale)
# 进行模板匹配
result = template_matching(im_source, scaled_search, threshold)
if result and result['confidence'] > best_confidence:
best_result = result
best_confidence = result['confidence']
return best_result
特点
- ✅ 可处理一定程度的缩放差异:适合分辨率略有不同的场景
- ✅ 比纯模板匹配更灵活
- ⚠️ 速度较慢:需要尝试多个缩放比例
3. 特征点匹配(Keypoint Matching)
Airtest 支持多种特征点匹配算法:
|
算法 |
名称 |
特点 |
|
SIFT |
|
精度高,速度慢,需要 opencv-contrib |
|
SURF |
|
精度较高,速度中等,需要 opencv-contrib |
|
KAZE |
|
精度高,速度慢,内存占用大 |
|
BRISK |
|
速度快,精度中等 |
|
AKAZE |
|
KAZE 的加速版本 |
|
ORB |
|
速度最快,精度较低 |
|
BRIEF |
|
需要 opencv-contrib |
原理
特征点匹配通过提取图像的关键特征点(如角点、边缘等),然后匹配这些特征点来识别目标。
# SIFT 特征点匹配示例
def sift_matching(im_source, im_search, threshold=0.8):
# 创建 SIFT 检测器
sift = cv2.SIFT_create()
# 检测关键点和描述符
kp1, des1 = sift.detectAndCompute(im_search, None)
kp2, des2 = sift.detectAndCompute(im_source, None)
# 匹配特征点
bf = cv2.BFMatcher()
matches = bf.knnMatch(des1, des2, k=2)
# 筛选好的匹配点
good_matches = []
for m, n in matches:
if m.distance < 0.75 * n.distance:
good_matches.append(m)
# 如果匹配点足够多,计算位置
if len(good_matches) > threshold:
return calculate_position(kp1, kp2, good_matches)
return None
特点
- ✅ 可跨分辨率识别:不受分辨率限制
- ✅ 对光照、旋转有一定鲁棒性
- ❌ 速度较慢:需要提取和匹配特征点
- ❌ 可能无结果:如果特征点不足,可能匹配失败
性能对比
根据 Airtest 官方 benchmark 测试:
内存占用:kaze > sift > akaze > surf > brief > brisk > orbCPU 占用:kaze > surf > akaze > brisk > sift > brief > orb运行时长:kaze > sift > akaze > surf > brisk > brief > orb识别效果:sift > surf > kaze > akaze > brisk > brief > orb
4. 算法选择策略
Airtest 通过 CVSTRATEGY 设置算法执行顺序:
from airtest.core.settings import Settings as ST
# 默认策略
ST.CVSTRATEGY = ["mstpl", "tpl", "sift", "brisk"]
# 自定义策略:优先使用模板匹配(速度快)
ST.CVSTRATEGY = ["tpl", "brisk"]
# 跨分辨率场景:优先使用特征点匹配
ST.CVSTRATEGY = ["sift", "surf", "tpl"]
执行流程:
- 按
CVSTRATEGY顺序依次尝试 - 找到置信度 >= threshold 的结果即返回
- 所有算法都失败或超时则返回 None
分辨率适配机制
问题背景
在实际项目中,经常需要在不同分辨率的设备上运行自动化脚本。例如:
- 录制模板时:设备 A(1080x1920)
- 运行脚本时:设备 B(1440x2560)
如果直接使用模板匹配,会因为分辨率不匹配导致匹配失败。
解决方案:resolution 参数
Airtest 通过 resolution 参数实现跨分辨率适配。
工作原理
def _resize_image(self, image, screen, resize_method):
"""模板匹配中,将输入的截图适配成等待模板匹配的截图"""
if not self.resolution:
return image # 未设置分辨率,不缩放
screen_resolution = aircv.get_resolution(screen)
# 如果分辨率一致,不需要适配
if tuple(self.resolution) == tuple(screen_resolution):
return image
# 计算缩放比例(使用 cocos_min_strategy)
h, w = image.shape[:2]
w_re, h_re = resize_method(w, h, self.resolution, screen_resolution)
# 缩放模板图片
image = cv2.resize(image, (w_re, h_re))
return image
关键理解
重要:resolution 参数保存的是录制模板时的屏幕分辨率,而不是模板图片本身的像素尺寸。
示例:
- 模板图片:100x50 像素的"一天内"按钮(局部截图)
- 录制时屏幕:1200x2652
- 当前屏幕:1080x2400
计算过程:
# 使用 cocos_min_strategy 计算缩放比例
design_resolution = (960, 640) # 设计分辨率
# 计算录制屏幕的缩放比
scale_sch = min(1200/960, 2652/640) = 1.25
# 计算当前屏幕的缩放比
scale_src = min(1080/960, 2400/640) = 1.125
# 最终缩放比例
scale = scale_src / scale_sch = 0.9
# 缩放模板图片
w_new = int(100 * 0.9) = 90
h_new = int(50 * 0.9) = 45
实际应用
动态获取分辨率
from airtest.core.api import *
# 获取当前设备分辨率
width, height = G.DEVICE.get_current_resolution()
# 创建模板时使用动态分辨率
template = Template(
"button.png",
resolution=(width, height), # 动态获取
threshold=0.75
)
完整示例
def setup_template_with_resolution(template_path):
"""设置模板并自动适配分辨率"""
width, height = G.DEVICE.get_current_resolution()
template = Template(
template_path,
resolution=(width, height),
threshold=0.75
)
return template
# 使用
button_template = setup_template_with_resolution("tpl_button.png")
if exists(button_template):
touch(button_template)
record_pos 参数的作用
record_pos 用于缩小搜索范围,提高匹配速度和准确性。
template = Template(
"button.png",
record_pos=(0.372, -0.819), # 相对屏幕中心的位置
resolution=(1200, 2652),
threshold=0.75
)
工作原理:
- 根据
record_pos和resolution预测目标位置 - 在预测位置周围裁剪搜索区域
- 只在裁剪区域内进行匹配
适用场景:
- 已知元素大致位置
- 需要提高匹配速度
- 固定设备型号
不适用场景:
- 不同机型(位置可能不同)
- 需要通用性强的脚本
实际应用场景
场景 1:移动应用 UI 自动化
需求:自动化小红书搜索功能
from airtest.core.api import *
import random
def xhs_search_automation():
"""小红书搜索自动化"""
# 启动应用
start_app("com.xingin.xhs")
sleep(3)
# 获取设备分辨率
width, height = G.DEVICE.get_current_resolution()
# 打开搜索页面
shell("am start -a android.intent.action.VIEW -d 'xhsdiscover://search?keyword=测试'")
sleep(6)
# 点击筛选按钮
filter_template = Template(
"tpl_filter.png",
resolution=(width, height),
threshold=0.8
)
if exists(filter_template):
touch(filter_template)
sleep(1)
# 选择排序方式
order_template = Template(
"tpl_latest.png",
resolution=(width, height),
threshold=0.75
)
if exists(order_template):
touch(order_template)
sleep(1)
# 选择时间范围
time_template = Template(
"tpl_day.png",
resolution=(width, height),
threshold=0.75
)
if exists(time_template):
touch(time_template)
sleep(1)
# 收起筛选面板
fold_template = Template(
"tpl_fold.png",
resolution=(width, height),
threshold=0.75
)
if exists(fold_template):
touch(fold_template)
优化:模板缓存机制
def cache_template(key, template_pos):
"""缓存模板匹配位置"""
template_path = f"/path/to/cache/{device_id}_{key}.txt"
with open(template_path, "w") as f:
f.write(f"{template_pos[0]} {template_pos[1]}")
def read_template(key):
"""读取缓存的模板位置"""
template_path = f"/path/to/cache/{device_id}_{key}.txt"
if os.path.exists(template_path):
with open(template_path, "r") as f:
return [int(x) for x in f.read().split(" ")]
return None
# 使用缓存
def smart_touch(template, key):
"""智能点击:优先使用缓存,失败则重新匹配"""
# 尝试读取缓存
cached_pos = read_template(key)
if cached_pos:
try:
touch(cached_pos)
return True
except:
pass
# 缓存失效,重新匹配
result = exists(template)
if result:
touch(result)
cache_template(key, result) # 保存缓存
return True
return False
场景 2:游戏自动化
游戏 UI 通常无法通过元素定位,图像识别是唯一选择。
def game_automation():
"""游戏自动化示例"""
# 等待游戏加载
wait(Template("game_logo.png"), timeout=30)
# 点击开始按钮
touch(Template("start_button.png", threshold=0.8))
sleep(2)
# 循环执行游戏操作
while True:
# 检查是否出现特定界面
if exists(Template("level_complete.png")):
touch(Template("next_level.png"))
sleep(1)
# 执行游戏操作
swipe((500, 800), (500, 400), duration=0.5)
sleep(1)
# 检查是否失败
if exists(Template("game_over.png")):
touch(Template("restart.png"))
sleep(2)
场景 3:跨设备兼容性测试
def cross_device_test():
"""跨设备兼容性测试"""
devices = [
"Android:///device1", # 1080x1920
"Android:///device2", # 1440x2560
"Android:///device3", # 720x1280
]
for device in devices:
connect_device(device)
width, height = G.DEVICE.get_current_resolution()
# 使用动态分辨率
template = Template(
"button.png",
resolution=(width, height),
threshold=0.7 # 跨设备时适当降低阈值
)
assert exists(template), f"设备 {device} 匹配失败"
常见问题与解决方案
问题 1:模板匹配置信度偏低
现象
[DEBUG] [Template] threshold=0.75, result={'confidence': 0.68}
匹配失败:置信度 0.68 < 阈值 0.75
原因分析
- 分辨率不匹配:模板和屏幕分辨率差异较大
- UI 变化:界面元素发生改变
- 光照/截图质量:截图质量差或光照变化
- 阈值设置过高:阈值设置不合理
解决方案
方案 1:降低阈值(快速修复,但容易)
# 从 0.75 降低到 0.65
template = Template(
"button.png",
threshold=0.65 # 根据实际置信度调整
)
方案 2:添加分辨率适配
width, height = G.DEVICE.get_current_resolution()
template = Template(
"button.png",
resolution=(width, height), # 添加分辨率适配
threshold=0.75
)
方案 3:使用备用模板
# 准备多个模板
templates = [
Template("button_v1.png", threshold=0.75),
Template("button_v2.png", threshold=0.75),
Template("button_v3.png", threshold=0.70),
]
# 依次尝试
for template in templates:
result = exists(template)
if result:
touch(result)
break
方案 4:使用特征点匹配
from airtest.core.settings import Settings as ST
# 优先使用特征点匹配(跨分辨率能力强)
ST.CVSTRATEGY = ["sift", "surf", "tpl"]
template = Template("button.png", threshold=0.7)
问题 2:匹配速度慢
现象
模板匹配耗时过长,影响测试效率。
解决方案
方案 1:使用 record_pos 缩小搜索范围
template = Template(
"button.png",
record_pos=(0.372, -0.819), # 已知大致位置
resolution=(1200, 2652),
threshold=0.75
)
方案 2:优化算法顺序
from airtest.core.settings import Settings as ST
# 优先使用快速的算法
ST.CVSTRATEGY = ["tpl", "brisk"] # 移除慢速算法
方案 3:使用模板缓存
# 第一次匹配后缓存位置
template_cache = {}
def cached_exists(template, key):
if key in template_cache:
# 使用缓存位置附近的小范围搜索
cached_pos = template_cache[key]
# 在缓存位置周围搜索
...
else:
result = exists(template)
if result:
template_cache[key] = result
return result
问题 3:跨分辨率匹配失败
现象
在不同分辨率的设备上,相同的模板无法匹配。
解决方案
必须使用 resolution 参数
# ❌ 错误:未设置 resolution
template = Template("button.png", threshold=0.75)
# ✅ 正确:设置 resolution
width, height = G.DEVICE.get_current_resolution()
template = Template(
"button.png",
resolution=(width, height), # 关键!
threshold=0.75
)
注意:resolution 应该是录制模板时的屏幕分辨率,而不是模板图片的像素尺寸。
问题 4:动态内容无法匹配
现象
界面包含动态内容(如时间、用户名),导致模板无法匹配。
解决方案
方案 1:使用 ROI(感兴趣区域)
# 只截取按钮部分,排除动态内容
# 在录制模板时,只截取按钮区域,不包含周围的动态文本
方案 2:使用 OCR + 图像识别混合
from airtest.core.api import *
import pytesseract
def smart_find_button():
# 先尝试图像匹配
template = Template("button.png")
if exists(template):
return template
# 图像匹配失败,使用 OCR
screen = snapshot()
text = pytesseract.image_to_string(screen)
if "确定" in text:
# 通过 OCR 找到文本位置,计算按钮位置
...
方案 3:使用更宽松的阈值
# 对于包含动态内容的区域,降低阈值
template = Template(
"button_with_text.png",
threshold=0.6 # 降低阈值,容忍部分差异
)
问题 5:误匹配(匹配到错误位置)
现象
模板匹配到了相似但不正确的元素。
解决方案
方案 1:提高阈值
template = Template(
"button.png",
threshold=0.9 # 提高阈值,要求更精确的匹配
)
方案 2:使用 RGB 三通道校验
template = Template(
"button.png",
rgb=True, # 启用 RGB 校验,提高准确性
threshold=0.75
)
方案 3:结合位置验证
def safe_touch(template, expected_region):
"""安全点击:验证位置是否在预期区域"""
result = exists(template)
if result:
x, y = result
# 验证位置是否在预期区域
if (expected_region[0] <= x <= expected_region[2] and
expected_region[1] <= y <= expected_region[3]):
touch(result)
return True
return False
问题 6:特征点匹配失败(返回 None)
现象
使用特征点匹配算法时,经常返回 None。
原因
特征点不足或特征不明显。
解决方案
方案 1:使用模板匹配作为备选
from airtest.core.settings import Settings as ST
# 特征点匹配失败后,使用模板匹配
ST.CVSTRATEGY = ["sift", "surf", "tpl"] # tpl 作为最后备选
方案 2:优化模板图片
- 选择特征明显的区域
- 避免纯色、渐变等特征少的区域
- 包含文字、图标等特征丰富的元素
方案 3:降低特征点匹配的阈值要求
# 特征点匹配的阈值通过匹配点数量控制
# 无法直接设置,但可以通过算法选择来优化
ST.CVSTRATEGY = ["brisk", "orb"] # 使用更宽松的特征点算法
问题 7:多设备适配困难
现象
需要在多种不同分辨率的设备上运行,维护成本高。
解决方案
统一使用动态分辨率
def create_adaptive_template(template_path, threshold=0.75):
"""创建自适应模板"""
width, height = G.DEVICE.get_current_resolution()
return Template(
template_path,
resolution=(width, height),
threshold=threshold
)
# 使用
button = create_adaptive_template("button.png")
if exists(button):
touch(button)
建立模板库
# 为不同 UI 版本准备多个模板
TEMPLATE_LIB = {
"button": [
"button_v1.png", # 版本 1
"button_v2.png", # 版本 2
"button_v3.png", # 版本 3
]
}
def find_template(template_key):
"""智能查找模板"""
width, height = G.DEVICE.get_current_resolution()
for template_path in TEMPLATE_LIB[template_key]:
template = Template(
template_path,
resolution=(width, height),
threshold=0.7
)
result = exists(template)
if result:
return result
return None
性能优化与最佳实践
1. 算法选择策略
场景 1:相同分辨率,追求速度
ST.CVSTRATEGY = ["tpl"] # 只使用模板匹配
场景 2:跨分辨率,追求准确性
ST.CVSTRATEGY = ["sift", "surf", "tpl"] # 优先特征点匹配
场景 3:平衡速度和准确性
ST.CVSTRATEGY = ["mstpl", "tpl", "brisk"] # 多尺度 + 模板 + 快速特征点
2. 阈值设置建议
|
场景 |
推荐阈值 |
说明 |
|
相同分辨率 |
0.75 - 0.85 |
可以设置较高阈值 |
|
跨分辨率 |
0.65 - 0.75 |
需要适当降低 |
|
动态内容 |
0.60 - 0.70 |
容忍部分差异 |
|
关键操作 |
0.80 - 0.90 |
防止误操作 |
3. 模板图片优化
最佳实践
- 选择特征丰富的区域
-
- ✅ 包含文字、图标
- ✅ 有明确的边界
- ❌ 避免纯色背景
- ❌ 避免渐变区域
- 合适的尺寸
-
- 太小:特征不足,容易误匹配
- 太大:匹配速度慢,容易受动态内容影响
- 推荐:50x50 到 200x200 像素
- 排除动态内容
-
- 只截取静态 UI 元素
- 避免包含时间、用户名等动态文本
4. 错误处理与重试机制
def robust_touch(template, max_retries=3, timeout=10):
"""健壮的点击操作,带重试机制"""
for i in range(max_retries):
try:
result = exists(template, timeout=timeout)
if result:
touch(result)
return True
except Exception as e:
print(f"第 {i+1} 次尝试失败: {e}")
sleep(1)
# 所有重试都失败,使用备用方案
return fallback_touch(template)
def fallback_touch(template):
"""备用点击方案:使用坐标缓存或 OCR"""
# 尝试读取缓存的坐标
cached_pos = read_cached_position(template.filename)
if cached_pos:
touch(cached_pos)
return True
# 使用 OCR 或其他方法
...
return False
5. 日志与调试
from airtest.core.api import *
from airtest.utils.logger import get_logger
logger = get_logger(__name__)
def debug_template_matching(template):
"""调试模板匹配"""
result = exists(template)
if result:
logger.info(f"匹配成功: {result}, 置信度: {result.get('confidence', 'N/A')}")
# 保存匹配结果截图
snapshot(filename=f"match_success_{int(time.time())}.png")
else:
logger.warning(f"匹配失败: {template.filename}")
# 保存失败截图用于分析
snapshot(filename=f"match_fail_{int(time.time())}.png")
return result
6. 性能监控
import time
def performance_monitor(func):
"""性能监控装饰器"""
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start_time
logger.info(f"{func.__name__} 耗时: {elapsed:.2f}秒")
return result
return wrapper
@performance_monitor
def find_and_touch(template):
result = exists(template)
if result:
touch(result)
return result
要点总结
核心要点总结
- 算法选择:根据场景选择合适的算法组合
-
- 同分辨率 → 模板匹配(快)
- 跨分辨率 → 特征点匹配(准)
- 分辨率适配:必须使用
resolution参数实现跨设备兼容 - 阈值调优:根据实际匹配度动态调整阈值
- 错误处理:实现重试机制和备用方案
- 性能优化:使用缓存、缩小搜索范围等方法提升速度
常见陷阱
- ❌ 忘记设置 resolution:导致跨分辨率匹配失败
- ❌ 阈值设置过高:导致匹配失败
- ❌ 模板包含动态内容:导致匹配不稳定
- ❌ 只使用一种算法:没有充分利用 Airtest 的多算法优势
最佳实践清单
- 使用动态分辨率适配
- 准备多个备用模板
- 实现重试和错误处理
- 优化算法选择策略
- 合理设置阈值
- 优化模板图片质量
- 添加日志和性能监控
- 建立模板库管理机制
参考资料
作者:基于实际项目经验总结日期:2025年版本:v1.0
更多推荐


所有评论(0)