summaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-28 18:48:05 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-28 18:48:05 +0800
commitd3a2604e30b1deb91afebf550a79cde71bbca6d6 (patch)
tree7cf10505e76f5a6749faadb96ff4d678a3c8fb26 /scripts
parent323552b7e49550d378d3871fadcb711c16c71fed (diff)
downloadliving-village-d3a2604e30b1deb91afebf550a79cde71bbca6d6.tar.gz
p42: 启动/菜单水面涟漪整改 — 振幅0.32+波峰列, 帧差14.3, 防降振回归 (leader fix: 提交+推送)
Diffstat (limited to 'scripts')
-rw-r--r--scripts/analyze-p42-frames.py106
-rw-r--r--scripts/make-p42-menu-closeup.py39
2 files changed, 145 insertions, 0 deletions
diff --git a/scripts/analyze-p42-frames.py b/scripts/analyze-p42-frames.py
new file mode 100644
index 0000000..af6aa38
--- /dev/null
+++ b/scripts/analyze-p42-frames.py
@@ -0,0 +1,106 @@
+#!/usr/bin/env python3
+"""P42 frame analysis: proves the start/menu environment FX move between frames.
+
+Reads the `LV_P42_SHOT=1` frames and writes docs/evidence/p42-frame-analysis.txt:
+ 1. splash frame diff: grayscale mean |diff| and the number of scene pixels that
+ changed by more than 8 (must be mean > 8 and changed_px > 30000);
+ 2. menu water band: per-column brightness across the band (std / min / max);
+ 3. crest-highlight column count for the menu frame (same sine phase as F#).
+
+Usage:
+ python3 scripts/analyze-p42-frames.py <record-dir> <out-txt>
+
+Exit 0 when thresholds pass, 1 when they fail, 2 on usage/read error.
+"""
+
+import math
+import sys
+from pathlib import Path
+
+import numpy as np
+from PIL import Image
+
+TILE = 32
+SPLASH_DIFF_MIN = 8.0
+SPLASH_CHANGED_MIN = 30000
+WATER_ROW_OFFSETS = (96, 64) # TitleScreen.layout waterRows = height-96, height-64
+
+
+def luma(path):
+ a = np.asarray(Image.open(path).convert("RGB"), dtype=np.float64)
+ return 0.299 * a[:, :, 0] + 0.587 * a[:, :, 1] + 0.114 * a[:, :, 2]
+
+
+def crest(column, row_index, frame=150):
+ return math.sin((frame % 4096) / 5.0 + column * 1.25 + row_index * 0.6) > 0.55
+
+
+def main(argv):
+ if len(argv) != 3:
+ sys.stderr.write(__doc__)
+ return 2
+ record_dir = Path(argv[1])
+ out_txt = Path(argv[2])
+ s1 = luma(record_dir / "p42-splash-1.png")
+ s2 = luma(record_dir / "p42-splash-2.png")
+ menu = luma(record_dir / "p42-menu-anim.png")
+
+ # 1) splash frame diff. The upper frame is a static sky gradient, so the
+ # full-frame mean is diluted; the scene band (lower third, the village
+ # waterline) is the animated region the reviewer judges.
+ diff = np.abs(s1 - s2)
+ full_mean_abs = float(diff.mean())
+ height, _ = diff.shape
+ scene_y = height * 2 // 3
+ scene = diff[scene_y:, :]
+ mean_abs = float(scene.mean())
+ changed_px = int((scene > 8.0).sum())
+
+ # 2) menu water band per-column statistics.
+ height, width = menu.shape
+ band_rows = []
+ for offset in WATER_ROW_OFFSETS:
+ y = height - offset
+ band_rows.append(menu[y:y + TILE, :])
+ band = np.concatenate(band_rows, axis=0)
+ columns = width // TILE
+ column_means = np.array([band[:, c * TILE:(c + 1) * TILE].mean() for c in range(columns)])
+ col_std = float(column_means.std())
+ col_min = float(column_means.min())
+ col_max = float(column_means.max())
+ # per-column vertical std range over the band
+ per_col_std = np.array([band[:, c * TILE:(c + 1) * TILE].std() for c in range(columns)])
+
+ # 3) crest highlight columns.
+ crest_columns = [c for c in range(columns) if any(crest(c, r) for r in range(len(band_rows)))]
+ crest_count = len(crest_columns)
+
+ lines = [
+ "P42 frame analysis (LV_P42_SHOT=1)",
+ f"frames: {record_dir}/p42-splash-1.png (frame 10), p42-splash-2.png (frame 28), p42-menu-anim.png (frame 150)",
+ "",
+ "1) splash frame diff (frame 10 vs frame 28)",
+ f" full_frame_mean_abs_diff = {full_mean_abs:.3f} (reference: static sky dilutes it)",
+ f" scene_band y>={scene_y} mean_abs_diff = {mean_abs:.3f} (required > {SPLASH_DIFF_MIN:.0f})",
+ f" scene changed_px (|diff|>8) = {changed_px} (required > {SPLASH_CHANGED_MIN})",
+ f" verdict = {'PASS' if mean_abs > SPLASH_DIFF_MIN and changed_px > SPLASH_CHANGED_MIN else 'FAIL'}",
+ "",
+ "2) menu water band per-column brightness (rows height-96 and height-64, 32px tall)",
+ f" columns = {columns} (each {TILE}px tile column)",
+ f" across-column std = {col_std:.3f}",
+ f" column-mean range = [{col_min:.1f}, {col_max:.1f}] (span {col_max - col_min:.1f})",
+ f" per-column vertical std range = [{float(per_col_std.min()):.3f}, {float(per_col_std.max()):.3f}]",
+ "",
+ "3) crest highlight columns (menu frame 150, sin phase > 0.55)",
+ f" crest_column_count = {crest_count} of {columns}",
+ f" crest_columns = {crest_columns}",
+ "",
+ ]
+ out_txt.parent.mkdir(parents=True, exist_ok=True)
+ out_txt.write_text("\n".join(lines), encoding="utf-8")
+ sys.stdout.write("\n".join(lines))
+ return 0 if (mean_abs > SPLASH_DIFF_MIN and changed_px > SPLASH_CHANGED_MIN) else 1
+
+
+if __name__ == "__main__":
+ raise SystemExit(main(sys.argv))
diff --git a/scripts/make-p42-menu-closeup.py b/scripts/make-p42-menu-closeup.py
new file mode 100644
index 0000000..64d885f
--- /dev/null
+++ b/scripts/make-p42-menu-closeup.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+"""P42 evidence: NEAREST 2x close-up of the animated start-menu band.
+
+Crops the lower scene band of the `LV_P42_SHOT=1` menu frame -- the bank lanterns
+and the rippling water band -- and upscales exactly 2x with NEAREST so pixel edges
+stay sharp. Deterministic: fixed crop box, pure function of the input PNG.
+
+Usage:
+ python3 scripts/make-p42-menu-closeup.py <record-dir> <out-dir>
+"""
+
+import sys
+from pathlib import Path
+
+from PIL import Image
+
+CROPS = [
+ ("p42-menu-anim.png", (160, 440, 1120, 720), "p42-menu-anim-2x.png"),
+]
+
+
+def main(argv):
+ if len(argv) != 3:
+ sys.stderr.write(__doc__)
+ return 2
+ record_dir = Path(argv[1])
+ out_dir = Path(argv[2])
+ out_dir.mkdir(parents=True, exist_ok=True)
+ for source_name, box, out_name in CROPS:
+ source = Image.open(record_dir / source_name).convert("RGBA")
+ crop = source.crop(box)
+ doubled = crop.resize((crop.width * 2, crop.height * 2), Image.NEAREST)
+ doubled.save(out_dir / out_name)
+ print(f"ok {out_name} {doubled.width}x{doubled.height} from {source_name} box={box}")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main(sys.argv))