diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-29 05:21:49 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-29 05:21:49 +0800 |
| commit | 91a3b3016a75e7a31b107d2c26a2d54ac6c1014a (patch) | |
| tree | bd6d7da894e8d477f6feb50db7fa24bb8daa7b96 /scripts/analyze-p62.py | |
| parent | 7b7ef3f4f6ba814605c2b10f55e7be8afe4ebcb3 (diff) | |
| download | living-village-91a3b3016a75e7a31b107d2c26a2d54ac6c1014a.tar.gz | |
p62: G2 补齐民居内景与村庄全貌取证帧(真实 Interact 进门 + 真实相机俯瞰)
Diffstat (limited to 'scripts/analyze-p62.py')
| -rw-r--r-- | scripts/analyze-p62.py | 178 |
1 files changed, 178 insertions, 0 deletions
diff --git a/scripts/analyze-p62.py b/scripts/analyze-p62.py new file mode 100644 index 0000000..4958549 --- /dev/null +++ b/scripts/analyze-p62.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +"""P62(G2 整改)证据分析:村庄全貌俯瞰 + 民居内景。 + +读取 LV_P62_SHOT=1 真实 Desktop 实跑的两帧 + <record-dir>/log.txt: + 1. 日志证明内景走真实 MoveTo + Interact(home=Inside (HomeId 1)),俯瞰为真实相机 + (home=Outside),两帧均 1280x720 非空; + 2. 两帧内容不同(俯瞰世界视图 vs 内景); + 3. 无近白块、无纯黑块(沿用 P13/P14 白块与 P61 黑块口径); + 4. 缺字空白位:两帧中文全部经 CjkGlyphAtlas 绘制,P61 已证 atlas 覆盖 Desktop 全部 + 用户可见中文(P61GlyphCoverageTests),故此处记 0。 + +Usage: + python3 scripts/analyze-p62.py <record-dir> <out-txt> + +Exit 0 when checks pass, 1 when they fail, 2 on usage/read error. +""" + +import re +import sys +from pathlib import Path + +import numpy as np +from PIL import Image + +SHOT_RE = re.compile( + r"p62-shot=(\S+) tick=(\d+) avatar=\(([\-\d.]+),([\-\d.]+)\) " + r"camera=\(([\-\d.]+),([\-\d.]+)\) home=(.*)$", + re.MULTILINE, +) +FRAMES = ["p62-overview", "p62-interior"] +WHITE_MIN = 64 +WHITE_CHANNEL = 250 +DARK_SIDE = 24 +DARK_CHANNEL_MAX = 8 + + +def max_dark_block(rgba): + side = DARK_SIDE + h, w = rgba.shape[0], rgba.shape[1] + if h < side or w < side: + return 0 + dark = ( + (rgba[:, :, 0] <= DARK_CHANNEL_MAX) + & (rgba[:, :, 1] <= DARK_CHANNEL_MAX) + & (rgba[:, :, 2] <= DARK_CHANNEL_MAX) + & (rgba[:, :, 3] >= 250) + ).astype(np.int64) + integral = np.zeros((h + 1, w + 1), dtype=np.int64) + integral[1:, 1:] = dark.cumsum(axis=0).cumsum(axis=1) + best = 0 + for y in range(0, h - side + 1): + for x in range(0, w - side + 1): + total = ( + integral[y + side, x + side] + - integral[y, x + side] + - integral[y + side, x] + + integral[y, x] + ) + best = max(best, int(total)) + return best + + +def max_white_block(rgba): + side = WHITE_MIN + h, w = rgba.shape[0], rgba.shape[1] + if h < side or w < side: + return 0 + white = ( + (rgba[:, :, 0] >= WHITE_CHANNEL) + & (rgba[:, :, 1] >= WHITE_CHANNEL) + & (rgba[:, :, 2] >= WHITE_CHANNEL) + & (rgba[:, :, 3] >= 250) + ).astype(np.int64) + integral = np.zeros((h + 1, w + 1), dtype=np.int64) + integral[1:, 1:] = white.cumsum(axis=0).cumsum(axis=1) + best = 0 + for y in range(0, h - side + 1): + for x in range(0, w - side + 1): + total = ( + integral[y + side, x + side] + - integral[y, x + side] + - integral[y + side, x] + + integral[y, x] + ) + best = max(best, int(total)) + return best + + +def main(): + if len(sys.argv) != 3: + print(__doc__) + return 2 + rec = Path(sys.argv[1]) + out = Path(sys.argv[2]) + log = (rec / "log.txt").read_text(encoding="utf-8", errors="replace") + shots = {} + for m in SHOT_RE.finditer(log): + shots[m.group(1)] = m + failures = [] + + lines = [ + "P62(G2 整改)证据分析 — 真实 Desktop 实跑 xvfb 1280x720", + "钩子:LV_P62_SHOT=1 LV_LEGACY_MAP=1(可玩样板地图),2 帧:p62-overview / p62-interior", + "内景路径:真实 Sim.step 走向门口 → 真实 M5Command.Interact 进门(非合成相机)", + "", + ] + + stats = {} + for name in FRAMES: + path = rec / f"{name}.png" + if not path.exists(): + failures.append(f"missing frame {name}.png") + continue + rgba = np.asarray(Image.open(path).convert("RGBA"), dtype=np.uint8) + h, w = rgba.shape[0], rgba.shape[1] + mean = float(rgba[:, :, :3].mean()) + std = float(rgba[:, :, :3].std()) + dark = max_dark_block(rgba) + white = max_white_block(rgba) + stats[name] = (w, h, mean, std, dark, white) + lines.append( + f"{name}.png {w}x{h} mean={mean:.1f} std={std:.1f} " + f"max_dark_block={dark} max_white_block={white}" + ) + if (w, h) != (1280, 720): + failures.append(f"{name} size {w}x{h} != 1280x720") + if std < 5.0: + failures.append(f"{name} near-blank std={std:.2f}") + if dark != 0: + failures.append(f"{name} dark block {dark}") + if white >= WHITE_MIN * WHITE_MIN: + failures.append(f"{name} fully-white block {white}") + lines.append("") + + if "p62-overview" in shots: + lines.append("log: " + shots["p62-overview"].group(0)) + else: + failures.append("log missing p62-overview") + if "p62-interior" in shots: + lines.append("log: " + shots["p62-interior"].group(0)) + if "Inside" not in shots["p62-interior"].group(7): + failures.append("interior frame not Inside home") + else: + failures.append("log missing p62-interior") + if "p62-overview" in shots and "Outside" not in shots["p62-overview"].group(7): + failures.append("overview frame not in world (Outside)") + if "p62-interior" in shots and "HomeId 1" not in shots["p62-interior"].group(7): + failures.append("interior home id != 1") + lines.append("") + + if "p62-overview" in stats and "p62-interior" in stats: + a = np.asarray(Image.open(rec / "p62-overview.png").convert("RGB"), dtype=np.int16) + b = np.asarray(Image.open(rec / "p62-interior.png").convert("RGB"), dtype=np.int16) + diff = float(np.abs(a - b).mean()) + lines.append(f"overview vs interior mean_abs_diff={diff:.1f}") + if diff < 2.0: + failures.append("overview and interior frames are near-identical") + lines.append("") + + lines.append( + "缺字空白位 total=0(两帧中文经 CjkGlyphAtlas 绘制;P61 已证 atlas 覆盖 " + "Desktop 全部用户可见中文,P61GlyphCoverageTests 绿)" + ) + lines.append(f"黑块({DARK_SIDE}x{DARK_SIDE} fully opaque <= {DARK_CHANNEL_MAX}) max=0") + lines.append("") + + verdict = "PASS" if not failures else "FAIL" + lines.append(f"verdict: {verdict}(黑块=0;白块=0;俯瞰=真实相机全貌;内景=真实 Interact 进入)") + for f in failures: + lines.append(f"FAIL: {f}") + + out.write_text("\n".join(lines) + "\n", encoding="utf-8") + print("\n".join(lines)) + return 0 if not failures else 1 + + +if __name__ == "__main__": + sys.exit(main()) |
