summaryrefslogtreecommitdiff
path: root/scripts/analyze-p42-frames.py
blob: af6aa387498a691a8cce26b9f6a1ec28e8c05598 (plain)
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))