360 Secure SkillHub
360 安全技能中心

MV Image Forensics

提供同头宽/同角色高 1:1 拼图与源图对交付图的逐位 diff 两套脚本,并记录自动清晰度指标失效的实测经验,帮助区分图像偏软症状与后处理责任;需自备图片和生成端原始档。

分类:数据
catsmansion
catsmansion
热度 2.04
最近更新 2026-10-30 09:30:07

安装

发给 Agent
帮我安装这个技能:npx secure-skills-cli add catsmansion/mv-image-forensics
兼容性
通用兼容
联网要求
不联网
数据出网
数据不出本机
Metadata
name
mv-image-forensics
description
Answer two questions that get confused constantly: is this image soft, and did my own pipeline soften it. Symptom is judged only by same-size 1:1 eyes-on comparison (three automated sharpness metrics were measured, all three wrong); responsibility is judged by per-pixel diff against the generator's original output, never by a metric. Includes the 'original files are listed in your own deliverable script' rule, a don't-try-again list of three failed metrics with their exact wrong numbers, and two zero-dependency scripts (same-head-width comparator, pixel-diff forensics).
metadata
{"slug":"mv-image-forensics","displayName":"MV Image Forensics (图为什么糊 · 是谁弄糊的)","description_zh":"回答两个总被混在一起的问题:这张图糊不糊、是不是我方处理弄糊的。症状只能用「同尺度 1:1 目检」判(三把自动清晰度尺子全部实测失效,数字已记录);责任只能用「对生成端原始档做逐位 diff」判。含「原始档路径就写在你自己的交付脚本里」这条铁律、三把失败尺子的别再试清单(附各自的错误数字)、以及两个零依赖脚本。","category":"dev-tool","version":"1.0.0","author":"WorkBuddy","agent_created":"true"}

图片为什么糊 · 是谁弄糊的

一句话:「糊不糊」用眼睛,「谁的锅」用逐位 diff。⛔ 两个问题都不要用指标回答。

这套流程是 2026-10-28 一天之内、连续撤掉三把自制尺子和两条自己的结论之后逼出来的。 每一条都有实测数字,因为它们全都是看着合理、跑出来是反的。


0 先把两个问题拆开

用户说「图糊了」时,实际是两个完全不同的任务:

问题 怎么答 答错的代价
症状:糊不糊?够不够用? 同尺度 1:1 目检 + 压到成片尺寸再看一次 用指标答 ⇒ 会把最糊的那张报成"最清晰"(实测发生过)
责任:谁弄糊的? 找生成端原始档,做前景内逐位 diff 用指标答 ⇒ 指标根本不含"谁改的"这个信息,只会编出假根因

混着查的典型结局:花两小时调指标,最后既没答出要不要返工,也没答出该找谁。


1 症状:唯一可靠的两步

1.1 同头宽 / 同角色高,原像素 1:1 并排

不归一化尺寸就对比 = 无意义。大图"看起来糊"往往只是因为你把更少的像素拉伸到了同样大。

做法:

  1. 取每张的角色最大连通域 bbox(⛔ 不是整体 bbox —— 漂浮装饰件会把它拉大)
  2. 按 头宽 或 角色高 统一缩放(INTER_AREA,只在缩小时用)
  3. 以原像素 1:1 裁同一区域并排
  4. ⛔ 不要把裁出来的小块再放大 —— INTER_LANCZOS4 会把软边补成假锐边,你会看到一张不存在的清晰图

实测(LU01 五张,角色高归一到 900px 后 1:1):

  • 锚图 P0:发丝、睫毛线条分明
  • F1:略软但线条还在(用户原话「F1 的清晰度勉强够」)
  • F2 / F3:明显更软
  • 同一批数据,归一化拉普拉斯方差报 F2 = 1.64× 锚图,即"F2 最清晰" ⇒ 完全反了

1.2 再压到成片尺寸看一次

素材清晰 ≠ 成片可见。必须按成片里角色的实际像素高换算,再判一次要不要返工。

# 例:FRAMING["FULL"] = 0.90,屏幕 1080 高,角色占画面 0.8839
film_h = 0.90 * 1080 / 0.8839        # ≈ 1100 px
# 素材角色高 1589~1778 ⇒ 进片是缩放 0.62~0.74×(缩小)

关键洞察:缩小会掩盖一部分软。 ⇒ 压到成片尺寸后还能看出软,说明进片就真的看得见,必须返工; ⇒ 压下去就分不出了,说明源图的软在成片里被采样吃掉了,可以放行。 这一步是"要不要花钱重出"的唯一依据。


