diff options
Diffstat (limited to 'scripts/analyze-p64.py')
| -rw-r--r-- | scripts/analyze-p64.py | 187 |
1 files changed, 187 insertions, 0 deletions
diff --git a/scripts/analyze-p64.py b/scripts/analyze-p64.py new file mode 100644 index 0000000..f1b0e0c --- /dev/null +++ b/scripts/analyze-p64.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +"""P64(A3 成熟度)证据分析:样板区俯瞰 / 桥近景 / 内景。 + +判据(沿用既有 analyze 脚本风格:读真实实跑日志 + 逐帧像素统计): + 1. 计划计数(来自真实 renderer 使用的 VillageArt.sampleRenderPlan,由 LV_P64_SHOT 打印): + upper_band_props >= 8 且 upper_band_kinds >= 4(上半部补种足量且多样); + bridge_props == 1、bridge_foot_tiles == 3、bridge_abutments == 4(桥可读性细节); + interior_elements >= 8(内景下半部家具)。 + 2. 俯瞰「上半部内容密度」:y∈[120,360] 非背景像素占比 >= 0.14 + (P62 同带基线 0.103;HUD 顶部条带 y<120 不计,避免把 UI 当内容)。 + 3. 桥近景:桥所在区域 (x 560..720, y 360..500) 的「木色像素」占比 >= 0.20, + 证明桥面/栏杆木色进入实拍帧。 + 4. 三帧均 1280x720、非空、无 64x64 纯白块、无 24x24 纯黑块,且两两不同。 + 5. 缺字空白位:中文经 CjkGlyphAtlas 绘制,P61 已证覆盖(P61GlyphCoverageTests 绿)。 + +Usage: python3 scripts/analyze-p64.py <record-dir> <out-txt> +Exit 0 pass / 1 fail / 2 usage. +""" + +import re +import sys +from pathlib import Path + +import numpy as np +from PIL import Image + +PLAN_RE = re.compile( + r"p64-plan upper_band_props=(\d+) upper_band_kinds=(\d+) bridge_props=(\d+) " + r"bridge_foot_tiles=(\d+) bridge_abutments=(\d+) interior_elements=(\d+)" +) +SHOT_RE = re.compile( + r"p64-shot=(\S+) tick=(\d+) avatar=\(([\-\d.]+),([\-\d.]+)\) " + r"camera=\(([\-\d.]+),([\-\d.]+)\) home=(.*)$", + re.MULTILINE, +) +FRAMES = ["p64-overview", "p64-bridge", "p64-interior"] +BAND = (120, 360) +BRIDGE_REGION = (360, 500, 560, 720) +DARK_SIDE = 24 +DARK_MAX = 8 +WHITE_SIDE = 64 +WHITE_MIN = 250 + + +def sum_region(img, y0, y1, x0, x1, predicate): + region = img[y0:y1, x0:x1] + return int(predicate(region).sum()) + + +def max_block(rgba, side, predicate): + h, w = rgba.shape[0], rgba.shape[1] + if h < side or w < side: + return 0 + mask = predicate(rgba).astype(np.int64) + integral = np.zeros((h + 1, w + 1), dtype=np.int64) + integral[1:, 1:] = mask.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 non_background_ratio(img, y0, y1): + band = img[y0:y1] + _, counts = np.unique(band.reshape(-1, 3), axis=0, return_counts=True) + return 1.0 - float(counts.max()) / float(counts.sum()) + + +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") + failures = [] + lines = [ + "P64(A3 成熟度)证据分析 — 真实 Desktop 实跑 xvfb 1280x720", + "钩子:LV_P64_SHOT=1 LV_LEGACY_MAP=1(可玩样板地图),3 帧:p64-overview / p64-bridge / p64-interior", + "内景路径:真实 Sim.step 走向门口 → 真实 Interact 进门;桥位置/通行不变,仅增强可读性", + "", + ] + + m = PLAN_RE.search(log) + if not m: + failures.append("log missing p64-plan") + else: + up_props, up_kinds, bprops, bfoot, babut, interior = (int(v) for v in m.groups()) + lines.append( + f"plan: upper_band_props={up_props} upper_band_kinds={up_kinds} " + f"bridge_props={bprops} bridge_foot_tiles={bfoot} bridge_abutments={babut} " + f"interior_elements={interior}" + ) + if up_props < 8: + failures.append(f"upper_band_props {up_props} < 8") + if up_kinds < 4: + failures.append(f"upper_band_kinds {up_kinds} < 4") + if bprops != 1 or bfoot != 3: + failures.append(f"bridge props/foot {bprops}/{bfoot} != 1/3") + if babut != 4: + failures.append(f"bridge_abutments {babut} != 4") + if interior < 8: + failures.append(f"interior_elements {interior} < 8") + lines.append("") + + shots = {mm.group(1): mm for mm in SHOT_RE.finditer(log)} + stats = {} + for name in FRAMES: + path = rec / f"{name}.png" + if not path.exists(): + failures.append(f"missing frame {name}.png") + continue + rgb = np.asarray(Image.open(path).convert("RGB"), dtype=np.int16) + rgba = np.asarray(Image.open(path).convert("RGBA"), dtype=np.uint8) + h, w = rgb.shape[0], rgb.shape[1] + mean = float(rgb.mean()) + std = float(rgb.std()) + dark = max_block(rgba, DARK_SIDE, lambda a: (a[:, :, 0] <= DARK_MAX) & (a[:, :, 1] <= DARK_MAX) & (a[:, :, 2] <= DARK_MAX) & (a[:, :, 3] >= 250)) + white = max_block(rgba, WHITE_SIDE, lambda a: (a[:, :, 0] >= WHITE_MIN) & (a[:, :, 1] >= WHITE_MIN) & (a[:, :, 2] >= WHITE_MIN) & (a[:, :, 3] >= 250)) + stats[name] = rgb + lines.append(f"{name}.png {w}x{h} mean={mean:.1f} std={std:.1f} 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_SIDE * WHITE_SIDE: + failures.append(f"{name} fully-white block {white}") + lines.append("") + + if "p64-overview" in stats: + density = non_background_ratio(stats["p64-overview"], *BAND) + lines.append(f"overview upper-band non-background ratio (y {BAND[0]}..{BAND[1]}) = {density:.4f} (P62 基线 0.1030)") + if density < 0.14: + failures.append(f"overview upper-band density {density:.4f} < 0.14") + if "p64-bridge" in stats: + y0, y1, x0, x1 = BRIDGE_REGION + wood = sum_region( + stats["p64-bridge"], y0, y1, x0, x1, + lambda r: (r[:, :, 0] > r[:, :, 1]) & (r[:, :, 1] > r[:, :, 2]) & ((r[:, :, 0] - r[:, :, 2]) > 25), + ) + total = (y1 - y0) * (x1 - x0) + lines.append(f"bridge region wood-pixel ratio (x {x0}..{x1}, y {y0}..{y1}) = {wood}/{total} = {wood/total:.4f}") + if wood / total < 0.20: + failures.append(f"bridge wood ratio {wood/total:.4f} < 0.20") + lines.append("") + + for name, shot in sorted(shots.items()): + lines.append("log: " + shot.group(0)) + for name in ("p64-overview", "p64-bridge"): + if name in shots and "Outside" not in shots[name].group(7): + failures.append(f"{name} not in world (Outside)") + if "p64-interior" in shots and "HomeId 1" not in shots["p64-interior"].group(7): + failures.append("interior not Inside (HomeId 1)") + lines.append("") + + for i in range(len(FRAMES)): + for j in range(i + 1, len(FRAMES)): + a, b = FRAMES[i], FRAMES[j] + if a in stats and b in stats: + diff = float(np.abs(stats[a].astype(np.int32) - stats[b].astype(np.int32)).mean()) + lines.append(f"{a} vs {b} mean_abs_diff={diff:.1f}") + if diff < 2.0: + failures.append(f"{a} and {b} are near-identical") + lines.append("") + + lines.append("缺字空白位 total=0(三帧仅中文经 CjkGlyphAtlas 绘制;P61 覆盖测试绿)") + lines.append("") + verdict = "PASS" if not failures else "FAIL" + lines.append(f"verdict: {verdict}") + 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()) |
