Appium images-plugin find-by-image 详解:“-image“ 图像模板定位策略与图像元素操作实战
2026/9/13 3:00:31 网站建设 项目流程

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):

  1. Image Comparison:新增 Appium 端点,支持多图比较(对应文档 image-comparison.md,非本篇重点);
  2. 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完成模板匹配(multipletrue时支持查找屏幕上所有匹配区域)。

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)
getAttributevisual:当getMatchedImageResulttrue时返回带匹配标记图像的 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 两个源码级的加分细节

  1. performActions 支持以图像元素为 originImageElementPlugin.performActions会扫描 W3C action sequence:当pointerMove/scrollorigin是图像元素时,把元素中心坐标累加到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;
  1. 元素缓存与生命周期ImageElementFinder用 LRU 缓存保存已匹配的 Image Element,上限 100 条、TTL 24 小时(MAX_CACHE_ITEMS/MAX_CACHE_AGE_MS);会话deleteSession时由插件统一清空缓存(plugin.ts)。对缓存中不存在的 ID 发起命令会抛出NoSuchElementError

4. 匹配全流程:findByImage源码拆解

ImageElementFinder.findByImage是整条链路的入口,完整流程如下(每一步都有对应设置项控制,见第 6 节):

  1. 读取设置{...DEFAULT_SETTINGS, ...driver.settings.getSettings()},取imageMatchThresholdimageMatchMethodfixImageTemplateSizefixImageTemplateScaledefaultImageTemplateScalegetMatchedImageResult等;

  2. 获取屏幕尺寸:优先调用驱动的getWindowRect(),兼容已弃用的getWindowSize()。驱动若两者都不支持会直接抛错——因为"最终决定在哪里点击的正是屏幕尺寸";

  3. 模板尺寸修正fixImageTemplateSizetrue时):ensureTemplateSizesharp读取模板元数据,若模板比屏幕(或容器矩形)大,按fit: 'inside'缩放。这一步存在的原因是 OpenCV 不允许模板大于底图;

  4. 获取并校正截图fixImageFindScreenshotDims控制,默认true):getScreenshotForImageFind通过驱动的getScreenshot()拿截图,若其尺寸与屏幕尺寸不一致,会分别处理两类偏差:

    • 宽高比不一致:比较screenARshotAR(以FLOAT_PRECISION = 100000的精度舍入比较),不一致时取min(xScale, yScale)作为缩放因子重定尺寸——避免拉伸变形,并记录 scale 用于后续坐标换算;
    • 宽高都不同:按屏幕尺寸fit: 'fill'缩放。

    关闭该设置可跳过检查以提速,但可能影响匹配坐标的准确性。

  5. 模板缩放修正fixImageTemplateScale):处理"底图被缩放到窗口尺寸后再匹配"的场景(如 iOS 截图 750×1334、窗口 375×667,缩放因子 0.5);也可用defaultImageTemplateScale还原用户存储的缩放模板。

  6. 模板匹配:调用compareImagesmatchTemplate模式,底层是@appium/opencvgetImageOccurrence(默认 TM_CCOEFF_NORMED 方法),传入thresholdvisualizemultiple以及可选的method

  7. 隐式等待重试:匹配包裹在driver.implicitWaitForCondition(performLookup)中。若compareImages报"Cannot find any occurrences",视为"尚未找到"并继续按隐式等待时间重试;等待超时后findElementNoSuchElementErrorfindElements返回空数组;

  8. 生成并注册 Image Element:把每个结果(rectscorevisualization)构造为ImageElementvisualization即"带匹配标记的图像",仅在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 四个字段)时:
    • autoUpdateImageElementPositiontrue→ 日志提示 "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逐项对应):

设置名说明取值范围默认值
imageMatchThresholdOpenCV 匹配阈值:低于该值即认为查找失败。0 表示不用阈值,1 表示参考图像必须像素级完全一致。中间值没有绝对含义,例如需要大幅缩放参考图的匹配得分会更低。建议先用默认值,找不到元素时逐步调低;匹配到错误元素时调高0 到 1 之间的数字0.4
fixImageFindScreenshotDimsAppium 知道屏幕尺寸,而屏幕尺寸最终决定点击坐标。若截图(无论来自原生方法还是外部来源)与屏幕尺寸不一致,开启该设置会让 Appium 调整截图尺寸以对齐,确保匹配元素位于正确坐标。若你确定不需要,可关闭以略提速true/falsetrue
fixImageTemplateSizeOpenCV 不允许参考图(模板)大于底图。若你发送的模板尺寸大于 Appium 截到的截图,匹配会自动失败。设为true后 Appium 会把模板缩放到小于截图尺寸,避免匹配直接失败true/falsefalse
fixImageTemplateScaleAppium 在匹配前会把底图缩放到窗口尺寸。若截图是 750×1334 而窗口是 375×667(缩放 0.5),而你的参考图是按截图尺寸裁剪的,则永远匹配不上。设为true后 Appium 会按相同比例缩放你的参考图true/falsefalse
defaultImageTemplateScale默认 Appium 不缩放模板图像(1.0)。但存储缩放后的模板可以节省存储:例如用 270×32 的模板表示 1080×126 的区域,则把该设置设为4.0,服务端会先把模板放大回原始比例再比较0.510.01001.0
checkForImageElementStaleness从匹配成功到实际点击之间,元素可能已经不在原位。Appium 唯一能判断的方法是点击前立即重新匹配;重匹配失败会抛出StaleElementException。设为false可跳过检查、略提速,但可能遭遇"陈旧元素"问题且没有异常提示true/falsetrue
autoUpdateImageElementPosition已匹配的图像在被点击前可能移动了位置。与上一设置类似,若重匹配发现位置变化,Appium 可自动按新位置点击true/falsefalse
imageElementTapStrategy点击已找到的图像元素时使用的触摸 API:W3C Actions 或旧版 MJSONWP TouchActions。除非你的驱动因某些原因不支持 W3C Actions,否则保持默认即可"w3cActions"/"touchActions""w3cActions"
getMatchedImageResult默认 Appium 不保存匹配图像结果。将其存入内存有助于调试"到底匹配上了哪块区域"。开启后,元素对attributeAPI 的visual请求会返回匹配区域的图像true/falsefalse

补充一点源码信息:设置接口 ImageSettings 中还存在imageMatchMethod(默认空字符串,即使用 OpenCV 默认的 TM_CCOEFF_NORMED),用于指定模板匹配算法,文档表格未列出,但findByImage在设置非空时会把它透传给比较选项(见 finder.ts)。

注意:各语言客户端可能通过各自的常量暴露这些设置,常量名与上述设置名可能略有差异。

7. 调试:用visual属性查看匹配结果

getMatchedImageResult设置是排查"Appium 是否按预期找到了图像"的利器。开启后,匹配成功的元素会带有visual属性,可通过getAttribute取得带匹配标记图像的 base64 数据(image-element.ts 中getAttributevisual返回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 缩放导致的匹配失败,优先考虑fixImageTemplateScaledefaultImageTemplateScale
  • 调试匹配区域用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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询