- 图像处理
【免费下载链接】pixelmatch
The smallest, simplest and fastest JavaScript pixel-level image comparison library
导读
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 提供了三组典型对比样例,可直观感受差异图的输出形态(黄色为检测到的抗锯齿像素,红色/绿色为真实差异像素):
| expected | actual | diff |
|---|---|---|
这些夹具图片位于 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 列出了全部可配置项,下表汇总其默认值与作用:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
threshold | number | 0.1 | 两个颜色相差多少才计为不匹配,范围0~1。它是逐像素的感知色差:1代表黑白之间的差距,而非整张图像的差异比例。若想允许整图一定比例的差异,应拿返回值与width * height比较,而不是调大 threshold。数值越小越敏感。 |
includeAA | boolean | false | 设为true时禁用"检测并忽略抗锯齿像素",即把抗锯齿像素也计入差异。 |
alpha | number | 0.1 | 差异图中未变化像素的混合因子:0为纯白背景,1为保留原图亮度。 |
aaColor | [number, number, number] | [255, 255, 0] | 差异图中抗锯齿像素的标记颜色(RGB 数组),默认黄色。 |
diffColor | [number, number, number] | [255, 0, 0] | 差异图中真实差异像素的颜色,默认红色。 |
diffColorAlt | [number, number, number] | null | 用于"深色对浅色"差异的备选颜色,可区分"新增/移除"部分。不提供时所有差异像素都使用diffColor。 |
diffMask | boolean | false | 为true时,差异绘制在透明背景上(生成 mask),而非叠加在原图上;此时也不会绘制(若检测到的)抗锯齿像素。 |
checkerboard | boolean | true | 比较半透明像素时,混合到棋盘格图案(true)而非纯白背景(false),避免两个颜色只在某一种背景上看起来相同而造成的误判。 |
windowSize | number | Infinity | 若设为有限数N,返回任意N×N滑动窗口内差异像素数的最大值,而非总数。 |
ignoreMask | Uint8Array / 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):
- 按需分配差异掩码:仅当
windowSize为有限数时才分配mask数组(Number.isFinite(windowSize) ? new Uint8Array(len) : null),默认路径保持零分配;掩码取值为 0(相同/忽略)、1(差异)、2(被排除的抗锯齿),差异像素用奇数编码,便于窗口扫描用& 1无分支计数; - 列和(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)对每个像素计算感知色差:
- 不透明像素快速路径:当两像素 alpha 均为 255 时走
colorDeltaOpaque,通过预计算好的 sRGB→线性查找表LIN、LMS 矩阵贡献表以及一个 12-bit 直接映射缓存OKLAB_CACHE快速求得立方根 LMS 与 toe 校正亮度; - 半透明像素路径:
colorDeltaTransparent先把两像素按背景(棋盘格或纯白)混合成小数通道值,再经查找表插值(linLUT/cbrtLUT)进入 OKLab 空间; - 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):
- 以目标像素为中心的 3×3 邻域内,用 Rec.601 亮度(而非 OKLab ΔL,因为检测器只需单调的标量来确定强度斜坡方向,且该方法被调用高达每候选像素 16 次)统计与中心像素"相同、更暗、更亮"的邻居数量;
- 若相同邻居超过 2 个,判定为普通图案而非抗锯齿;
- 若同时存在更暗与更亮的邻居,则取最暗/最亮邻居,用
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 的核心价值可以概括为四点:
- 感知正确的差异判定:OKLab + HyAB 度量让"看起来一样"的像素不被误报,同时能抓住人眼可感知的细微变化;
- 自动排除抗锯齿噪声:基于论文算法的抗锯齿检测,让文字/图形渲染的边缘差异不再污染测试结果;
- 灵活的差异可视化:可配置差异颜色、抗锯齿颜色、备选颜色、灰度混合因子、mask 输出,满足从测试断言到人工审查的各种需要;
- 高性能与零依赖:纯数组操作、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
相关推荐
抗锯齿检测中的像素分类:pixelmatch中的isExcludedAA判断
抗锯齿检测中的像素分类:pixelmatch中的isExcludedAA判断 引言:图像比较中的抗锯齿挑战 在前端视觉回归测试、图像比较应用中,开发者常面临一个
图像处理如何快速实现像素级图像对比:使用pixelmatch的完整指南
如何快速实现像素级图像对比:使用pixelmatch的完整指南 pixelmatch是一款最小、最简单且最快的JavaScript像素级图像对比库,能够精准检测
图像处理情感词典大揭秘:python-girlfriend-mood数据文件结构与自定义词库指南
情感词典大揭秘:python girlfriend mood数据文件结构与自定义词库指南 python girlfriend mood是一款能够通过女朋友的聊天
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考