目录

  1. Airtest 简介
  2. 核心原理深度解析
  3. 图像识别算法详解
  4. 分辨率适配机制
  5. 实际应用场景
  6. 常见问题与解决方案
  7. 性能优化与最佳实践
  8. 总结与展望

Airtest 简介

Airtest 是网易开源的一款跨平台的 UI 自动化测试框架,支持 Android、iOS、Windows 和 Web 应用。它最大的特点是基于图像识别的自动化,无需依赖 UI 元素定位,通过截图匹配即可实现自动化操作。

核心优势

  1. 跨平台支持:一套代码可运行在 Android、iOS、Windows 等多个平台
  2. 图像识别驱动:不依赖 UI 结构,适合游戏、原生应用等场景
  3. 简单易用:Python API 简洁,学习成本低
  4. 丰富的算法支持:集成多种图像匹配算法,适应不同场景

适用场景

  • 移动应用自动化测试: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

"sift"

精度高,速度慢,需要 opencv-contrib

SURF

"surf"

精度较高,速度中等,需要 opencv-contrib

KAZE

"kaze"

精度高,速度慢,内存占用大

BRISK

"brisk"

速度快,精度中等

AKAZE

"akaze"

KAZE 的加速版本

ORB

"orb"

速度最快,精度较低

BRIEF

"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"]

执行流程

  1. CVSTRATEGY 顺序依次尝试
  2. 找到置信度 >= threshold 的结果即返回
  3. 所有算法都失败或超时则返回 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
)

工作原理

  1. 根据 record_posresolution 预测目标位置
  2. 在预测位置周围裁剪搜索区域
  3. 只在裁剪区域内进行匹配

适用场景

  • 已知元素大致位置
  • 需要提高匹配速度
  • 固定设备型号

不适用场景

  • 不同机型(位置可能不同)
  • 需要通用性强的脚本

实际应用场景

场景 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
原因分析
  1. 分辨率不匹配:模板和屏幕分辨率差异较大
  2. UI 变化:界面元素发生改变
  3. 光照/截图质量:截图质量差或光照变化
  4. 阈值设置过高:阈值设置不合理
解决方案

方案 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. 模板图片优化

最佳实践
  1. 选择特征丰富的区域
    • ✅ 包含文字、图标
    • ✅ 有明确的边界
    • ❌ 避免纯色背景
    • ❌ 避免渐变区域
  1. 合适的尺寸
    • 太小:特征不足,容易误匹配
    • 太大:匹配速度慢,容易受动态内容影响
    • 推荐:50x50 到 200x200 像素
  1. 排除动态内容
    • 只截取静态 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

要点总结

核心要点总结

  1. 算法选择:根据场景选择合适的算法组合
    • 同分辨率 → 模板匹配(快)
    • 跨分辨率 → 特征点匹配(准)
  1. 分辨率适配:必须使用 resolution 参数实现跨设备兼容
  2. 阈值调优:根据实际匹配度动态调整阈值
  3. 错误处理:实现重试机制和备用方案
  4. 性能优化:使用缓存、缩小搜索范围等方法提升速度

常见陷阱

  1. 忘记设置 resolution:导致跨分辨率匹配失败
  2. 阈值设置过高:导致匹配失败
  3. 模板包含动态内容:导致匹配不稳定
  4. 只使用一种算法:没有充分利用 Airtest 的多算法优势

最佳实践清单

  • 使用动态分辨率适配
  • 准备多个备用模板
  • 实现重试和错误处理
  • 优化算法选择策略
  • 合理设置阈值
  • 优化模板图片质量
  • 添加日志和性能监控
  • 建立模板库管理机制


参考资料


作者:基于实际项目经验总结日期:2025年版本:v1.0

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