summaryrefslogtreecommitdiff
path: root/scripts/analyze-p62.py
blob: 4958549a3b668460d9f6884be59ed9c789599054 (plain)
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
#!/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())