视觉回归:为什么逐像素比对会放过真实变化

视觉回归:为什么逐像素比对会放过真实变化

大多数开源视觉回归工具——BackstopJS、Wraith,或 Playwright 和 Cypress 的截图检查——都基于同一个思路:前后各截一张截图,然后数出有变化的像素。引擎几乎总是同一个:把两张图逐像素比对。

它简单、稳健,非常适合捕捉大的布局崩坏。但这种方法存在一个少有人真正去量化的结构性取舍。我们量化了它——有数据为证——而在专门用于暴露它的用例上,结果一目了然。

不能混淆的两个设置

这种混淆随处可见,我们先说清楚。这类工具有两个不同的设置:

  1. 逐像素容差:颜色相差多少,才算这个像素「真的」变了。它是一个颜色敏感度旋钮,逐像素地应用。
  2. 判定阈值:数完不同的像素后,看图像有百分之几发生了变化,据此决定测试通过还是失败。参考工具 BackstopJS 把它默认设为 0.1% 的像素

我们的测试正是遵循这套逻辑:数出不同的像素(工具使用默认敏感度),然后套用 0.1% 的判定阈值来判定通过或失败。

阈值的两难

这个百分比阈值把整图比对锁进了一个它无法取胜的取舍:

  • 设了百分比阈值(默认设置,0.1%) → 你会放过真实的小变化。一个按钮换色、一条边框变圆、一个单元格状态从「OK」→「ERROR」:这些都只占极小一部分像素。低于 0.1%,测试就转绿,尽管页面已明显改变。一个真实变化被忽略了。
  • 完全不设容差(另一个极端,Playwright 的默认设置) → 你会因最微小的一个像素差异而失败,包括单纯的边缘平滑——也就是字母和形状边缘那些半透明像素,它们在每次渲染之间会略有不同。结果:假警报满天飞,团队最终会无视这个工具。

真正的工具提供护栏——遮罩区域、忽略区域、裁剪。它们有效,但必须事先手动逐区配置。局部敏感度并非自动。

真实条件下的测试

我们在完全相同的页面上(由浏览器渲染、可复现地冻结、屏幕宽度 1280 px、整页捕获)比较了两种方法:

  • Delta-QA:我们的比对器,逐元素比对(它在两个版本之间匹配元素,只在最细的层级比对像素);
  • 整图比对:把两张截图逐像素比对,然后套用 0.1% 的判定阈值。

五个用例,专门用于暴露盲区:

用例 变化 Delta-QA(逐元素) 整图比对(变化像素百分比,在 0.1% 阈值下的判定)
位移 一个三角形在页面内移动 2 个信号:元素上的 消失 + 出现 0.005% → 未检出
重排 卡片与菜单被重排 13 个局部化信号(哪些卡片、哪些条目) 0.63% → 失败,但弥散色块,什么都没说清
细微变化 边框圆角从 12 px 变到 30 px 受影响卡片上 1 个信号 0.011% → 未检出
局部颜色 某卡片头部由绿变紫 正确的头部(强度 0.996)+ 2 个弱的父级信号 5.03% → 失败,但弥散色块,什么都没说清
表格单元格 某行状态 FAIL → WARN 正确行上的 2 个强信号 0.036% → 未检出

在这五个用例中,三个真实变化——元素位移、边框变圆、单元格状态——都落在标准 0.1% 判定阈值之下。一个使用默认配置的工具会判定它们「无变化」。然而这些恰恰是视觉测试本应捕捉的回归。

对于逐像素比对确实「检测到」的两个用例(重排、颜色),它只说一句话:「某处有 X% 的像素变了」。完全不知道是哪个元素,也不知道是什么性质的变化。而 Delta-QA 会指明确切的元素并界定变化类型(移动、新增、删除、修改)。

为什么元素级别改变一切

Delta-QA 不比对一张大图。它会:

  1. 重建页面元素的树;
  2. 在两个版本之间匹配每个元素(先按内容,再按位置);
  3. 只在最细的层级比对像素,并通过忽略某个块中已变化子元素的区域,检测这个块自身的变化;
  4. 边缘平滑从真正不同像素的计数中剔除

结果是:它可以高度敏感(在大块上捕捉到 1 px 的边框)而不被平滑变化淹没,因为这种噪声被剔除了,而且每个信号都绑定到具体元素。位移不是一块「红色色块」:它是一个元素在旧位置被标记为 消失、在新位置被标记为 出现。局部敏感度是自动的,无需事先准备遮罩。

方法论——及其局限

我们重视严谨,所以这里是灰色地带:

  • 两者使用同一个页面。 两种方法都从完全相同的、已渲染并冻结的页面出发——没有显示偏差。
  • 数据已对照参考工具验证。 我们的测试台用与最常用的逐像素比对工具相同的方式重新计算颜色差异。我们在 5 个用例上都与这款官方工具做了交叉核对:在明显的颜色变化上,两者给出 5.036% 对 5.034%——几乎一致。在其他用例上,参考工具计入更少像素(它忽略边缘平滑)——因此它更容易放过小变化。表中的数字就是它自己的。
  • Delta-QA 会过度上报(我们承认)。 在颜色变化上,它发出 3 个信号:真实的那个(头部,强度 0.996)+ 2 个非常弱的父级信号(0.005 和 0.001)。这是有意为之:我们把一切都上报,而界面的敏感度滑块默认隐藏这些弱信号。但要说清楚:原始计数并不是「1 个变化 = 1 个信号」。
  • 单一测试场景。 这些测量是在单一屏幕尺寸、页面静止、受控测试页面上进行的。我们不就多种屏幕尺寸、交互状态(悬停、聚焦)或真正嘈杂的页面做任何断言——那是另外的课题。
  • 重排。 Delta-QA 把重排的卡片归类为「已修改」而非「已移动」,但按元素局部化——这仍然远胜于一块弥散色块。

平心而论:比对整张图简单,每页只需一次捕获,对于大崩坏依然出色,其区域遮罩也确实有效。问题不在于它差——而在于它逼你在放过细微和对噪声大呼小叫之间二选一,并手动配置精细度。

要点回顾

如果你的视觉回归测试依赖于默认设置下带百分比阈值的整图比对,那它很可能放过用户能看到的变化——位移、局部颜色、微小样式变化。调低阈值能抓到它们,却会唤醒假警报;遮罩有帮助,但要事先逐区配置。

元素比对不是一项设置:它是另一种架构,同时找回了敏感度精确度——并额外附带元素名称和变化的性质

延伸阅读


可复现测试:「整图」比对由开源参考工具(pixelmatch 包,Node/npm)在默认敏感度下产出,然后套用与 BackstopJS 相同的 0.1% 判定阈值——基于与 Delta-QA 完全相同的冻结页面。