☰
pixelmatch:基于 OKLab 感知色彩与抗锯齿检测的 JavaScript 像素级图像比对库
2026/10/15 13:38:09 网站建设 项目流程
  • 图像处理

【免费下载链接】pixelmatch

The smallest, simplest and fastest JavaScript pixel-level image comparison library

项目地址:https://gitcode.com/gh_mirrors/pi/pixelmatch
点击查看免费下载

导读

pixelmatch 是一个"小而快"的 JavaScript 像素级图像比较库,最初为测试中的截图对比而设计。它以极简 API 对原始像素数组(Buffer/Uint8Array/Uint8ClampedArray)逐像素比较,内置精确的抗锯齿(anti-aliasing)像素检测和基于 OKLab 颜色空间的感知色彩差异度量,且零运行时依赖,可在 Node.js 与浏览器中运行。读完本文,你将掌握 pixelmatch 的完整 API 与全部配置参数、Node/浏览器/命令行三种使用方式,并理解其窗口化差异计数、忽略遮罩等高级能力背后的源码级实现原理。

本文主体基于仓库根目录的 README.md,实现细节佐证于 index.js、bin/pixelmatch 与 test/test.js。

项目概览

pixelmatch 的定位非常明确:针对同一尺寸的两张图像做逐像素比对,输出差异图并返回不匹配像素数量。它具备以下核心特征:

  • 抗锯齿像素检测:自动识别并忽略由字体渲染、图形抗锯齿带来的伪差异,避免把"渲染噪声"误报为真实回归;
  • 感知色彩差异:颜色差异在 OKLab 颜色空间下用 HyAB 距离度量,更贴合人眼对颜色差异的感知,而非简单的 RGB 欧氏距离;
  • 代码量小、零依赖:核心实现只有几百行,直接操作原始字节数组,不依赖任何第三方运行时库,性能出色;
  • 跨端运行:既能在 Node.js 中配合 pngjs 解码 PNG,也能在浏览器中直接消费ImageData。

从 package.json 可以看到,当前仓库版本为8.0.0,采用 ESM("type": "module"),运行依赖仅 pngjs(用于命令行与测试中的 PNG 编解码),核心比对逻辑本身不依赖任何包。

感知度量的算法背景

README 明确说明,pixelmatch 的色彩比较基于两个公开发表的成果:

  • OKLab 颜色空间(Björn Ottosson,2020):一种感知均匀的 Lab 类色彩空间,被广泛用于图像处理中的感知色差计算;
  • HyAB 距离(Saeideh Abasi 等,2019):即|ΔL| + √(Δa² + Δb²),在跟踪大色差时表现良好;
  • 抗锯齿像素与强度斜率检测器(Vytautas Vyšniauskas,2009):用于识别目标像素是否为抗锯齿过渡像素。

这些算法均实现在 index.js 中:colorDelta及colorDeltaOpaque/colorDeltaTransparent完成 OKLab + HyAB 度量,antialiased函数则实现了 2009 年论文中的检测逻辑(详见下文"底层原理"一节)。

示例输出效果

README 提供了三组典型对比样例,可直观感受差异图的输出形态(黄色为检测到的抗锯齿像素,红色/绿色为真实差异像素):

expectedactualdiff

这些夹具图片位于 test/fixtures/ 目录下,测试用例(test/test.js)会逐字节断言比对结果与预期 diff 图片完全一致,例如3a/3b在threshold: 0.05下应得到 220 个差异像素。

API 详解

函数签名

pixelmatch(img1, img2, output, width, height[, options])
  • img1、img2— 待比较图像数据,类型为Buffer、Uint8Array或Uint8ClampedArray;
  • output— 写入差异图的目标数据,若不需要差异图可传null;
  • width、height— 图像宽高。注意:三张图像必须具有相同尺寸(即img1、img2与output长度一致)。

函数返回不匹配像素的数量;当设置了windowSize时,返回任何 N×N 滑动窗口内差异像素数的最大值(详见"窗口化差异计数"一节)。

