diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-29 16:02:35 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-29 16:02:35 +0800 |
| commit | 975d6721973fa526014fb2401e97075ecbf51c2c (patch) | |
| tree | 2099e87d01f742ce57868e20a20abb2ba2fa8e7c /scripts | |
| parent | 3196b4914d037052db0cf191be0431dc4ac4d596 (diff) | |
| download | living-village-975d6721973fa526014fb2401e97075ecbf51c2c.tar.gz | |
p78: 夜帧水面微光斑、生成器世界炊烟与桥头窄光晕
Diffstat (limited to 'scripts')
| -rw-r--r-- | scripts/analyze-p78.py | 189 |
1 files changed, 189 insertions, 0 deletions
diff --git a/scripts/analyze-p78.py b/scripts/analyze-p78.py new file mode 100644 index 0000000..f97edd5 --- /dev/null +++ b/scripts/analyze-p78.py @@ -0,0 +1,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()) |
