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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
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))
|