From cbd89dbff21a66725132eaf1c37e2fb870e72f8d Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Tue, 29 Sep 2026 02:05:39 +0800 Subject: p57: 菜单/启动水面第二层高光波光——逐列相位错开(步进2.4rad)/更快(周期80 vs 96 tick)/更亮更窄细高光条 + 昼夜 tint 衰减参数 + 帧计数纯函数回归;ANALYZE splash hl/main std=274.97 corr=-0.017、menu hl/main std=279.77 corr=-0.011、层间相关<0.95、相位差2.4rad、无黑块白块 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/analyze-p57.py | 173 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 scripts/analyze-p57.py (limited to 'scripts') diff --git a/scripts/analyze-p57.py b/scripts/analyze-p57.py new file mode 100644 index 0000000..6cf3097 --- /dev/null +++ b/scripts/analyze-p57.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +"""P57 menu/splash second water highlight layer analysis. + +Reads the `LV_P57_SHOT=1` frames (same pinned splash/menu frame captured once with +the highlight layer off, once on) plus /log.txt, and writes +docs/evidence/p57-analysis.txt with: + + * per-layer column profile std (main water band brightness vs highlight alpha) + and their correlation, from the logged pure-function arrays; + * adjacent-column phase steps for both layers (highlight must be phase-shifted + and move at a slightly different rate); + * pixel diff of base vs highlight frames (the second layer must be visible and + confined to the water band), and a no-big-black/white-block sanity check. + +Hard verdict: + highlight std >= main std * 0.3, layer correlation < 0.95, phase step > 0, + pixel diff > threshold inside the water band, no large pure black/white blocks. + +Usage: + python3 scripts/analyze-p57.py +Exit 0 on PASS, 1 on FAIL, 2 on usage/read error. +""" + +import re +import sys +from pathlib import Path + +import numpy as np +from PIL import Image + +STATUS_RE = re.compile( + r"p57-status=(\S+) pendingName=(\S+) disabled=(true|false) frame=(\d+) " + r"main=([\d.,\-]+) hl=([\d.,\-]+) mainphase=([\d.,\-]+) hlphase=([\d.,\-]+)" +) +HIGHLIGHT_STD_RATIO_MIN = 0.3 +LAYER_CORR_MAX = 0.95 +PIXEL_DIFF_MIN = 20.0 +TOP_CLEAN_DIFF_MAX = 20.0 +PURE_BLACK_MAX_FRACTION = 0.02 +PURE_WHITE_MAX_FRACTION = 0.05 +WATER_ROWS = (624, 656) +TILE = 32 + + +def fail(lines, out_txt, message): + lines.append(f"FAIL: {message}") + lines.append("") + lines.append("verdict: FAIL") + text = "\n".join(lines) + "\n" + if out_txt is not None: + out_txt.parent.mkdir(parents=True, exist_ok=True) + out_txt.write_text(text, encoding="utf-8") + print(text) + return 1 + + +def load(path): + return np.asarray(Image.open(path).convert("RGB"), dtype=np.float64) + + +def parse_floats(text): + return [float(v) for v in text.split(",")] + + +def main(argv): + if len(argv) != 3: + sys.stderr.write(__doc__) + return 2 + record_dir = Path(argv[1]) + out_txt = Path(argv[2]) + log_path = record_dir / "log.txt" + if not log_path.exists(): + sys.stderr.write(f"missing {log_path}\n") + return 2 + + lines = ["P57 second water highlight layer analysis (LV_P57_SHOT=1)", ""] + statuses = {} + for line in log_path.read_text(encoding="utf-8", errors="replace").splitlines(): + match = STATUS_RE.search(line) + if match: + statuses[match.group(1)] = { + "disabled": match.group(3) == "true", + "frame": int(match.group(4)), + "main": parse_floats(match.group(5)), + "hl": parse_floats(match.group(6)), + "mainphase": parse_floats(match.group(7)), + "hlphase": parse_floats(match.group(8)), + } + + wanted = [ + ("splash", "p57-splash-base", "p57-splash-highlight"), + ("menu", "p57-menu-base", "p57-menu-highlight"), + ] + for label, base_name, hl_name in wanted: + if base_name not in statuses or hl_name not in statuses: + return fail(lines, out_txt, f"missing log status for {base_name}/{hl_name}") + base_status = statuses[base_name] + hl_status = statuses[hl_name] + if not base_status["disabled"] or hl_status["disabled"]: + return fail(lines, out_txt, f"{label}: base must be disabled, highlight must be enabled") + if base_status["frame"] != hl_status["frame"]: + return fail(lines, out_txt, f"{label}: both captures must pin the same frame") + + main = np.array(hl_status["main"], dtype=np.float64) + hl = np.array(hl_status["hl"], dtype=np.float64) + main_phase = np.array(hl_status["mainphase"], dtype=np.float64) + hl_phase = np.array(hl_status["hlphase"], dtype=np.float64) + main_std = float(main.std()) + hl_std = float(hl.std()) + if hl_std <= 0.0 or main_std <= 0.0: + return fail(lines, out_txt, f"{label}: column profiles must vary") + corr = float(np.corrcoef(main, hl)[0, 1]) + main_step = float(main_phase[1] - main_phase[0]) + hl_step = float(hl_phase[1] - hl_phase[0]) + lines += [ + f"[{label}] frame={hl_status['frame']} columns={len(main)}", + f"[{label}] main column std = {main_std:.4f}, highlight column std = {hl_std:.4f}", + f"[{label}] highlight std / main std = {hl_std / main_std:.3f} (>= {HIGHLIGHT_STD_RATIO_MIN})", + f"[{label}] layer correlation = {corr:.4f} (< {LAYER_CORR_MAX})", + f"[{label}] adjacent phase step: main = {main_step:.4f} rad, highlight = {hl_step:.4f} rad", + ] + if hl_std < main_std * HIGHLIGHT_STD_RATIO_MIN: + return fail(lines, out_txt, f"{label}: highlight std {hl_std:.4f} < main std * {HIGHLIGHT_STD_RATIO_MIN}") + if not (corr < LAYER_CORR_MAX): + return fail(lines, out_txt, f"{label}: layer correlation {corr:.4f} is not < {LAYER_CORR_MAX}") + if abs(hl_step) < 1e-3: + return fail(lines, out_txt, f"{label}: adjacent highlight columns are not phase-shifted") + + base_img = load(record_dir / f"{base_name}.png") + hl_img = load(record_dir / f"{hl_name}.png") + if base_img.shape != hl_img.shape: + return fail(lines, out_txt, f"{label}: frame sizes differ") + diff = np.abs(hl_img - base_img).mean(axis=2) + band = diff[WATER_ROWS[0]:WATER_ROWS[1] + TILE, :] + top = diff[:300, :] + band_mean = float(band.mean()) + band_max = float(band.max()) + band_pixels = int((band > 10.0).sum()) + top_max = float(top.max()) + lines += [ + f"[{label}] pixel diff: water band mean={band_mean:.3f} max={band_max:.1f} " + f"pixels>10={band_pixels}, top max={top_max:.1f}", + ] + if band_max < 20.0 or band_pixels < 50: + return fail(lines, out_txt, f"{label}: the highlight layer is not visible in the water band") + if top_max > 20.0: + return fail(lines, out_txt, f"{label}: highlight leaked outside the water band") + + for tag, img in (("base", base_img), ("highlight", hl_img)): + std = float(img.std()) + black = float((img.max(axis=2) <= 1.0).mean()) + white = float((img.min(axis=2) >= 254.0).mean()) + lines.append( + f"[{label}] {tag}: std={std:.1f} pure_black={black:.4%} pure_white={white:.4%}" + ) + if std < 5.0: + return fail(lines, out_txt, f"{label} {tag}: frame looks blank") + if black > PURE_BLACK_MAX_FRACTION: + return fail(lines, out_txt, f"{label} {tag}: large pure-black block ({black:.2%})") + if white > PURE_WHITE_MAX_FRACTION: + return fail(lines, out_txt, f"{label} {tag}: large pure-white block ({white:.2%})") + lines.append("") + + lines.append("verdict: PASS") + text = "\n".join(lines) + "\n" + out_txt.parent.mkdir(parents=True, exist_ok=True) + out_txt.write_text(text, encoding="utf-8") + print(text) + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) \ No newline at end of file -- cgit v1.2.3