修: 对齐修好了配对,判决却用旧配对的帧占比(0912 洞见 假拒绝)

洞见在 crf 24 上被判 'mean 0.9974 但 20/1199 帧低于 0.85' → 拒。实测: 0.9974/0.9936 是对齐后的数字,
而 20 帧是直配的旧计数 —— 两个数字来自不同配对。三向对照测试(本地,同一 60 s 窗口):
  健康对     min 0.9940 mean 0.9971 0 坏帧 → 走快路径(无对齐搜索),通过 ✓
  错位对     'alignment repaired the pairing: 20 frame(s) below 0.85 before, 0 after'
             min 0.9940 mean 0.9972 1199 帧 0 坏帧 → 通过 ✓ ← 与洞见判决书 20/1199 逐项吻合
  真缺陷对   mean 0.9612 151 帧(10.07%) → 仍被拒 ✓(没变橡皮章)
改动:
  - 触发条件补上帧占比: 原来只看 min<0.985 / mean<0.995,帧占比超标不触发搜索;
  - _paired_ssim / _perframe_best 现在返回 below 计数,并同样剔除近平坦帧(之前对齐后的数字
    含黑场帧,会让对齐看起来比直配还差);
  - 采用对齐结果时同步更新 stats['below'](判决用的就是它),并打印 'X frames below before, Y after'。
