Appium images-plugin find-by-image 详解:"-image" 图像模板定位策略与图像元素操作实战
【免费下载链接】appiumCross-platform automation framework for all kinds of apps, built on top of the W3C WebDriver protocol项目地址: https://gitcode.com/GitHub_Trending/ap/appium
本文基于 Appium 仓库中的 find-by-image 官方文档 与@appium/images-plugin的源码实现,系统讲解-image定位策略的工作原理、Image Element 支持的操作边界、全部相关 Settings 参数及其源码级匹配流程,帮助你在无法依赖原生 UI 层级(如 Canvas 渲染、自绘界面、跨平台像素级校验)的场景下,用模板图像精确找到并操作屏幕区域。
1. 启用 images 插件
@appium/images-plugin是 Appium 3 插件架构下的官方插件,提供两大能力(见 README):
- Image Comparison:新增 Appium 端点,支持多图比较(对应文档 image-comparison.md,非本篇重点);
- Finding Elements by Image:用一张模板图像找到屏幕上与之视觉匹配的区域,并以标准 Appium 元素命令对其进行交互——即本文主题。
安装与启动方式:
appium plugin install images appium --use-plugins=images从 package.json 可以看到该插件的注册信息与运行约束:
"appium": { "pluginName": "images", "mainClass": "ImageElementPlugin" }- 插件注册名为
images,核心类为ImageElementPlugin(lib/plugin.ts); - 依赖
appium@^3.0.0-beta.0(peerDependency),即需要 Appium 3 的插件体系; - 图像匹配核心依赖
@appium/opencv,尺寸校正与缩放依赖sharp,图像元素缓存依赖lru-cache。
2.-image定位策略:selector 就是一段 base64
使用该插件支持的-image定位策略时,你可以向 Appium 发送一张代表目标元素的图像文件。如果 Appium 在截图中找到了与模板匹配的屏幕区域,它会把该区域信息封装成标准WebElement返回给你的 Appium 客户端。各客户端的 API 形式不同,例如:driver.findElementByImage()。
关键约定:
- 策略(strategy):
-image(源码常量IMAGE_STRATEGY = '-image',见 constants.ts); - selector:必须是模板图像的 base64 编码字符串。
对应的 HTTP 端点是标准的 WebDriver 查找端点,只是using参数多了一个取值:
POST /session/:sessionId/element POST /session/:sessionId/elements端点修改说明可参考 plugins API 参考。
从源码看拦截逻辑非常清晰,ImageElementPlugin重写了findElement/findElements,在_find中只有当策略恰好是-image时才介入,否则直接放行给后续中间件:
const [strategy, selector] = args; // if we're not actually finding by image, just do the normal thing if (strategy !== IMAGE_STRATEGY) { return await next(); } return await this.finder.findByImage(Buffer.from(selector, 'base64'), driver, {multiple});即 selector 被按 base64 解码为Buffer后交给ImageElementFinder完成模板匹配(multiple为true时支持查找屏幕上所有匹配区域)。
3. Image Element 的真相:一组坐标 + 受限的元素操作
匹配成功后,Appium 会缓存匹配信息并返回标准元素响应,在你的测试脚本中实例化为一个标准元素对象。元素 ID 带有appium-image-element-前缀加 UUID(constants.ts 中IMAGE_ELEMENT_PREFIX),客户端拿到的仍是"合法"的 WebElement。
3.1 支持的操作
文档与源码ImageElement.execute的 switch 分支一一对应,Image Element 仅支持以下方法:
| 方法 | 行为(源码依据) |
|---|---|
click | 在元素屏幕包围盒中心构造一次点击(含过期检查,见第 5 节) |
isDisplayed/elementDisplayed | 直接返回true(既然刚匹配成功,就认为可见) |
getSize | 返回{width, height}(匹配矩形宽高) |
getLocation/getLocationInView | 返回左上角坐标{x, y} |
getElementRect | 返回完整Rect(x、y、width、height) |
getAttribute | visual:当getMatchedImageResult为true时返回带匹配标记图像的 base64 数据;score:返回[0.0, 1.0]区间的相似度分数 |
findElementFromElement/findElementsFromElement | 以该图像元素区域为容器,在其中继续用-image查找子元素(见 3.3) |
调用其他属性会抛出NotYetImplementedError。
3.2 为什么只支持这些操作
这些操作之所以被支持,是因为它们只依赖屏幕坐标即可工作。而sendKeys之类的操作之所以不支持,是因为基于模板图像,Appium 唯一能确定的是"屏幕上是否存在一个视觉上与模板匹配的区域"——它没有能力把这一信息转换成驱动特定的 UI 元素对象,也就无法执行依赖原生元素的操作。
这里要特别强调文档给出的核心认知:Image Element 没有任何"魔法"——它引用的只是屏幕坐标。所谓"点击一个 Image Element",内部不过是 Appium 在该元素屏幕包围盒的中心点构造一次点击(而且你可以指定使用哪个 API 执行这次点击,见 5.2)。
3.3 两个源码级的加分细节
- performActions 支持以图像元素为 origin。
ImageElementPlugin.performActions会扫描 W3C action sequence:当pointerMove/scroll的origin是图像元素时,把元素中心坐标累加到x/y偏移上,然后删除origin属性,交给底层驱动按纯坐标处理:
const elId = util.unwrapElement(actionWithEl.origin as Element); if (!elId?.startsWith(IMAGE_ELEMENT_PREFIX)) { continue; } const imgEl = this.finder.getImageElement(elId); // Add the element's center to the offset. actionWithEl.x = (actionWithEl.x ?? 0) + imgEl.center.x; actionWithEl.y = (actionWithEl.y ?? 0) + imgEl.center.y; delete actionWithEl.origin;- 元素缓存与生命周期。
ImageElementFinder用 LRU 缓存保存已匹配的 Image Element,上限 100 条、TTL 24 小时(MAX_CACHE_ITEMS/MAX_CACHE_AGE_MS);会话deleteSession时由插件统一清空缓存(plugin.ts)。对缓存中不存在的 ID 发起命令会抛出NoSuchElementError。
4. 匹配全流程:findByImage源码拆解
ImageElementFinder.findByImage是整条链路的入口,完整流程如下(每一步都有对应设置项控制,见第 6 节):
读取设置:
{...DEFAULT_SETTINGS, ...driver.settings.getSettings()},取imageMatchThreshold、imageMatchMethod、fixImageTemplateSize、fixImageTemplateScale、defaultImageTemplateScale、getMatchedImageResult等;获取屏幕尺寸:优先调用驱动的
getWindowRect(),兼容已弃用的getWindowSize()。驱动若两者都不支持会直接抛错——因为"最终决定在哪里点击的正是屏幕尺寸";模板尺寸修正(
fixImageTemplateSize为true时):ensureTemplateSize用sharp读取模板元数据,若模板比屏幕(或容器矩形)大,按fit: 'inside'缩放。这一步存在的原因是 OpenCV 不允许模板大于底图;获取并校正截图(
fixImageFindScreenshotDims控制,默认true):getScreenshotForImageFind通过驱动的getScreenshot()拿截图,若其尺寸与屏幕尺寸不一致,会分别处理两类偏差:- 宽高比不一致:比较
screenAR与shotAR(以FLOAT_PRECISION = 100000的精度舍入比较),不一致时取min(xScale, yScale)作为缩放因子重定尺寸——避免拉伸变形,并记录 scale 用于后续坐标换算; - 宽高都不同:按屏幕尺寸
fit: 'fill'缩放。
关闭该设置可跳过检查以提速,但可能影响匹配坐标的准确性。
- 宽高比不一致:比较
模板缩放修正(
fixImageTemplateScale):处理"底图被缩放到窗口尺寸后再匹配"的场景(如 iOS 截图 750×1334、窗口 375×667,缩放因子 0.5);也可用defaultImageTemplateScale还原用户存储的缩放模板。模板匹配:调用
compareImages的matchTemplate模式,底层是@appium/opencv的getImageOccurrence(默认 TM_CCOEFF_NORMED 方法),传入threshold、visualize、multiple以及可选的method;隐式等待重试:匹配包裹在
driver.implicitWaitForCondition(performLookup)中。若compareImages报"Cannot find any occurrences",视为"尚未找到"并继续按隐式等待时间重试;等待超时后findElement抛NoSuchElementError,findElements返回空数组;生成并注册 Image Element:把每个结果(
rect、score、visualization)构造为ImageElement(visualization即"带匹配标记的图像",仅在getMatchedImageResult开启时非空),再写入 LRU 缓存并返回包装后的元素 ID。
其中score是[0.0, 1.0]的浮点相似度分数,1.0 表示完全一致(types.ts 中ImageElementOpts的注释),与imageMatchThreshold比较决定匹配成败。
5. 点击行为详解:过期检查与点击策略
文档强调"你可以告诉 Appium 用哪个 API 执行点击"。ImageElement.click的实现完整呈现了这三个设置如何协同:
5.1 过期检查(checkForImageElementStaleness/autoUpdateImageElementPosition)
点击前若任一设置为true,会先用同一模板立即重新匹配一次:
- 重匹配失败 → 抛出
StaleElementReferenceError; - 重匹配位置与原位置不同(
equals比较 rect 四个字段)时:autoUpdateImageElementPosition为true→ 日志提示 "Click will proceed at new coordinates",并更新this.rect;- 否则按原始坐标点击,并提示如需自动更新请设置
autoUpdateImageElementPosition。
注意重匹配时内部会传ignoreDefaultImageTemplateScale: true,因为此时的模板已经是"基于设备截图管理"的图像,不应再按用户存储模板的缩放比例缩放(见 image-element.ts)。
5.2 点击策略(imageElementTapStrategy)
取值仅两种:"w3cActions"(默认)或"touchActions",非法值直接抛错。
- W3C Actions 路径:在元素中心
(x, y)构造标准 pointer 动作序列——pointerMove(duration 0) →pointerDown(button 0) →pause(125ms,常量TAP_DURATION_MS) →pointerUp,然后调用驱动的performActions;若驱动未实现performActions,会告警并降级到 TouchActions; - MJSONWP TouchActions 路径:构造
{action: 'tap', options: {x, y}}调用performTouch;若驱动连performTouch都没实现,则抛出明确要求驱动同时支持两种命令的错误。
6. 相关设置(Settings)完整说明
图像查找依赖"图像分析软件 + Appium 截图能力 + 你提供的参考图像"三者结合,因此插件提供一组设置来调节匹配行为——有时能加速匹配,有时能提高准确性。
这些设置通过 Appium Settings API 访问,也可以作为特殊能力settings[]在建会话时预置。完整参数表(与 constants.ts 的DEFAULT_SETTINGS逐项对应):
| 设置名 | 说明 | 取值范围 | 默认值 |
|---|---|---|---|
imageMatchThreshold | OpenCV 匹配阈值:低于该值即认为查找失败。0 表示不用阈值,1 表示参考图像必须像素级完全一致。中间值没有绝对含义,例如需要大幅缩放参考图的匹配得分会更低。建议先用默认值,找不到元素时逐步调低;匹配到错误元素时调高 | 0 到 1 之间的数字 | 0.4 |
fixImageFindScreenshotDims | Appium 知道屏幕尺寸,而屏幕尺寸最终决定点击坐标。若截图(无论来自原生方法还是外部来源)与屏幕尺寸不一致,开启该设置会让 Appium 调整截图尺寸以对齐,确保匹配元素位于正确坐标。若你确定不需要,可关闭以略提速 | true/false | true |
fixImageTemplateSize | OpenCV 不允许参考图(模板)大于底图。若你发送的模板尺寸大于 Appium 截到的截图,匹配会自动失败。设为true后 Appium 会把模板缩放到小于截图尺寸,避免匹配直接失败 | true/false | false |
fixImageTemplateScale | Appium 在匹配前会把底图缩放到窗口尺寸。若截图是 750×1334 而窗口是 375×667(缩放 0.5),而你的参考图是按截图尺寸裁剪的,则永远匹配不上。设为true后 Appium 会按相同比例缩放你的参考图 | true/false | false |
defaultImageTemplateScale | 默认 Appium 不缩放模板图像(1.0)。但存储缩放后的模板可以节省存储:例如用 270×32 的模板表示 1080×126 的区域,则把该设置设为4.0,服务端会先把模板放大回原始比例再比较 | 如0.5、10.0、100 | 1.0 |
checkForImageElementStaleness | 从匹配成功到实际点击之间,元素可能已经不在原位。Appium 唯一能判断的方法是点击前立即重新匹配;重匹配失败会抛出StaleElementException。设为false可跳过检查、略提速,但可能遭遇"陈旧元素"问题且没有异常提示 | true/false | true |
autoUpdateImageElementPosition | 已匹配的图像在被点击前可能移动了位置。与上一设置类似,若重匹配发现位置变化,Appium 可自动按新位置点击 | true/false | false |
imageElementTapStrategy | 点击已找到的图像元素时使用的触摸 API:W3C Actions 或旧版 MJSONWP TouchActions。除非你的驱动因某些原因不支持 W3C Actions,否则保持默认即可 | "w3cActions"/"touchActions" | "w3cActions" |
getMatchedImageResult | 默认 Appium 不保存匹配图像结果。将其存入内存有助于调试"到底匹配上了哪块区域"。开启后,元素对attributeAPI 的visual请求会返回匹配区域的图像 | true/false | false |
补充一点源码信息:设置接口 ImageSettings 中还存在imageMatchMethod(默认空字符串,即使用 OpenCV 默认的 TM_CCOEFF_NORMED),用于指定模板匹配算法,文档表格未列出,但findByImage在设置非空时会把它透传给比较选项(见 finder.ts)。
注意:各语言客户端可能通过各自的常量暴露这些设置,常量名与上述设置名可能略有差异。
7. 调试:用visual属性查看匹配结果
getMatchedImageResult设置是排查"Appium 是否按预期找到了图像"的利器。开启后,匹配成功的元素会带有visual属性,可通过getAttribute取得带匹配标记图像的 base64 数据(image-element.ts 中getAttribute对visual返回imgEl.matchedImage,对score返回相似度分数):
# Ruby core @driver.update_settings({ getMatchedImageResult: true }) el = @driver.find_element_by_image 'path/to/img.png' img_el.visual # returns base64 encoded string# Python self.driver.update_settings({"getMatchedImageResult": True}) el = self.driver.find_element_by_image('path/to/img.png') el.get_attribute('visual') # returns base64 encoded string调试时建议配合score属性一起看:分数明显低于预期阈值附近时,多半是模板裁剪范围、DPI 缩放(见fixImageTemplateScale/defaultImageTemplateScale)或阈值设置的问题。
8. 端到端测试如何验证这条链路
插件自带的 E2E 测试 plugin.e2e.spec.ts 使用 fake-driver 启动真实 Appium 服务(--use-plugins=images),验证了本文涉及的核心能力,可直接作为调用范例:
// 用本地图片路径作为模板定位(客户端 SDK 负责转 base64) const imageEl = await driver.$(APPSTORE_IMG_PATH); const {x, y} = await imageEl.getLocation(); // 断言 x=28, y=72 const {width, height} = await imageEl.getSize(); // 断言 80×91 await imageEl.click(); // performActions 中以图像元素为 origin const actionSequence = { type: 'pointer', id: 'mouse', parameters: {pointerType: 'touch'}, actions: [ {type: 'pointerMove', x: 0, y: 0, duration: 0, origin: imageEl}, {type: 'pointerDown', button: 0}, {type: 'pause', duration: 125}, {type: 'pointerUp', button: 0}, ], }; await driver.performActions([actionSequence]);测试还覆盖了子元素查找:对图像元素调用saveScreenshot保存其区域截图(即getElementScreenshot,返回模板原图的 base64),用sharp裁剪出中间 1/2 区域作为新模板,再通过imageEl.$(tmpImgPath)在该元素范围内继续-image查找——这正是 3.3 节中findElementFromElement+containerRect容器过滤(containsRect)的用武之地。测试用的模板图像为 appstore.png。
9. 小结
-image策略 + base64 selector 是全部调用约定;找到的"元素"本质是坐标,只支持位置类操作(click、isDisplayed、getSize/getLocation/getElementRect、visual/score属性);- 默认设置已经比较稳健(阈值 0.4、截图尺寸校正开启、过期检查开启、W3C 点击);找不到元素先调低
imageMatchThreshold,点错元素先调高它; - 高分屏/DPI 缩放导致的匹配失败,优先考虑
fixImageTemplateScale与defaultImageTemplateScale; - 调试匹配区域用
getMatchedImageResult+visual属性; - 所有行为的最终实现均可在 packages/images-plugin/lib 中核对:
plugin.ts(端点拦截)、finder.ts(匹配流程)、image-element.ts(元素操作与点击)、constants.ts(默认值)。
【免费下载链接】appiumCross-platform automation framework for all kinds of apps, built on top of the W3C WebDriver protocol项目地址: https://gitcode.com/GitHub_Trending/ap/appium
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考