diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-28 18:48:05 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-28 18:48:05 +0800 |
| commit | d3a2604e30b1deb91afebf550a79cde71bbca6d6 (patch) | |
| tree | 7cf10505e76f5a6749faadb96ff4d678a3c8fb26 /scripts/analyze-p42-frames.py | |
| parent | 323552b7e49550d378d3871fadcb711c16c71fed (diff) | |
| download | living-village-d3a2604e30b1deb91afebf550a79cde71bbca6d6.tar.gz | |
p42: 启动/菜单水面涟漪整改 — 振幅0.32+波峰列, 帧差14.3, 防降振回归 (leader fix: 提交+推送)
Diffstat (limited to 'scripts/analyze-p42-frames.py')
| -rw-r--r-- | scripts/analyze-p42-frames.py | 106 |
1 files changed, 106 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)) |
