#!/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 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())