2 ⛔ 三把不可用的尺子(实测数字,别再试)

尺子 它报了什么 它为什么错
归一化拉普拉斯方差 F2 1.64× 锚图、F1 1.37×、F3 1.07×、H1/H2 0.10× 彩度对比撑起方差。高饱和橘发 + 纯绿幕 = 极高方差,与"细节量"无关。同一批里它把最糊的 F2 报成最清晰
径向频谱悬崖(高频/低频能量比) 锚图 0.126 > 现役 0.111~0.123 > F1 0.077 / F3 0.052 锚图 1080×1920 = 2.07 MP,反而比新图 1024×1536 = 1.57 MP 像素更多 ⇒ 它测的是「图有多大」,不是「有多清楚」
内线锐边% / 轮廓带宽 新图 43% vs 锚图 12%,即"新图更锐" 量到的是噪点:新图有键控残留,边缘处的小跳变被算成"锐边"。另一半数字更没法比:轮廓带宽 0.00(真 alpha 通道)vs 1.02(绿幕键控),两种来源口径不可比

三把尺子的共同病根

它们都是单图绝对量,而"糊"是两图相对的判断。 生成图之间的差异(彩度、噪点、边缘来源:真 alpha vs 键控)会盖过真实的锐度差。

⇒ 想做相对判断,两张图必须走完全相同的处理链;否则任何指标比的都是"处理链差异",不是"锐度差异"。

⇒ 想让自动指标可用,唯一的办法是:同一张源图,分别过两条待比较的处理链,再比指标。 (例:原图 vs 原图降采样 1.25× 再升回来 —— 这时指标才真的在测"锐度")


2.5 ⛔ 「有 alpha 通道」≠「是真透明底」

做同尺度归一的第一步是取角色 bbox,而取 bbox 要先有前景 mask。 最容易犯的错是:看到 PNG 有 alpha 就直接 alpha > 200 当 mask。

实测反例(锚图 P0_luna_G見.png):

shape = (1920, 1080, 4)     # 有 4 通道
alpha: min = 255, max = 255, mean = 255.0     # ← 全不透明,形同虚设
四角像素 BGR = (0, 255, 0)                     # ← 真背景是纯绿

⇒ 按 alpha 取前景,得到 bbox = 整个画幅 (0,0,1080,1920), "角色高"被算成 1920(真值 1694), 于是这张图在拼图里被额外缩小 12%,看起来"角色比别人小一圈"—— 一个错误的分割,会伪装成"这张图景别不同"。

正确做法:alpha 里确实存在透明区域时才用 alpha,否则回退绿幕色差。

if alpha is not None and (alpha < 200).mean() > 0.02:
    mask = alpha > 200                       # 真透明底
else:
    mask = ~((G > 90) & ((G - R) > 45) & ((G - B) > 45))   # 绿幕底

兜底:如果 bbox 覆盖 ≥ 95% 画幅,直接报警,别静默出图。 same_head_compare.py 里就有这条,并支持 --force-body-h LABEL=<px> 手动覆盖。

同一条道理的通用版:任何"从图里自动找角色"的步骤,都要有一条"我现在是不是把整幅图当成了角色"的自检。 分割静默失败时不会报错,只会让后面每个数字都错 12%。


3 责任:逐位 diff,不看指标

3.1 第一步永远是「找到生成端的原始档」

⭐ 原始档路径通常就写在你自己的交付/后处理脚本里。

实测案例:_lu01_deliver.py 第 87–93 行的 RAW 字典里,逐条写着 5 张图的 Miora 原始绝对路径:

RAW = {
    'F1': (r'C:/Users/mercy/.workbuddy/plugins/data/mcp-miora/miora-media/miora_edit_image-1793146468626-0-....png', 1080, 1920),
    ...
}

⛔ 我因为没读这个脚本,先下了「最终那批原始档没留」的结论, 再据此编出「F 组被放大 1.03~1.16×」的假根因,两条都被推翻。 一条结论的代价是:后面所有推论都建在沙子上。

行动清单:

grep -n "RAW\|SRC\|INPUT\|\.png" <deliver_script>.py

生成通道的输出目录也要记住(例:~/.workbuddy/plugins/data/mcp-miora/miora-media/)。

3.2 逐位 diff 的正确口径

