summaryrefslogtreecommitdiff
path: root/scripts/analyze-p78.py
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-29 16:02:35 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-29 16:02:35 +0800
commit975d6721973fa526014fb2401e97075ecbf51c2c (patch)
tree2099e87d01f742ce57868e20a20abb2ba2fa8e7c /scripts/analyze-p78.py
parent3196b4914d037052db0cf191be0431dc4ac4d596 (diff)
downloadliving-village-975d6721973fa526014fb2401e97075ecbf51c2c.tar.gz
p78: 夜帧水面微光斑、生成器世界炊烟与桥头窄光晕
Diffstat (limited to 'scripts/analyze-p78.py')
-rw-r--r--scripts/analyze-p78.py189
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())