1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
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))
|