summaryrefslogtreecommitdiff
path: root/scripts/analyze-p78.py
blob: f97edd5329bd9f2ecf4d3387b4910676522be1ee (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
179
180
181
182
183
184
185
186
187
188
189
#!/usr/bin/env python3
"""P78(夜晚氛围与场景细节)基准截图抽帧分析。

读 `LV_P78_SHOT=1` 的真实 xvfb 1280x720 帧(同日同时刻机位先拍正午、再拍 22 时)与日志,
在像素层给出可测断言,避免只靠 vision 手判:

  1. 夜帧水面逐格微光斑:光斑格条带亮度 - 相邻非光斑水格亮度 >= 10;非光斑水格 std <= 12
     (暗部平整、光斑不是噪声)。
  2. 暗部层次:夜帧水面 bbox std >= 白天水面 bbox std,且夜帧无实心 24x24 近黑块、
     近黑占比 <= 0.02(不是「铁板一块」)。
  3. 炊烟:夜帧炊烟区亮度 - 白天炊烟区亮度 >= 20(夜帧更醒目);日志 smoke_alpha(夜) > 80。
  4. 桥头灯柱窄光晕:灯柱环带亮度 - 外圈亮度 夜帧 >= 8,白天 < 8(夜帧才加、窄)。
  5. 白天零回归:日志 lantern=0.00;白天/夜帧 pairwise diff > 0。

Usage: python3 scripts/analyze-p78.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
from numpy.lib.stride_tricks import sliding_window_view

REGIONS_RE = re.compile(
    r"^p78-regions (day|night) glint_src=(-?\d+),(-?\d+) glint_ctl=(-?\d+),(-?\d+) bridge_src=(-?\d+),(-?\d+)",
    re.MULTILINE,
)
NIGHT_RE = re.compile(
    r"^p78-shot=night .*smoke_src=(-?\d+),(-?\d+) smoke_alpha=(\d+)",
    re.MULTILINE,
)
DAY_RE = re.compile(r"^p78-shot=day .*lantern=([\d.]+)", re.MULTILINE)

TILE = 32
BLOCK = 24
NEAR_BLACK = 24
WATER_BBOX_Y = (280, 376)


def luma(a):
    return 0.299 * a[:, :, 0] + 0.587 * a[:, :, 1] + 0.114 * a[:, :, 2]


def last_regions(text, tag):
    hits = [m for m in REGIONS_RE.finditer(text) if m.group(1) == tag]
    if not hits:
        return None
    m = hits[-1]
    return {
        "glint": (int(m.group(2)), int(m.group(3))),
        "ctl": (int(m.group(4)), int(m.group(5))),
        "bridge": (int(m.group(6)), int(m.group(7))),
    }


def ring_mean(a, cx, cy, r0, r1):
    y, x = np.ogrid[: a.shape[0], : a.shape[1]]
    d = np.sqrt((x - cx) ** 2 + (y - cy) ** 2)
    m = (d >= r0) & (d < r1)
    return float(luma(a)[m].mean())


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 = [
        "P78(夜晚氛围与场景细节)证据分析 — 真实 Desktop xvfb 1280x720,同日同机位 正午 vs 22 时",
        "近黑阈值 24;实心块 = 24x24 全近黑。",
        "",
    ]

    day_img = rec / "p78-day.png"
    night_img = rec / "p78-night.png"
    if not day_img.exists() or not night_img.exists():
        out.write_text("missing p78-day.png / p78-night.png\nverdict=FAIL\n", encoding="utf-8")
        print("missing frames")
        return 1
    day = np.asarray(Image.open(day_img).convert("RGB"), dtype=np.int16)
    night = np.asarray(Image.open(night_img).convert("RGB"), dtype=np.int16)

    day_regions = last_regions(log, "day")
    night_regions = last_regions(log, "night")
    if not day_regions or not night_regions:
        failures.append("missing p78-regions day/night")
    if "p78-shot=day" not in log or "p78-shot=night" not in log:
        failures.append("missing p78-shot day/night lines")

    day_lantern = DAY_RE.search(log)
    if day_lantern and float(day_lantern.group(1)) != 0.0:
        failures.append(f"白天 lantern={day_lantern.group(1)} 应为 0.00(白天零回归)")
    lines.append(f"day lantern={day_lantern.group(1) if day_lantern else '?'}(须 0.00)")

    # 1. 夜帧水面微光斑对比。
    if night_regions:
        gx, gy = night_regions["glint"]
        cx, cy = night_regions["ctl"]
        if gx < 0 or cx < 0:
            failures.append("夜帧未定位到可见光斑/对照水格")
        else:
            g_tile = luma(night)[gy : gy + TILE, gx : gx + TILE]
            c_tile = luma(night)[cy : cy + TILE, cx : cx + TILE]
            stripe = g_tile[12:16, 6:26].mean()
            ctl_mean = c_tile.mean()
            ctl_std = c_tile.std()
            delta = stripe - ctl_mean
            lines.append(
                f"night glint stripe={stripe:.1f} ctl_mean={ctl_mean:.1f} ctl_std={ctl_std:.1f} delta={delta:.1f}"
            )
            if delta < 10:
                failures.append(f"夜帧光斑对比不足 delta={delta:.1f} < 10")
            if ctl_std > 12:
                failures.append(f"夜帧非光斑水面不够平整 std={ctl_std:.1f} > 12")

    # 2. 暗部层次 + 无铁板一块。
    day_water = luma(day)[WATER_BBOX_Y[0] : WATER_BBOX_Y[1], :]
    night_water = luma(night)[WATER_BBOX_Y[0] : WATER_BBOX_Y[1], :]
    lines.append(
        f"water bbox std: day={day_water.std():.1f} night={night_water.std():.1f}"
    )
    if night_water.std() < day_water.std():
        failures.append("夜帧水面层次低于白天(暗部被压平)")
    mask = (night[:, :, 0] <= NEAR_BLACK) & (night[:, :, 1] <= NEAR_BLACK) & (night[:, :, 2] <= NEAR_BLACK)
    frac = float(mask.mean())
    block = int(sliding_window_view(mask.astype(np.int64), (BLOCK, BLOCK)).sum(axis=(2, 3)).max())
    lines.append(f"night near-black frac={frac:.3f} max24blk={block}")
    if frac > 0.02:
        failures.append(f"夜帧近黑占比 {frac:.3f} > 0.02(铁板一块)")
    if block >= BLOCK * BLOCK:
        failures.append("夜帧存在实心 24x24 近黑块")

    # 3. 炊烟夜帧更醒目。
    night_meta = NIGHT_RE.search(log)
    if not night_meta:
        failures.append("missing p78-shot=night smoke metadata")
    else:
        sx, sy, smoke_alpha = int(night_meta.group(1)), int(night_meta.group(2)), int(night_meta.group(3))
        day_smoke = luma(day)[320:400, 620:720].mean()
        night_smoke = luma(night)[320:400, 620:720].mean()
        lines.append(
            f"smoke region mean: day={day_smoke:.1f} night={night_smoke:.1f} delta={night_smoke - day_smoke:.1f} smoke_alpha={smoke_alpha}"
        )
        if night_smoke - day_smoke < 20:
            failures.append(f"夜帧炊烟不够醒目 delta={night_smoke - day_smoke:.1f} < 20")
        if smoke_alpha <= 80:
            failures.append(f"夜帧炊烟 alpha {smoke_alpha} 应 > 80")

    # 4. 桥头灯柱窄光晕。
    if day_regions and night_regions and night_regions["bridge"][0] >= 0:
        bx, by = night_regions["bridge"]
        ccx, ccy = bx + TILE // 2, by + TILE // 2
        day_ring = ring_mean(day, ccx, ccy, 8, 30) - ring_mean(day, ccx, ccy, 40, 64)
        night_ring = ring_mean(night, ccx, ccy, 8, 30) - ring_mean(night, ccx, ccy, 40, 64)
        lines.append(f"bridge ring diff: day={day_ring:.1f} night={night_ring:.1f}")
        if night_ring < 8:
            failures.append(f"夜帧桥头窄光晕不足 diff={night_ring:.1f} < 8")
        if day_ring >= 8:
            failures.append(f"白天桥头不应有夜光晕 diff={day_ring:.1f}")

    # 5. 白天/夜帧差异。
    diff = int((np.abs(day.astype(np.int64) - night.astype(np.int64)).sum(axis=2) > 0).sum())
    lines.append(f"day vs night differing_pixels={diff}")
    if diff == 0:
        failures.append("白天与夜帧完全相同")

    lines.append("")
    if failures:
        lines.append("verdict=FAIL")
        for f in failures:
            lines.append("  - " + f)
    else:
        lines.append("verdict=PASS")
        lines.append("  夜帧光斑对比/非光斑平整、水面层次不低于白天、无铁板一块;")
        lines.append("  炊烟夜帧更醒目、桥头窄光晕仅夜帧出现;白天 lantern=0 且两帧有差异。")
    out.parent.mkdir(parents=True, exist_ok=True)
    out.write_text("\n".join(lines) + "\n", encoding="utf-8")
    print(f"wrote {out} verdict={'FAIL' if failures else 'PASS'}")
    return 1 if failures else 0


if __name__ == "__main__":
    sys.exit(main())