options 全部参数

README 列出了全部可配置项,下表汇总其默认值与作用:

参数类型默认值说明
thresholdnumber0.1两个颜色相差多少才计为不匹配,范围0~1。它是逐像素的感知色差:1代表黑白之间的差距,而非整张图像的差异比例。若想允许整图一定比例的差异,应拿返回值与width * height比较,而不是调大 threshold。数值越小越敏感。
includeAAbooleanfalse设为true时禁用"检测并忽略抗锯齿像素",即把抗锯齿像素也计入差异。
alphanumber0.1差异图中未变化像素的混合因子:0为纯白背景,1为保留原图亮度。
aaColor[number, number, number][255, 255, 0]差异图中抗锯齿像素的标记颜色(RGB 数组),默认黄色。
diffColor[number, number, number][255, 0, 0]差异图中真实差异像素的颜色,默认红色。
diffColorAlt[number, number, number]null用于"深色对浅色"差异的备选颜色,可区分"新增/移除"部分。不提供时所有差异像素都使用diffColor。
diffMaskbooleanfalse为true时,差异绘制在透明背景上(生成 mask),而非叠加在原图上;此时也不会绘制(若检测到的)抗锯齿像素。
checkerboardbooleantrue比较半透明像素时,混合到棋盘格图案(true)而非纯白背景(false),避免两个颜色只在某一种背景上看起来相同而造成的误判。
windowSizenumberInfinity若设为有限数N,返回任意N×N滑动窗口内差异像素数的最大值,而非总数。
ignoreMaskUint8Array / Uint8ClampedArray / Buffer不设置长度为width * height的逐像素遮罩;值非零的像素被排除在比较之外,并在差异图中绘制为未变化。

这些默认值在 index.js 的解构赋值中原样体现:

const { threshold = 0.1, alpha = 0.1, aaColor = [255, 255, 0], diffColor = [255, 0, 0], checkerboard = true, windowSize = Infinity, includeAA, diffColorAlt, diffMask, ignoreMask } = options;
threshold 的正确理解

README 特别强调:threshold是每像素的感知色差阈值,1表示黑与白的差距,不是图像差异面积占比。测试 test/test.js 中有一个典型验证:一对人眼几乎无差别的颜色(0,254,252)与(94,254,252),在默认阈值下应判为"相同"(返回 0);而(41,56,157)与(41,56,0)这样肉眼差异明显的颜色对则必须判为"不同"(返回 1)。这说明 OKLab + HyAB 度量能正确区分感知上相近与相远的颜色,这正是它优于简单 YIQ/RGB 距离的原因。

若需要"允许整图 X% 的像素不同",正确写法是把返回的差异像素数与总面积比较:

const numDiffPixels = pixelmatch(img1, img2, null, width, height, {threshold: 0.1}); if (numDiffPixels / (width * height) > 0.02) throw new Error('图片差异超过 2%');
半透明像素与 checkerboard

checkerboard默认true,比较半透明像素时先将其混合到棋盘格背景上。测试用例给出了一个直观示例(test/test.js):

// 这两个像素在白色背景上视觉相同,但在深色棋盘格上不同 const img1 = new Uint8Array([0, 0, 0, 128]); // 50% 透明黑色 const img2 = new Uint8Array([127, 127, 127, 255]); // 不透明灰色 assert.equal(match(img1, img2, null, 1, 1, {checkerboard: false}), 0); assert.equal(match(img1, img2, null, 1, 1), 1);

即:checkerboard: false时二者被判为相同(都混合到纯白),而默认棋盘格模式下能正确识别出它们实质不同。在源码中,透明像素路径colorDeltaTransparent(index.js)会按像素索引的斐波那契哈希选择 8 种棋盘格底色之一进行混合。

diffColorAlt 的"新增/移除"语义

