【免费下载链接】jevgrep
Find code by asking what it does. A CLI for coding agents that uses Jev to discover relevant files and source context.
本文深入解读 jevgrep 仓库中.agents/skills/compare-screenshots/SKILL.md所定义的截图对比评审技能。它解决的是视觉开发中最棘手的判断问题:当候选渲染图与基准图存在差异时,如何客观判断"哪一边更接近正确",而不是机械地要求匹配基准。读完本文,你将掌握一套完整的六步评审工作流、双图对比与单图自检的全部指标语义、固定的距离分数公式,以及配套脚本visual-parity-diff.mjs的真实用法与自检方式,可直接用于 UI、游戏画面、文档渲染、图表等任何生成资产的接受/拒绝决策。
核心原则:判断"哪张图更不错误",而非"是否匹配基准"
该技能的第一条纪律就是反转直觉:比较的目标是"哪张图更不错误"(less wrong),而不是"候选图是否与基准一致"。
基准(baseline)只是更早的一次尝试,它同样可能是错的——可能相机意图不对、内容缺失、光照错误或裁剪有问题。因此:
- 两张图都只是候选者(candidates),统一用你自行建立的目标(target)来衡量;
- 指标的作用是定位两张图在哪些地方不同(locate divergence),它们永远不会替你裁决谁是对的;
- 距离为 0 不代表成功(可能是两张图错得一样),距离很大也不代表失败(更丰富的场景、更清晰的模型、更实的标签、真正的深度、更好的光照都会合法地拉大距离)。
这一原则贯穿整个技能:所有指标、公式、脚本产物都围绕"定位差异"设计,而把"裁决"留给第 4 步基于目标的逐项判断。
六步评审工作流
技能的完整工作流如下,每一步都不可跳过:
- 从第一性原理建立目标(Establish the target from first principles)。在查看任何距离指标之前,先决定这张图应该呈现什么:视觉需求、设计意图、真实世界里物体长什么样,以及拥有该画面话语权的领域技能。这——不是基准——才是 ground truth。用一两句具体的话写下来(例如"低角度的太阳应向东投下长影;树木应填满树冠;标签在这个缩放级别下保持可读")。
- 如果正确答案不明确——存在互相竞争的合理解读、品味或产品意图之争、只有负责人能拍板的权衡——停下来问用户正确答案应该是什么,并把对比结果展示给他们。不要为了省事而悄悄默认基准,那会把基准犯下的错误固化下来。
- 确认可比性(Confirm comparability),确保你看到的差异是真实的而非采集伪影:相同的视口(viewport)、DPR、路由/页面、冻结的时间/帧、相机意图、UI 状态、数据、字体/资源。不可比则修正采集设置,或只对比差异无害的裁剪/局部特征。
- 生成定位差异的产物(Generate artifacts to locate divergence),规模与问题匹配:并排图(side-by-side)、关键特征裁剪/放大(crops/zooms)、灰度图、绝对灰度热力图、pixelmatch 差异图、每张图的 Sobel 边缘图、边缘差异热力图,以及 JSON 指标。
- 逐项对照目标裁决每个差异(Judge each divergence against the target)。对每一处不同,用平实的语言说出那里实际有什么——内容缺失、相机错误、层级混乱、对比度弱、深度错误、文字重叠、布局偏移、边缘被裁、意外模糊、风格不匹配——并判断哪一边更接近正确。答案可以是:候选对、基准对、两者都错、或真正的难分伯仲。
- 对争议或高风险结论获取中立第二意见(Get a neutral second opinion):让一个全新的子代理(subagent)只拿到两张图和中性标签进行独立判断,配置见 references/subagent-visual-review.md。
- 以一个裁决收尾(Conclude with one verdict):候选更不错误(接受,并在存在基准时重新 bless 基准)、基准更不错误(拒绝)、两者都错(需要再来一轮——指出还差什么)、或结论不清(询问用户)。绝不因为分数更低就接受、或因为分数更高就拒绝。绝不隐藏内容、模糊细节、裁掉差异、或让采集变得不真实来推动某个数字。
双图对比指标:为"问题"挑选"度量"
在完整视觉对比中,技能建议报告以下指标(全部可追溯到脚本visual-parity-diff.mjs的实际输出字段):
| 指标 | 含义 | 说明 |
|---|---|---|
mae | 灰度平均绝对差(0..255) | 越低越接近 |
rmse | 灰度均方根误差 | 越低越接近 |
diffRatio16/32/64 | 灰度差超过 16/32/64 的像素占比 | 三个阈值下越界像素的比例 |
pixelmatchRatio | 灰度图上 pixelmatch 的不匹配像素占比 | 由 pixelmatch 库在阈值 0.08、含抗锯齿(includeAA: true)下计算 |
edgeEnergyCurrent/edgeEnergyCandidate | 两侧的平均 Sobel 边缘强度 | 判断画面结构丰富度 |
edgeEnergyRatio | 候选/当前的边缘能量比 | 远低于 1 通常意味着几何、道具、标签或地形缺失;远高于 1 通常意味着噪点或错误细节 |
edgeDiffRatio32 | Sobel 边缘差异超过 32 的像素占比 | 结构差异的量化 |
avgLuminanceCurrent/Candidate/Delta | 平均亮度及差值 | 当渲染明显偏暗/偏亮、即使整体距离分数改善时使用 |
针对场景还提供内容代理指标(content proxies):black/void 占比(blackRatio)、类地形占比(terrainLikeRatio)、水类占比、队伍颜色占比、标签/文字掩码占比。脚本中的isTerrainLike实现为:g > 72 && r > 62 && b > 38 && r > b * 0.92 && g > b * 0.92。
对于 UI/文档/布局类评审,还应使用裁剪边界(crop bounds)、文字/前景掩码覆盖率、对比度检查、边缘裁切、元素位置与前后尺寸——这些在击败全局像素距离时优于全局像素指标。
单图指标:没有对照时,如何判断一张截图是否值得
每一对指标都在"图 vs 图",没有任何一个能回答"这张截图值不值得"——当没有东西可比时;而且对分数是对称的,巨大的距离永远不说明哪一边是空帧。以下绝对数值可以,它们在单张 PNG 的粗网格上计算:
colorEntropyBits低于约 3.0,或dominantColorShare超过约 0.6:一种颜色统治了整帧——稀疏场景、未点亮场景、或主体从未被画出来。edgeDensity低于约 0.04:几乎没有任何形状——空取景、图元主导的场景、或主体在裁剪框之外。luminanceContrast低于约 60:雾、黑暗或薄雾把整帧压进同一个色带。transparentShare在应当不透明的截图上大于 0:截图本身错了。透明像素保留着遗留的 RGB,所以一帧不可见的画面在被合成前可能看起来很丰富——场景指标先做合成再测量,并如实报出不可见占比,而不是让你去推断。成对指标仍读取存储的 RGB,所以这个字段是透明度被如实讲出来的唯一位置。
从源码看,computeSceneMetrics按约 160×90 的粗网格采样(stepX = max(1, floor(width/160)),stepY = max(1, floor(height/90))),每通道取 4 bit 分桶(key = r>>4, g>>4, b>>4),这样梯度与抖动不会误读为"精心创作的多彩",成本也远低于成对计算。
这些阈值是"怀疑"的触发器,不是门禁。刻意极简的设计、夜景、空状态页面、白板都会诚实地触发它们。用途是决定"往哪儿看",然后说出这帧画面实际在做什么——永远不要为了抬高某个数字而调整采集,这与裁掉差异是同一种失败。
距离分数:一个固定配对数字的默认公式
当需要一个固定的单对数字时,技能给出适用于结构变化默认公式:
distance = 0.35 * diffRatio32 + 0.25 * pixelmatchRatio + 0.25 * edgeDiffRatio32 + 0.15 * min(1, abs(log2(edgeEnergyRatio)))它衡量的是与另一张图的距离,仅此而已。因为基准可能错,距离 0 不是成功、大距离也不是失败——更丰富的场景、更清晰的模型、更强的标签、真实的深度、更好的光照都会合法地抬高它。用法是:用这个分数找到图像"移动"的位置,在第 4 步裁决谁对。脚本中报告字段命名为parityDistance,报告注记明确写着"This is a distance metric, not an acceptance gate",并且输出会同时给出全帧分数和(当 UI 主导画面时)带标签的世界裁剪分数——用世界裁剪分数定位渲染器移动,保留全帧分数让 UI/相机错误继续可见。
实现细节值得一提:edgeEnergyRatioOf为两个都没有边缘的帧设了1e-6下限并返回 1——两张完全没有边缘的平坦帧不是"无限远",而是同一帧;如果直接做除法,一对相同的空渲染(正是这套指标要抓的情况)会被送到非零距离。
分数纪律
- 每一轮迭代都引用同一对的上一次与本次距离,然后说明移动方向:朝目标、远离目标、还是诊断性噪声。
- 缺内容类 bug 优先用边缘指标:一张平坦的俯视地图可能展现出欺骗性的中等灰度差,而边缘能量能证明树木、道路、城市轮廓或军队剪影确实不存在。
- 当稳定 UI 主导画面而问题在场景渲染时,先分割出稳定 UI,同时保留一个带标签的全帧分数。
- 相机错了,像素分数只是诊断性的。先修相机意图,再判断渲染。
配套脚本实战:visual-parity-diff.mjs
技能规定:对比脚本应保留在技能内部或临时工作区,不要放进产品代码——除非产品确实需要在运行时做截图对比。本仓库内提供了现成的可复用助手:
# 成对模式:需要 REFERENCE_DIR 与 CANDIDATE_DIR REFERENCE_DIR=<png文件夹> CANDIDATE_DIR=<png文件夹> OUT_DIR=<产物文件夹> node .agents/skills/compare-screenshots/scripts/visual-parity-diff.mjs # 固定配对顺序:按 a,b,c 的顺序固定报告排序 REPORT_ORDER=a,b,c # 带标签裁剪:按图像 id 键控,单位为像素或 { "unit": "ratio" } 归一化边界 CROPS_JSON=<file.json> # 单图模式:不设 REFERENCE_DIR,只测量每张截图的自身指标 CANDIDATE_DIR=<png文件夹> OUT_DIR=<产物文件夹> node .agents/skills/compare-screenshots/scripts/visual-parity-diff.mjs行为细节(与 SKILL.md 及源码一致):
- 有
REFERENCE_DIR时执行成对差异:按文件名(.png后缀)匹配REFERENCE_DIR与CANDIDATE_DIR中的共同图片,尺寸不一致直接抛错(image sizes differ),每对生成 8 类产物:*-side-by-side.png、*-current-gray.png、*-candidate-gray.png、*-absdiff.png、*-pixelmatch.png、*-current-edges.png、*-candidate-edges.png、*-edge-diff.png,并写入visual-parity-diff.json,结果按parityDistance降序排列,报告同时标注worstPair。 - 有
CROPS_JSON时对指定裁剪区域做同样的全套分析,产物前缀为*-world-crop-*,输出独立的parityDistance与同结构指标。 - 不设
REFERENCE_DIR时只写scene-metrics.json,不含任何 diff 产物——用于孤立截图、任何人在评审前检查完整采集集、或判断大距离中哪一边才是空帧。 - 透明像素先按 alpha 复合到 RGB 再测(
r = png.data[o] * alpha),因此"不可见帧看起来丰富"的陷阱会被transparentShare直接点名。 - 灰度用 Rec. 601 亮度权重:
0.2126 * r + 0.7152 * g + 0.0722 * b;Sobel 用 3×3 邻域算子(gx = -a -2d -g + c + 2f + i等),边缘能量按像素归一。 - 脚本从
REPO_ROOT(缺省为当前工作目录或其父目录)解析web/package.json中的pngjs与pixelmatch依赖,运行前需保证这些依赖在对应仓库中可解析。
用 eval 脚本守住助手本身
visual-parity-diff.eval.mjs 是助手的自检测试:它用draw()逐像素生成正确性"由构造即已知"的夹具——空(empty-solid、empty-transparent-noise、empty-transparent-white、empty-smooth-gradient)、有内容(content-noise、content-scene)、图元主导(primitive-dominant)、半透明混合(mixed-half-transparent)、低对比度暗帧(dim-lowcontrast)、微小真实图(small-but-real)、退化图(one-pixel-wide)——然后断言:
- 场景指标能正确区分"空"与"有内容"(
colorEntropyBits < 3.0 || dominantColorShare > 0.6 || edgeDensity < 0.04判空); - 全透明帧报
transparentShare === 1、不透明帧报 0、半透明帧接近 0.5; - 尺度不变性(同一场景 1x 与 2x 分辨率的
edgeDensity、colorEntropyBits接近)与色调不变性(纯红/纯蓝的平坦度指标完全相等); - 透明黑覆在噪点上与其合成等价物(flat-black)指标完全一致;
- 成对模式:相同对距离为 0、距离对称、两侧都带
sceneMetrics以打破对称平局、仍写出 diff 产物而单图模式不写任何 diff 产物; - 263 张批量图的管道化 stdout 完整可解析、与落盘报告一致;
- 失败路径响亮:缺
CANDIDATE_DIR、空目录都会非零退出并打印明确错误。
运行方式:REPO_ROOT=<repo> node visual-parity-diff.eval.mjs,在修改脚本后用同一REPO_ROOT运行,任一期望集失败即非零退出并打印所有检查项。纪律是:绝不允许用放宽阈值来通过失败检查——除非先证明错的是夹具而不是代码。
中立子代理评审:防止历史偏差污染判断
当对话历史或先前的结论可能影响主代理的视觉判断时,使用 references/subagent-visual-review.md 中定义的独立评审配置:
- 生成配置:
agent_type: default、fork_context: false、把两张截图作为local_image附上、中性标签Image A/Image B或Reference/Candidate;绝不告诉子代理哪张是候选、参考、期望、已接受、已失败、更新、更旧、更好或更差。 - 提示词要点:子代理"对同一视觉目标的两张截图做无偏视觉评审,无任何先验上下文",报告:① 是否显示相同的视口/状态/内容;② 相机/视角、布局、内容、缺失细节、标签/文字、图标、颜色、光照、深度/层次、裁切、伪影、可读性、风格上的主要可见差异;③ 对表面任务而言哪张更完整/可读,为什么;④ 关于图像是否保持预期视觉关系或需要再来一轮的简明裁决——"不要假设任一张是期望目标,只依据可见像素判断"。
- 结果的使用:把子代理结果当作"哪张更不错误"的独立证据,不是指标或你自己检查的替代品,也不是支持基准的投票。若子代理指出相机错误、状态不匹配、内容缺失或可见伪影,先修采集/渲染质量再判断其余;当它改变或确认下一个实现目标时,把裁决引述到工作笔记中。
何时使用该技能
按技能自身的触发条件,当以下场景出现时启用:UI、游戏、文档、渲染、图表或生成资产需要客观视觉遥测、并排检查、裁剪/缩放评审,或在接受/拒绝一次视觉变更前需要一份全新的第二意见;也适用于只有一张截图、没有对照的情况——用它测量该帧是否平坦、空洞或取景糟糕,趁任何人评审之前。
本文所有指标语义、公式、阈值与运行方式均以 SKILL.md 为准,实现细节可对照 visual-parity-diff.mjs 与 visual-parity-diff.eval.mjs 两处源码验证。
【免费下载链接】jevgrep
Find code by asking what it does. A CLI for coding agents that uses Jev to discover relevant files and source context.
相关推荐
Front-End-Checklist 视觉回归测试实战:用 Playwright 截图对比拦截 CSS 回归
Front End Checklist 视觉回归测试实战:用 Playwright 截图对比拦截 CSS 回归 视觉回归测试(Visual Regression
oh-my-claudecode 视觉裁决(visual-verdict):用结构化 JSON 驱动截图与参考图的逐轮比对
oh my claudecode 视觉裁决(visual verdict):用结构化 JSON 驱动截图与参考图的逐轮比对 导读 在 Claude Code 驱
人工智能AI Agent多智能体Agent 编排Agent 工作流AI 技能CLI开发工具Front-End-Checklist 视觉回归测试实践指南:用 Playwright 截图对比与 Chromatic 守护页面外观
Front End Checklist 视觉回归测试实践指南:用 Playwright 截图对比与 Chromatic 守护页面外观 视觉回归测试(Visual
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考