# 源多半经过补边 / 绿幕归一 ⇒ 先自己复现那一步,再比;否则差异全是那一步造成的
m = fgmask(src_padded) & fgmask(delivered)          # 只在共同前景内统计
inside = np.abs(src_padded.astype(int) - delivered.astype(int)).max(axis=2)[m]
zero_ratio = (inside == 0).mean()                    # ← 这个数字就是判据

⛔ 不要统计全图。背景差异(floodFill 刷纯绿)会淹没角色差异, 实测全图 diff>8 占 76.6%~76.8%,全部落在绿幕上,与角色无关。

实测(LU01,源 → 交付):

图 共同前景 px mean diff p99 max diff==0 占比
H1 667,675 0.096 1.00 75 98.98%
F1 485,501 0.258 11.00 77 97.59%
F2 483,124 0.288 11.00 78 97.32%
F3 486,256 0.256 10.00 76 97.42%

⇒ 97%+ 逐位完全相同 ⇒ 后处理、补边、键控 对角色像素零损伤。糊是生成端带来的。

3.3 顺手要核的:缩放到底有没有发生

看代码分支比看数字可靠。

if src.shape[0] != target_h:
    src = cv2.resize(src, (target_w, target_h), interpolation=cv2.INTER_LANCZOS4)

F 组源 1072×1920、target_h = 1920 ⇒ 根本没进这个分支,只在宽度上左右各补了 4px 绿边(scale_y = 1.0000)。

⇒ 任何"被放大"的说法,先对一遍「源尺寸 vs 交付尺寸」这两个数,别对「角色高比值」—— 角色高会因裁切/补边而变,尺寸不会。


4 反面清单(都是今天真的踩过的)

踩的 真相
拿 LU01-三张_assets/ 里的 1024×1536 当 F 组的源 那 3 张是另一个角色(银白发 + 豹纹兜帽),首轮文生图跑偏的废稿。配对 corr 只有 0.03~0.30、三张都配到同一张 —— 这就是信号
用 hh/ih > 0.85 判"近景" 全身锚图的 bbox 也撑满画幅 ⇒ 误判。别用 bbox 占画幅比例推景别
见到 4 通道就 alpha>200 当前景 生成端常给「纯绿底 + alpha 全 255」。锚图 P0 的 alpha min=max=255,角色高被算成 1920(真值 1694),整张图被额外缩小 12%
认为 1024×1536 是生成端唯一原生尺寸 同一目录里还有 1072×1728 / 1920×1072 / 1072×1920(9:16 原生档) / 1024×1024 / 480×480
交付脚本没留 RAW 映射 事后连"我当时用的是什么源"都查不到。RAW 映射必须写进交付脚本
派单没落盘成 md 事后查不到自己当时写的 prompt,于是"是 prompt 要了柔和、还是模型自己软"永远判不了

5 一张图走完这套流程

1. 症状  同头宽 1:1 并排 → 肉眼排序           (只信眼睛)
2. 成片  压到成片角色高 → 再排一次             (决定要不要返工)
3. 责任  grep 交付脚本找原始档 → 复现补边/键控 → 前景内逐位 diff
4. 边界  能判到哪说到哪。原始档确实不存在 ⇒ 明说判不了,⛔ 不编根因

第 4 步不是免责声明,是硬要求。 「生成端本来就软」和「后处理弄糊的」这两条结论,在拿到原始档之前一条都不能拿出来。


6 脚本

脚本 干什么
scripts/same_head_compare.py 多图 → 按角色高归一 → 原像素 1:1 并排拼图(步骤 1)。前景分割失效时自动报警,可用 --force-body-h LABEL=PX 手动覆盖
scripts/pixel_forensics.py 源 vs 交付 → 前景内逐位 diff + 尺寸/缩放检测(步骤 3)

两个都零依赖(numpy + opencv),中文路径统一走 np.fromfile + cv2.imdecode。

# 步骤 1:症状
python scripts/same_head_compare.py --out cmp.png --body-h 900 --tile-w 240 \
    --img "锚图=poses_p0/P0.png" --img "F1=out/LU01_F1.png" --img "F2=out/LU01_F2.png"

# 步骤 3:责任
python scripts/pixel_forensics.py \
    --src ~/.workbuddy/plugins/data/mcp-miora/miora-media/miora_edit_image-XXXX.png \
    --del out/LU01_F1.png --pad-w 1080
# → fg diff==0 ratio: 97.59%     VERDICT: 后处理对角色像素零损伤 ⇒ 糊来自生成端

Licence

MIT。见 LICENSE。