当diffColorAlt提供时,变暗方向(img2 中像素比 img1 更暗)的差异使用备选色绘制,形成"新增 vs 移除"的视觉区分。从源码看,这一判断基于colorDelta返回值的符号:返回负值代表 img2 像素更暗,此时使用altR/altG/altB,否则使用diffColor(index.js)。测试中7a/7b使用{diffColorAlt: [0, 255, 0]}(绿色)来区分差异方向(test/test.js)。

窗口化差异计数(windowSize)

为什么需要窗口化

常规模式下返回的是整图差异像素总数。但 GPU 抖动(dithering)和亚像素抗锯齿会把零散像素洒满整张图,这些"噪声"永远不会填满一个小的方形窗口;而一次真正的回归(regression)通常改变的是一个紧凑区域。因此,把windowSize设为有限值N,返回任意 N×N 滑动窗口内差异像素数量的最大值,能天然抗噪。

使用方式(README 示例):

if (pixelmatch(img1, img2, null, width, height, {windowSize: 16}) > 28) throw new Error('changed');

这里的语义是:"28是你允许放行的差异像素数,N则是这些差异必须聚集得多么紧凑"。只有差异足够密集地出现在同一个16×16窗口内才会触发报错。

边界行为

  • N永远不会超过图像的任意一边:例如在10×2的图像上,windowSize: 32实际得到的是2×2的窗口;
  • 默认Infinity使窗口等于整张图,即退化为总数;
  • 抗锯齿像素仅在includeAA: true时计入窗口计数。

从源码(index.js)可以看到窗口化实现的具体规则:N先向下取整,再钳制到[1, min(width, height)]区间(Math.min(Math.max(Math.floor(windowSize), 1), width, height))。测试 test/test.js 验证了这些边界:windowSize: 2.9取整为 2、0与-5回退为 1×1 窗口、NaN因非有限数而保持整图总数、超大值(如Number.MAX_SAFE_INTEGER)饱和到整图而不会溢出回绕。

源码级实现原理

窗口化模式并非"对每个窗口暴力扫描",而是有两条重要的性能设计(index.js):

  1. 按需分配差异掩码:仅当windowSize为有限数时才分配mask数组(Number.isFinite(windowSize) ? new Uint8Array(len) : null),默认路径保持零分配;掩码取值为 0(相同/忽略)、1(差异)、2(被排除的抗锯齿),差异像素用奇数编码,便于窗口扫描用& 1无分支计数;
  2. 列和(colSum)增量维护 + 行带裁剪:维护每一列在过去n行内的差异像素数colSum,进入一行时累加、离开一行时减去;同时只扫描首个差异行到最后一个差异行之间的范围(firstDiffY/lastDiffY),并用带内差异总数bandTotal作为窗口计数的上界,带内总数无法超过当前maxCount时整条带直接跳过。测试对真实夹具的验证(test/test.js)显示:256×256 的6a/6b总差异 51 像素,windowSize: 32时为 29,windowSize: 8时为 6——窗口越小,能"装下"的聚集差异越少。

命令行用法

pixelmatch 自带一个可直接处理 PNG 的二进制命令(定义于 package.json 的bin字段,实现见 bin/pixelmatch):

pixelmatch image1.png image2.png [diff.png] [threshold] [includeAA]
  • image1.png、image2.png— 待比较的两张 PNG;
  • diff.png— 可选,差异图输出路径;
  • threshold— 可选,感知色差阈值;
  • includeAA— 可选,任意非"false"的值都会启用抗锯齿像素计入差异。

命令会打印不同像素的数量,并以退出码表达结果:

退出码含义
0图像匹配(无差异像素)
66图像不同
65图像尺寸不匹配
64用法错误(参数不足)

从 bin/pixelmatch 的源码可确认这些细节:参数少于 4 个时打印 usage 并以 64 退出;两张图宽高不一致时打印Image dimensions do not match: ...并以 65 退出;比对完成后会打印耗时(matched in ...)、different pixels: N与误差百分比error: x.xx%,最后以diffs ? 66 : 0退出。注意includeAA的解析规则是includeAA !== 'false',即传入任意值(包括空值之外的其他字符串)都会视为开启。

