1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
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())
|