summaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'scripts')
-rw-r--r--scripts/analyze-p62.py178
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())