命令行本身通过 pngjs 完成 PNG 的同步解码与编码(bin/pixelmatch),这也是pngjs是唯一运行依赖的原因。

示例用法

Node.js 场景(配合 pngjs 解码 PNG)

import fs from 'fs'; import {PNG} from 'pngjs'; import pixelmatch from 'pixelmatch'; const img1 = PNG.sync.read(fs.readFileSync('img1.png')); const img2 = PNG.sync.read(fs.readFileSync('img2.png')); const {width, height} = img1; const diff = new PNG({width, height}); pixelmatch(img1.data, img2.data, diff.data, width, height, {threshold: 0.1}); fs.writeFileSync('diff.png', PNG.sync.write(diff));

要点:PNG.sync.read产出的PNG对象带有data(RGBA 字节数组)与width/height属性,直接符合 pixelmatch 的输入约定。这也是 test/test.js 中readImage/writeImage辅助函数采用的做法。

浏览器场景(配合 Canvas ImageData)

const img1 = img1Context.getImageData(0, 0, width, height); const img2 = img2Context.getImageData(0, 0, width, height); const diff = diffContext.createImageData(width, height); pixelmatch(img1.data, img2.data, diff.data, width, height, {threshold: 0.1}); diffContext.putImageData(diff, 0, 0);

getImageData返回的ImageData.data是Uint8ClampedArray,属于受支持的输入类型;差异结果通过putImageData写回画布即可可视化。

安装

npm install pixelmatch

或通过 CDN 在浏览器中直接以 ES 模块引入:

<script type="module"> import pixelmatch from 'https://esm.run/pixelmatch'; </script>

底层实现原理(源码级解读)

快速路径:先判断两图是否逐字节相同

pixelmatch 在逐像素比较之前,先把 RGBA 字节数组按 32 位整数批量比对(index.js)。如果两张图完全相同,直接返回 0,并按需把整张图绘制为灰色背景(diffMask: false时),避免无谓的逐像素计算。若设置了ignoreMask,被忽略的像素不参与该判定。

输入校验

入口处有严格的类型与尺寸校验(index.js),与 README 的"三张图像需同尺寸"要求对应:

  • 输入必须是ArrayBuffer.isView且BYTES_PER_ELEMENT === 1的数组(兼容Buffer,并规避部分 Jest 环境下instanceof Uint8Array失效的问题);
  • img1.length、img2.length必须相等(含output);
  • 数组长度必须等于width * height * 4;
  • ignoreMask长度必须等于width * height。

对应测试见 test/test.js,每种违规都会抛出对应错误信息。

OKLab + HyAB 感知色差度量

colorDelta(index.js)对每个像素计算感知色差:

  1. 不透明像素快速路径:当两像素 alpha 均为 255 时走colorDeltaOpaque,通过预计算好的 sRGB→线性查找表LIN、LMS 矩阵贡献表以及一个 12-bit 直接映射缓存OKLAB_CACHE快速求得立方根 LMS 与 toe 校正亮度;
  2. 半透明像素路径:colorDeltaTransparent先把两像素按背景(棋盘格或纯白)混合成小数通道值,再经查找表插值(linLUT/cbrtLUT)进入 OKLab 空间;
  3. HyAB 阈值判定:oklabHyabDelta将 HyAB 距离|ΔLr| + √(Δa²+Δb²)与maxDelta(即threshold)比较时巧妙地避免开方——距离不超过阈值当且仅当|ΔLr| <= maxDelta且Δa²+Δb² <= (maxDelta − |ΔLr|)²,返回值仅保留0/±1(符号表示 img2 像素更亮还是更暗),供差异绘制与计数使用。

值得一提的实现细节:lightness使用 Ottosson 的 toe 校正(TOE_K1 = 0.206、TOE_K2 = 0.03),避免近黑区域的显示/图像差异被过度放大,同时保持黑白之间0..1的刻度。测试专门验证了近黑处理(test/test.js):纯黑与gray=13判为相同、与gray=23判为不同;纯黑与单通道 1 级偏差不越过threshold: 0.05的阈值,而与(32,20,20)这种真实可见的跳变则必须判为不同。

