summaryrefslogtreecommitdiff
path: root/scripts/analyze-p47.py
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-28 20:26:42 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-28 20:26:42 +0800
commit08925d4863d8aaf93a974b8fa21a91e931a0928e (patch)
treef18055c232fc1a6022eeab552402185c6948ec02 /scripts/analyze-p47.py
parent2ab4b8a22aaec3482d62433cfa4b73a657f2a165 (diff)
downloadliving-village-08925d4863d8aaf93a974b8fa21a91e931a0928e.tar.gz
p47: 场景细节升级 — 河道倒影(waterFrameTickAt微抖)+岸边芦苇加密度+鸭/叶漂浮元素+近景屋顶统一光源阴影
Diffstat (limited to 'scripts/analyze-p47.py')
-rw-r--r--scripts/analyze-p47.py173
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))