This commit is contained in:
2026-10-12 21:44:57 +08:00
parent eb4b0bd26f
commit 767fa3ac08
+48 -12
View File
@@ -742,7 +742,12 @@ def ssim_vs_source(src_local, out_local, info, workdir, tag, dur=SSIM_WINDOW):
# 2. coarse, STEP 10 over +-100 - unchanged, for a file that has drifted by tens of frames;
# 3. per-frame best match over +-ALIGN_PERFRAME, for a window whose displacement varies.
used_off = 0 # the alignment the VMAF observation below is measured at
if n and (lo is None or lo < SSIM_TARGET or mean is None or mean < SSIM_MEAN_MIN):
# Trigger on the FRAME-SHARE as well, not just min/mean: a window can pair wrongly for a stretch
# of frames while its min stays above SSIM_TARGET, and then the share is the only symptom. That
# was 0912 洞见 (20 of 1199 frames below 0.85 - refused on the share, never searched).
direct_below = bad
if n and (lo is None or lo < SSIM_TARGET or mean is None or mean < SSIM_MEAN_MIN
or bad > MAX_BAD_SHARE * n):
best = None
nf = min(n, 1200)
pid = os.getpid() # two lanes share the stage dir: a fixed tag let them overwrite
@@ -751,9 +756,10 @@ def ssim_vs_source(src_local, out_local, info, workdir, tag, dur=SSIM_WINDOW):
for off in offs:
if off == 0:
continue
mn, mm, k = _paired_ssim(ref, out, off, nf, workdir, "%d%s%d" % (pid, stage, abs(off)))
mn, mm, k, bl = _paired_ssim(ref, out, off, nf, workdir,
"%d%s%d" % (pid, stage, abs(off)), flat)
if mn is not None and (best is None or mn > best[0]):
best = (mn, mm, k, off, stage)
best = (mn, mm, k, off, stage, bl)
if best and best[0] >= 0.90 and best[1] >= 0.98:
break # the fine stage already explains it; don't pay for the rest
if best and best[0] >= 0.90 and best[1] >= 0.98:
@@ -767,12 +773,24 @@ def ssim_vs_source(src_local, out_local, info, workdir, tag, dur=SSIM_WINDOW):
% (best[3], best[4]))
lo, mean, n = best[0], best[1], best[2]
used_off = best[3]
# The share MUST come from the same pairing as the min/mean that were just adopted.
# Leaving the direct count in place refused an encode whose pictures had already been
# verified at the aligned offset (0912 洞见: min 0.9936 mean 0.9974 after realignment,
# refused by "20 of 1199 frames below 0.85" from the WRONG pairing).
if best[5] is not None:
stats["below"] = best[5]
log(" alignment repaired the pairing: %d frame(s) below %.2f before, %s after"
% (direct_below, BAD_FRAME_SSIM, stats.get("below")))
else:
per = _perframe_best(ref, out, nf, workdir, "%d" % pid)
per = _perframe_best(ref, out, nf, workdir, "%d" % pid, flat)
if per and per[1] >= 0.90 and per[2] >= 0.98:
stats["align_note"] = ("per-frame best match (+-%d frames) - displacement varies "
"inside the window" % ALIGN_PERFRAME)
lo, mean, n = per[1], per[2], per[0]
if per[3] is not None:
stats["below"] = per[3]
log(" per-frame realignment: %d frame(s) below %.2f before, %s after"
% (direct_below, BAD_FRAME_SSIM, stats.get("below")))
else:
log(" no alignment explains the mismatch (best constant offset %s,"
" per-frame best-match %s) - keeping the direct-pairing verdict"
@@ -901,18 +919,34 @@ def vmaf_of_pair(ref_raw, out_raw, off, nf, workdir, tag):
pass
def _paired_ssim(ref_raw, out_raw, off, nf, workdir, tag):
"""(min, mean, frames) for one constant offset - see `_ssim_series` for the sign convention."""
def _paired_ssim(ref_raw, out_raw, off, nf, workdir, tag, flat=()):
"""(min, mean, frames, below) for one constant offset - see `_ssim_series` for the sign.
`flat` holds the indices of near-flat source frames; they are dropped here exactly as the direct
pairing drops them, so the alignment is judged on the same frame set as the verdict that follows
(without this a black fade dragged the aligned mean below the direct one and the search looked
useless on files that were merely dark).
`below` is what the frame-share test actually consumes - returning only min/mean meant a repaired
alignment still got refused by the share count taken from the WRONG pairing.
"""
vals = _ssim_series(ref_raw, out_raw, off, nf, workdir, tag)
return (min(vals), sum(vals) / len(vals), len(vals)) if vals else (None, None, 0)
base = max(0, off)
keep = [v for i, v in enumerate(vals) if (base + i) not in flat]
if not keep:
return (None, None, 0, 0)
return (min(keep), sum(keep) / len(keep), len(keep),
sum(1 for v in keep if v < BAD_FRAME_SSIM))
def _perframe_best(ref_raw, out_raw, nf, workdir, tag):
"""(frames, min, mean) where each frame scores against the best of its +-ALIGN_PERFRAME
neighbours.
def _perframe_best(ref_raw, out_raw, nf, workdir, tag, flat=()):
"""(frames, min, mean, below) where each frame scores against the best of its +-ALIGN_PERFRAME
neighbours, over the non-flat frames only.
For a window whose displacement is not one constant shift. A wrong-picture defect cannot be
rescued by this - such frames have no good neighbour - while a duplicate-frame stretch can.
rescued by this - such frames have no good neighbour - while a duplicate-frame stretch can, and
that is precisely the shape a false frame-share refusal has (measured 2026-10-12: 0912 洞见
scored 20 of 1199 frames below 0.85 with a 0.9974 mean by direct pairing; shifting the encode by
ONE frame produced 22 such frames on the same content and a correct pairing produced none).
"""
per = {}
for off in range(-ALIGN_PERFRAME, ALIGN_PERFRAME + 1):
@@ -920,12 +954,14 @@ def _perframe_best(ref_raw, out_raw, nf, workdir, tag):
base = max(0, off)
for i, v in enumerate(vals):
j = base + i
if j in flat:
continue
if v > per.get(j, -1.0):
per[j] = v
if not per:
return None
s = [per[k] for k in sorted(per)]
return len(s), min(s), sum(s) / len(s)
return len(s), min(s), sum(s) / len(s), sum(1 for v in s if v < BAD_FRAME_SSIM)
def pin_to_e_cores(mask=None):