抗锯齿检测与忽略

对于每个超过阈值的像素,pixelmatch 会调用antialiased(index.js)来判断它是否是抗锯齿过渡像素(仅当includeAA: false):

  1. 以目标像素为中心的 3×3 邻域内,用 Rec.601 亮度(而非 OKLab ΔL,因为检测器只需单调的标量来确定强度斜坡方向,且该方法被调用高达每候选像素 16 次)统计与中心像素"相同、更暗、更亮"的邻居数量;
  2. 若相同邻居超过 2 个,判定为普通图案而非抗锯齿;
  3. 若同时存在更暗与更亮的邻居,则取最暗/最亮邻居,用hasManySiblings检查其在两张图中是否都有 3 个以上同色邻居——满足条件即认为该差异是抗锯齿所致。

被判定为抗锯齿的像素:不计数、在差异图中以aaColor(默认黄色)标记,且在diffMask模式下完全不绘制。半透明像素在检测时固定混合到纯白背景,以保证斜坡结构判定的确定性。

差异图的绘制规则

  • 真实差异像素:按delta符号用diffColor或diffColorAlt绘制(index.js);
  • 抗锯齿像素:用aaColor绘制,仅在非diffMask模式下;
  • 相同像素:用drawGrayPixel绘制为与alpha因子混合的灰度(alpha: 0为纯白、alpha: 1保留原亮度),同样仅在非diffMask模式下;diffMask: true时相同像素保持透明,从而得到只含差异像素的蒙版图。

在测试与基准中使用

仓库自带的 test/test.js 是很好的集成示例:它用 node 内置的node:test运行 15+ 组测试,逐像素断言 diff 输出与 test/fixtures/ 中预期图片完全一致,并断言返回的差异像素数与预期精确相等。其中1a/1b(threshold: 0.05)预期 152 个差异像素、默认阈值下为 121,而5a/5b预期 0(差异全为抗锯齿所致);9a/9b与10a/10b分别验证了 ignoreMask 在差异部分与快速路径(差异全部落在遮罩内)下的行为。

若需要更新测试夹具,可设置UPDATE环境变量重新生成 diff 图片(test/test.js)。运行测试与基准:

npm test # 先跑 eslint(pretest),再执行 tsc 类型检查与 node --test node bench.js # 对 1a–7a/1b–7b 夹具各比较 100 轮并计时

bench.js 对 7 组夹具做 100 轮重复比对并输出整体耗时,可用于评估不同阈值或算法改动对性能的影响。需要说明:本文不提供任何性能数据,具体数字请以当前仓库在你机器上的实际运行为准。

总结

pixelmatch 的核心价值可以概括为四点:

  1. 感知正确的差异判定:OKLab + HyAB 度量让"看起来一样"的像素不被误报,同时能抓住人眼可感知的细微变化;
  2. 自动排除抗锯齿噪声:基于论文算法的抗锯齿检测,让文字/图形渲染的边缘差异不再污染测试结果;
  3. 灵活的差异可视化:可配置差异颜色、抗锯齿颜色、备选颜色、灰度混合因子、mask 输出,满足从测试断言到人工审查的各种需要;
  4. 高性能与零依赖:纯数组操作、32 位批量快速路径、零分配默认路径、查找表与缓存优化,加上窗口化计数解决 GPU 抖动类噪声,使其非常适合集成进大型测试套件。

如需进一步深入了解,推荐阅读仓库中的 README.md(API 与用法总览)、index.js(约 600 行的核心实现,含大量性能优化注释)、bin/pixelmatch(命令行实现)以及 test/test.js(覆盖所有参数与边界行为的测试用例)。

  • 图像处理

【免费下载链接】pixelmatch

The smallest, simplest and fastest JavaScript pixel-level image comparison library

项目地址:https://gitcode.com/gh_mirrors/pi/pixelmatch
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询