// 两张 PNG 的数值对拍:PSNR / SSIM / 平均亮度差(0-255)。 // 用法:node tools/diff.mjs [diff-out.png] // 判读:平均差 < 1 视为肉眼无差异;1-3 为细微;> 8 通常意味着构图/内容变了。 import { spawn } from "node:child_process"; const [a, b, outPng] = process.argv.slice(2); if (!a || !b) { console.error("usage: node tools/diff.mjs [diff-out.png]"); process.exit(2); } function ff(args) { return new Promise((resolve) => { const p = spawn("ffmpeg", ["-hide_banner", "-loglevel", "info", ...args], { stdio: ["ignore", "pipe", "pipe"] }); let err = ""; p.stderr.on("data", (d) => (err += d)); p.stdout.on("data", () => {}); p.on("close", () => resolve(err)); }); } const psnr = await ff(["-i", a, "-i", b, "-lavfi", "psnr", "-f", "null", "-"]); const ssim = await ff(["-i", a, "-i", b, "-lavfi", "ssim", "-f", "null", "-"]); const yavg = await ff([ "-i", a, "-i", b, "-lavfi", "blend=all_mode=difference,signalstats,metadata=print:key=lavfi.signalstats.YAVG", "-f", "null", "-", ]); const num = (text, re) => { const m = text.match(re); return m ? Number(m[1]) : NaN; }; const yavgs = [...yavg.matchAll(/YAVG=([\d.]+)/g)].map((m) => Number(m[1])); const report = { psnr_avg: /average:inf/.test(psnr) ? Infinity : num(psnr, /average:([\d.]+)/), ssim_all: num(ssim, /All:([\d.]+)/), mean_abs_diff: yavgs.length ? yavgs[yavgs.length - 1] : NaN, }; console.log(`psnr=${report.psnr_avg} dB ssim=${report.ssim_all} mean|diff|=${report.mean_abs_diff}/255`); if (outPng) { await ff(["-y", "-i", a, "-i", b, "-lavfi", "blend=all_mode=difference", "-frames:v", "1", outPng]); console.log(`diff image -> ${outPng}`); }