gate: judge the DEFECT SHAPE, and ignore frames where SSIM means nothing
The gate failed a perfectly good encode (min 0.2552, mean 0.9841) because it demanded a per-frame floor of 0.985 for EVERY frame. Two measured facts say that is the wrong test: * SSIM collapses on near-flat frames. Two almost-black frames of a fade score ~0.25 while their neighbours score 0.998, so a minimum over all frames condemns good encodes. The gate now measures each source frame's luma std and drops the flat ones from the verdict (reported, never hidden; if more than half the window is flat the exclusion is skipped so the gate still means something). * A defect has a SHAPE. Measured distributions on this library: good encodes 0 of 1500 frames below 0.95; the file that triggered this 5 of 1500 below 0.85 (0.33%, isolated) at a 0.984 mean; the genuine decoder-defect files 44 of 1499 (2.9%) with the mean falling. So the verdict is now 'mean < 0.98 OR more than 1% of frames below 0.85' -> fail; a handful of scattered low frames at a healthy mean is logged as fades, not a defect - and the log line carries the counts, so the decision is auditable.
This commit is contained in:
+64
-8
@@ -57,6 +57,33 @@ X265_POOLS = 10 # E-cores only (affinity 0x3FF0000)
|
||||
E_CORE_MASK = 0x3FF0000
|
||||
SSIM_TARGET = 0.985 # per-frame floor vs the source window (480x270 comparison)
|
||||
SSIM_WINDOW = 60 # seconds sampled for the SSIM gate
|
||||
BAD_FRAME_SSIM = 0.85 # a frame this far below the source is 'bad' for the share test
|
||||
MAX_BAD_SHARE = 0.01 # ... and more than this share of them means a systematic defect, not fades
|
||||
FLAT_LUMA_STD = 3.0 # a source frame flatter than this: SSIM on it means nothing
|
||||
|
||||
|
||||
def _flat_frames(raw_path, n, w=480, h=270, thresh=FLAT_LUMA_STD):
|
||||
"""Indices of near-flat source frames (fades, black, plain titles) in a raw YUV window.
|
||||
|
||||
SSIM collapses on such frames - two nearly black frames score ~0.25 while their neighbours
|
||||
score 0.998 - so a minimum taken over every frame condemns perfectly good encodes (measured on
|
||||
this library: the file that failed had 5 of 1500 frames below 0.85, all isolated, at a 0.98
|
||||
mean, while the genuine decoder-defect files had 44 of 1499).
|
||||
"""
|
||||
import numpy as np
|
||||
fsz = w * h * 3 // 2
|
||||
flat = set()
|
||||
with open(raw_path, "rb") as f:
|
||||
for i in range(n):
|
||||
luma = f.read(w * h)
|
||||
if len(luma) < w * h:
|
||||
break
|
||||
if float(np.frombuffer(luma, dtype=np.uint8).std()) < thresh:
|
||||
flat.add(i)
|
||||
f.seek(fsz - w * h, 1)
|
||||
return flat
|
||||
|
||||
|
||||
SIZE_CEIL = 1.0 # output video bitrate must not exceed the source's
|
||||
QUALITY_STEPS = 2 # on a quality-gate failure, retry with the quality raised this many
|
||||
# steps (cq -2 / crf -2) before giving up; the retry only fires on a
|
||||
@@ -306,7 +333,7 @@ def ssim_vs_source(src_local, out_local, info, workdir, tag, dur=SSIM_WINDOW):
|
||||
"-map", "0:v:0", "-an", "-vf", vf,
|
||||
"-fps_mode", "passthrough", "-f", "rawvideo", dstp], timeout=7200)
|
||||
if r.returncode != 0 or not os.path.exists(dstp) or not os.path.getsize(dstp):
|
||||
return None, None, 0, None
|
||||
return None, None, 0, None, {}
|
||||
counts.append(os.path.getsize(dstp) // (480 * 270 * 3 // 2))
|
||||
n_src, n_out = counts
|
||||
if n_src and abs(n_out - n_src) / float(n_src) > 0.01:
|
||||
@@ -318,7 +345,7 @@ def ssim_vs_source(src_local, out_local, info, workdir, tag, dur=SSIM_WINDOW):
|
||||
return None, None, 0, ("source timeline irregular: %d frames vs %d in the sampled window "
|
||||
"(%.1f%% difference) - the CFR encode fills the source's holes, so a "
|
||||
"frame-by-frame comparison is not applicable" % (n_src, n_out,
|
||||
100.0 * (n_out - n_src) / n_src))
|
||||
100.0 * (n_out - n_src) / n_src)), {}
|
||||
subprocess.run([FFMPEG, "-y", "-v", "error", "-f", "rawvideo", "-pix_fmt", "yuv420p", "-s", cmp_scale,
|
||||
"-i", ref, "-f", "rawvideo", "-pix_fmt", "yuv420p", "-s", cmp_scale, "-i", out,
|
||||
"-lavfi", "ssim=stats_file=ssim_%s.log" % tag, "-f", "null", "-"],
|
||||
@@ -332,8 +359,23 @@ def ssim_vs_source(src_local, out_local, info, workdir, tag, dur=SSIM_WINDOW):
|
||||
if m:
|
||||
vals.append(float(m.group(1)))
|
||||
n = len(vals)
|
||||
# SSIM is meaningless on a nearly flat frame: two almost-black frames of a fade scored 0.25
|
||||
# while their neighbours scored 0.998, so a MINIMUM over every frame condemns good encodes.
|
||||
# Measure the source frames' flatness and drop them from the verdict (reported, not hidden).
|
||||
flat = set()
|
||||
try:
|
||||
flat = _flat_frames(ref, n)
|
||||
except Exception as e: # numpy missing / unreadable raw: fall back to using every frame
|
||||
log(" ! flat-frame scan skipped: %s" % e)
|
||||
if flat and len(flat) < n * 0.5:
|
||||
vals = [v for i, v in enumerate(vals) if i not in flat]
|
||||
log(" %d near-flat source frame(s) excluded from the picture verdict (SSIM is "
|
||||
"meaningless on them)" % len(flat))
|
||||
n = len(vals)
|
||||
lo = min(vals) if vals else None
|
||||
mean = (sum(vals) / len(vals)) if vals else None
|
||||
bad = sum(1 for v in vals if v < BAD_FRAME_SSIM)
|
||||
stats = {"below": bad, "flat": len(flat), "total": n}
|
||||
note = None
|
||||
# If the direct pairing looks bad, try to EXPLAIN the mismatch as a timeline offset before
|
||||
# condemning the file: a source whose timeline drifts (or had a hole earlier) makes the encode
|
||||
@@ -357,7 +399,7 @@ def ssim_vs_source(src_local, out_local, info, workdir, tag, dur=SSIM_WINDOW):
|
||||
os.remove(f)
|
||||
except OSError:
|
||||
pass
|
||||
return lo, mean, n, note
|
||||
return lo, mean, n, note, stats
|
||||
|
||||
|
||||
def _paired_ssim(ref_raw, out_raw, off, nf, workdir, tag):
|
||||
@@ -535,7 +577,7 @@ def process(src, args, dest=None, origin=None):
|
||||
# that the output video bitrate did not exceed the source's.
|
||||
ssim_note, growth = "copy (video bit-identical)", 100.0
|
||||
if tier != "copy":
|
||||
lo, mean, nfr, irreg = ssim_vs_source(loc_src, loc_out, info, STAGE, tag)
|
||||
lo, mean, nfr, irreg, sstat = ssim_vs_source(loc_src, loc_out, info, STAGE, tag)
|
||||
obps = out_video_bps(loc_out, info["dur"])
|
||||
growth = 100.0 * obps / vbps if vbps else 0.0
|
||||
if irreg:
|
||||
@@ -552,13 +594,27 @@ def process(src, args, dest=None, origin=None):
|
||||
return "FAIL ssim: no frames compared", None
|
||||
ssim_note = "ssim min %.4f mean %.4f (%d frames)%s" % (
|
||||
lo, mean, nfr, "" if not q_off else " at quality+%d" % q_off)
|
||||
if lo < SSIM_TARGET:
|
||||
# The verdict is a DEFECT SHAPE, not a single frame. A systematic mismatch drops the
|
||||
# mean and puts a real share of frames below the floor; a handful of scattered low
|
||||
# frames at a healthy mean is what fades, black frames and hard cuts do to SSIM.
|
||||
# Measured on this library: good encodes 0 of 1500 frames below 0.95; the file that
|
||||
# triggered this rule 5 of 1500 below 0.85 (0.33%) at 0.98 mean; the genuine
|
||||
# decoder-defect files 44 of 1499 (2.9%) with the mean falling too.
|
||||
bad_share = (sstat.get("below", 0) / float(nfr)) if nfr else 0.0
|
||||
systematic = (mean is not None and mean < 0.98) or bad_share > MAX_BAD_SHARE
|
||||
if systematic:
|
||||
if q_off < QUALITY_STEPS and (attempt == 1):
|
||||
q_off = QUALITY_STEPS
|
||||
log(" quality gate %.4f < %.3f - retrying at quality+%d" %
|
||||
(lo, SSIM_TARGET, q_off))
|
||||
log(" quality gate: mean %.4f, %d of %d frames below %.2f (%.2f%%) - "
|
||||
"retrying at quality+%d" % (mean, sstat.get("below", 0), nfr,
|
||||
BAD_FRAME_SSIM, 100.0 * bad_share, q_off))
|
||||
continue
|
||||
return "FAIL quality: %s < target %.3f" % (ssim_note, SSIM_TARGET), None
|
||||
return ("FAIL quality: %s - systematic: mean %.4f, %.2f%% of frames below %.2f"
|
||||
% (ssim_note, mean, 100.0 * bad_share, BAD_FRAME_SSIM)), None
|
||||
if lo is not None and lo < SSIM_TARGET:
|
||||
log(" %d scattered frame(s) below %.2f (%.2f%% of the window) with a healthy "
|
||||
"mean - treated as fades/black, not a defect"
|
||||
% (sstat.get("below", 0), BAD_FRAME_SSIM, 100.0 * bad_share))
|
||||
if vbps and obps > vbps * SIZE_CEIL:
|
||||
return "FAIL size: output %.0f kbps > source %.0f kbps (%.1f%%)" % (
|
||||
obps / 1000, vbps / 1000, growth), None
|
||||
|
||||
Reference in New Issue
Block a user