diff options
Diffstat (limited to 'scripts')
| -rw-r--r-- | scripts/analyze-p47.py | 173 |
1 files changed, 173 insertions, 0 deletions
diff --git a/scripts/analyze-p47.py b/scripts/analyze-p47.py new file mode 100644 index 0000000..ae9ade1 --- /dev/null +++ b/scripts/analyze-p47.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +"""P47 scene-detail analysis. + +Reads the `LV_P47_SHOT=1` day/night/closeup frames + <record-dir>/log.txt and writes +docs/evidence/p47-analysis.txt with: + + 1. reflection luma correlation (bank column profile vs vertically-mirrored water + band) and reflection-vs-control-water luma delta; + 2. element-count metrics (bank reeds / floating props / reflection streaks); + 3. no big black block (min 64px square) / no white block / opaque frames. + +Usage: + python3 scripts/analyze-p47.py <record-dir> <out-txt> +""" + +import re +import sys +from pathlib import Path + +import numpy as np +from PIL import Image + +TILE = 32 +MIN_BLOCK = 64 +REFLECTION_CORR_MIN = 0.20 +REFLECTION_DELTA_MIN = 10.0 +REFLECTION_RE = re.compile(r"p47-reflection src=(\d+),(\d+) dst=(\d+),(\d+) alpha=(\d+)") +ELEMENTS_RE = re.compile(r"p47-elements reeds=(\d+) floats=(\d+) reflections=(\d+) sources=(\d+)") + + +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 has_solid_block(mask, min_side): + rows, cols = mask.shape + if rows < min_side or cols < min_side: + return None + integral = np.zeros((rows + 1, cols + 1), dtype=np.int64) + integral[1:, 1:] = mask.astype(np.int64).cumsum(0).cumsum(1) + for y in range(0, rows - min_side + 1): + for x in range(0, cols - min_side + 1): + total = (integral[y + min_side, x + min_side] - integral[y, x + min_side] + - integral[y + min_side, x] + integral[y, x]) + if total == min_side * min_side: + return (x, y) + return None + + +def main(argv): + if len(argv) != 3: + sys.stderr.write(__doc__) + return 2 + record_dir = Path(argv[1]) + out_txt = Path(argv[2]) + log_text = (record_dir / "log.txt").read_text(encoding="utf-8") + frames = [n for n in ["p47-day", "p47-night", "p47-close"] if (record_dir / f"{n}.png").exists()] + if len(frames) < 3: + sys.stderr.write(f"expected p47-day/night/close, found {frames}\n") + return 1 + + images = {n: luma(record_dir / f"{n}.png") for n in frames} + failures = [] + lines = ["P47 scene-detail analysis (LV_P47_SHOT=1)", ""] + + # visible reflections (unique source tiles) and their dst tiles. + seen = set() + reflections = [] + for line in log_text.splitlines(): + m = REFLECTION_RE.search(line) + if m: + sx, sy, dx, dy, alpha = (int(m.group(i)) for i in range(1, 6)) + if (sx, sy, dx, dy) in seen: + continue + seen.add((sx, sy, dx, dy)) + reflections.append((sx, sy, dx, dy, alpha)) + + # 1) reflection luma correlation: bank column profile vs vertically-mirrored band. + lines.append("1) 倒影区 luma 相关性 (bank column profile vs vertically-mirrored water band)") + for name in ["p47-day", "p47-night"]: + img = images[name] + above, below = [], [] + used = set() + for (sx, sy, _, _, _) in reflections: + if (sx, sy) in used: + continue + used.add((sx, sy)) + half, up, down = 64, TILE, 3 * TILE + if sx - half < 0 or sx + half > img.shape[1] or sy + up + down > img.shape[0]: + continue + above.append(img[sy:sy + up, sx - half:sx + half].mean(axis=0)) + below.append(img[sy + up:sy + up + down, sx - half:sx + half].mean(axis=0)) + if not above: + failures.append(f"{name}: no fully-visible reflection to correlate") + lines.append(f" {name}: no fully-visible reflection") + continue + a = np.concatenate(above) + b = np.concatenate(below) + a = a - a.mean() + b = b - b.mean() + r = float(np.corrcoef(a, b)[0, 1]) if a.std() > 0 and b.std() > 0 else 0.0 + lines.append(f" {name}: pearson r = {r:.3f} (required >= {REFLECTION_CORR_MIN})") + if r < REFLECTION_CORR_MIN: + failures.append(f"{name} reflection correlation {r:.3f} < {REFLECTION_CORR_MIN}") + + # reflection adds warm light vs neighbouring open water. + night = images["p47-night"] + deltas = [] + for (_, _, dx, dy, _) in reflections: + if dy + TILE > night.shape[0] or dx - 96 < 0: + continue + refl = night[dy:dy + TILE, dx:dx + TILE].mean() + ctrl = night[dy:dy + TILE, dx - 96:dx - 96 + TILE].mean() + deltas.append(float(refl - ctrl)) + if deltas: + lines.append(f" reflection vs control water luma delta (night) = {np.mean(deltas):.2f} (required >= {REFLECTION_DELTA_MIN})") + if float(np.mean(deltas)) < REFLECTION_DELTA_MIN: + failures.append(f"reflection luma delta {np.mean(deltas):.2f} < {REFLECTION_DELTA_MIN}") + else: + failures.append("no visible reflection tile to measure luma delta") + lines.append("") + + # 2) element-count metrics. + lines.append("2) 元素数量指标") + seen_counts = set() + for line in log_text.splitlines(): + m = ELEMENTS_RE.search(line) + if m: + counts = tuple(int(m.group(i)) for i in range(1, 5)) + if counts in seen_counts: + continue + seen_counts.add(counts) + lines.append(f" reeds={counts[0]} floats={counts[1]} reflections={counts[2]} sources={counts[3]}") + if not seen_counts: + failures.append("no p47-elements line in log") + else: + reeds, floats, refl, sources = next(iter(seen_counts)) + if min(reeds, floats, refl, sources) <= 0: + failures.append("element counts must all be positive") + lines.append("") + + # 3) no big black block / no white block / opaque. + lines.append("3) 无大黑块 / 无白块 / 不透明校验") + names = ["p47-day", "p47-night", "p47-close"] + for name in names: + a = np.asarray(Image.open(record_dir / f"{name}.png").convert("RGBA"), dtype=np.uint8) + rgb = a[:, :, :3] + alpha_min = int(a[:, :, 3].min()) + black = (rgb.max(axis=2) < 6) + white = (rgb.min(axis=2) >= 250) + bb = has_solid_block(black, MIN_BLOCK) + wb = has_solid_block(white, MIN_BLOCK) + lines.append(f" {name}: alpha_min={alpha_min} black_px={int(black.sum())} black_block={bb} white_block={wb}") + if alpha_min < 250: + failures.append(f"{name} has transparent pixels (alpha_min {alpha_min})") + if bb is not None: + failures.append(f"{name} has a {MIN_BLOCK}x{MIN_BLOCK} black block at {bb}") + if wb is not None: + failures.append(f"{name} has a {MIN_BLOCK}x{MIN_BLOCK} white block at {wb}") + lines.append("") + + lines.append("verdict = " + ("PASS" if not failures else "FAIL")) + out_txt.parent.mkdir(parents=True, exist_ok=True) + out_txt.write_text("\n".join(lines) + "\n", encoding="utf-8") + sys.stdout.write("\n".join(lines) + "\n") + for f in failures: + sys.stderr.write(f"FAIL {f}\n") + return 0 if not failures else 1 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv)) |
