// 量出「立绘在屏幕上的实际足迹」:正常帧与藏 canvas 参考帧的差值掩码。 // 不依赖对视口/相机数学的任何建模——直接测渲染结果。 // 输出:足迹包围盒、四条边是否被画布切掉、覆盖面积占比、足迹底边距画布底部的距离。 // 用法:node tools/subject.mjs [threshold] [edgeBand] import { spawn } from "node:child_process"; const [normal, ref, threshold = "8", edgeBand = "2"] = process.argv.slice(2); if (!normal || !ref) { console.error("usage: node tools/subject.mjs [threshold] [edgeBand]"); process.exit(2); } const T = Number(threshold); const BAND = Number(edgeBand); function probe(file) { return new Promise((resolve) => { const p = spawn("ffprobe", ["-v", "error", "-select_streams", "v:0", "-show_entries", "stream=width,height", "-of", "csv=p=0", file]); let out = ""; p.stdout.on("data", (d) => (out += d)); p.on("close", () => { const [w, h] = out.trim().split(",").map(Number); resolve({ w, h }); }); }); } const { w: W, h: H } = await probe(normal); const buf = await new Promise((resolve, reject) => { const p = spawn("ffmpeg", ["-v", "error", "-i", normal, "-i", ref, "-lavfi", "blend=all_mode=difference,format=gray", "-f", "rawvideo", "-pix_fmt", "gray", "-"], { stdio: ["ignore", "pipe", "pipe"], }); const chunks = []; let err = ""; p.stdout.on("data", (d) => chunks.push(d)); p.stderr.on("data", (d) => (err += d)); p.on("close", (code) => (code === 0 ? resolve(Buffer.concat(chunks)) : reject(new Error(err || `exit ${code}`)))); }); if (buf.length < W * H) { console.error(`差分流只返回 ${buf.length} 字节,期望 ${W * H}`); process.exit(1); } let minX = W, maxX = -1, minY = H, maxY = -1, count = 0; const edge = { left: 0, right: 0, top: 0, bottom: 0 }; for (let y = 0; y < H; y++) { for (let x = 0; x < W; x++) { if (buf[y * W + x] <= T) continue; count++; if (x < minX) minX = x; if (x > maxX) maxX = x; if (y < minY) minY = y; if (y > maxY) maxY = y; if (x < BAND) edge.left++; if (x >= W - BAND) edge.right++; if (y < BAND) edge.top++; if (y >= H - BAND) edge.bottom++; } } if (!count) { console.log(`${normal} 没有超过阈值 ${T} 的像素——立绘层是空的?`); process.exit(1); } const pct = (n) => ((n / (W * H)) * 100).toFixed(3) + "%"; console.log(`${normal} (${W}x${H}, 阈值 ${T})`); console.log(` 足迹包围盒 x:[${minX}, ${maxX}] y:[${minY}, ${maxY}] 尺寸 ${maxX - minX + 1}x${maxY - minY + 1}`); console.log(` 覆盖面积 ${count} 像素 (${pct(count)})`); console.log(` 底部余量 画布底 ${H} - 足迹底 ${maxY} = ${H - 1 - maxY} px`); console.log(` 顶部余量 ${minY} px`); console.log(` 左右余量 左 ${minX} px / 右 ${W - 1 - maxX} px`); console.log( ` 贴边情况 ${[ edge.left ? `左 ${edge.left}` : "左 -", edge.right ? `右 ${edge.right}` : "右 -", edge.top ? `上 ${edge.top}` : "上 -", edge.bottom ? `下 ${edge.bottom}` : "下 -", ].join(" ")} (${BAND}px 带内)`, ); const cut = edge.left || edge.right || edge.top || edge.bottom; console.log(` 判定 ${cut ? "被画布边缘切到" : "四边都未触边"}`);