summaryrefslogtreecommitdiff
path: root/scripts/analyze-p57.py
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-29 02:05:39 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-29 02:05:39 +0800
commitcbd89dbff21a66725132eaf1c37e2fb870e72f8d (patch)
tree0696454739e3479ecb697d000c02841c06d7319e /scripts/analyze-p57.py
parent1a67f2bf57e58ef748e71e1bcc2affe901444c39 (diff)
downloadliving-village-cbd89dbff21a66725132eaf1c37e2fb870e72f8d.tar.gz
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、无黑块白块
Diffstat (limited to 'scripts/analyze-p57.py')
-rw-r--r--scripts/analyze-p57.py173
1 files changed, 173 insertions, 0 deletions
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 <record-dir>/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 <record-dir> <out-txt>
+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