diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-29 10:58:24 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-29 10:58:24 +0800 |
| commit | 0b1330de9b8d78f398753d878f495f324617d203 (patch) | |
| tree | 298710d868c5a965c4dfb94fa5db89a796ca78c0 /scripts | |
| parent | d170af8485ad787cbc2a87b352d2b5cd81605b4d (diff) | |
| download | living-village-0b1330de9b8d78f398753d878f495f324617d203.tar.gz | |
p73: 昼夜光照四时段档(清晨/正午/黄昏/夜晚)
M6Presentation 新增 periodAtTick/periodLighting 参数表(AmbientTint 乘性色调 + Veil/VeilAlpha 加性覆盖 + LanternGlow),边界与 timePeriodLabel 一致;正午为恒等档不改白平衡。DrawWorldView 并入时段色调与 effectiveLanternGlow(黄昏 0.6 起点亮、夜晚 1.0),覆盖色最后叠加。新增 P73 回归;新增 LV_P73_SHOT 同机位四帧证据 + analyze-p73(verdict=PASS);字形表补 正/午(345)。Desktop 346/346、Kernel 120/120、digest 与 p70 一致。
Diffstat (limited to 'scripts')
| -rw-r--r-- | scripts/analyze-p73.py | 181 |
1 files changed, 181 insertions, 0 deletions
diff --git a/scripts/analyze-p73.py b/scripts/analyze-p73.py new file mode 100644 index 0000000..31595bf --- /dev/null +++ b/scripts/analyze-p73.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +"""P73(昼夜光照层次:四时段档)证据分析:同一桥头机位 6/12/18/22 时四张真实帧。 + +判据(读真实实跑日志 + 逐帧像素统计): + 1. 日志含四行 `p73-shot=p73-<period> hour=.. period=.. blend=.. lantern=.. tint=.. veil=..`, + 时段名覆盖 清晨/正午/黄昏/夜晚。 + 2. 参数一致性:正午 veil=0 且 lantern=0;黄昏 lantern>=0.3(路灯/灯笼开始点亮); + 夜晚 lantern>=1.0;四行 veil/tint/blend 组合两两不同。 + 3. 抽帧 PNG:1280x720、非空白(std>=8)、无 24x24 纯黑块。 + 4. 可辨差异:逐帧平均亮度(night 最暗、noon 最亮)与平均暖度(黄昏 R-B 最高); + 四帧两两像素 diff > 0。 + 5. 色调是否「好看/自然」的 vision 复核由 Hermes 侧完成,非本脚本判据。 + +Usage: python3 scripts/analyze-p73.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 + +SHOT_RE = re.compile( + r"^p73-shot=(\S+) hour=([\d.]+) tick=(\d+) period=(\S+) blend=([\d.]+) lantern=([\d.]+) tint=(\d+),(\d+),(\d+) veil=(\d+)$", + re.MULTILINE, +) +ORDER = ["p73-morning", "p73-noon", "p73-dusk", "p73-night"] +DARK_SIDE = 24 +DARK_MAX = 8 + + +def max_block(rgba, side, predicate): + h, w = rgba.shape[0], rgba.shape[1] + if h < side or w < side: + return 0 + mask = predicate(rgba).astype(np.int64) + integral = np.zeros((h + 1, w + 1), dtype=np.int64) + integral[1:, 1:] = mask.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") + failures = [] + lines = [ + "P73(昼夜光照层次:清晨/正午/黄昏/夜晚四档)证据分析 — 真实 Desktop xvfb 1280x720", + "钩子:LV_P73_SHOT=1(同一桥头光源机位,6/12/18/22 各一帧,画面差异只来自光照)", + "路径:tick → periodAtTick → periodLighting(环境色调/覆盖色/VeilAlpha/灯光) + 连续 blend/warmth veils", + "", + ] + + shots = {} + for m in SHOT_RE.finditer(log): + name = m.group(1) + if name not in shots: + shots[name] = { + "hour": float(m.group(2)), + "tick": int(m.group(3)), + "period": m.group(4), + "blend": float(m.group(5)), + "lantern": float(m.group(6)), + "tint": (int(m.group(7)), int(m.group(8)), int(m.group(9))), + "veil": int(m.group(10)), + } + for name in ORDER: + if name not in shots: + failures.append(f"log missing p73-shot={name}") + else: + s = shots[name] + lines.append( + f"{name}: hour={s['hour']} tick={s['tick']} period={s['period']} blend={s['blend']} lantern={s['lantern']} tint={s['tint']} veil={s['veil']}" + ) + lines.append("") + + if "p73-noon" in shots: + s = shots["p73-noon"] + if s["veil"] != 0: + failures.append(f"noon veil {s['veil']} != 0 (must be identity)") + if s["lantern"] != 0.0: + failures.append(f"noon lantern {s['lantern']} != 0") + if "p73-dusk" in shots and shots["p73-dusk"]["lantern"] < 0.3: + failures.append(f"dusk lantern {shots['p73-dusk']['lantern']} < 0.3 (lamps must start at dusk)") + if "p73-night" in shots and shots["p73-night"]["lantern"] < 1.0: + failures.append(f"night lantern {shots['p73-night']['lantern']} < 1.0") + combos = {(s["blend"], s["lantern"], s["tint"], s["veil"]) for s in shots.values()} + if len(combos) < len(shots): + failures.append("two periods share identical (blend,lantern,tint,veil) parameters") + lines.append("") + + stats = {} + for name in ORDER: + 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) + rgb = rgba[:, :, :3].astype(np.float64) + h, w = rgb.shape[0], rgb.shape[1] + mean = rgb.mean(axis=(0, 1)) + warmth = float(mean[0] - mean[2]) + brightness = float(rgb.mean()) + std = float(rgb.std()) + dark = max_block( + rgba, DARK_SIDE, + lambda a: (a[:, :, 0] <= DARK_MAX) & (a[:, :, 1] <= DARK_MAX) & (a[:, :, 2] <= DARK_MAX) & (a[:, :, 3] >= 250), + ) + stats[name] = {"brightness": brightness, "warmth": warmth, "std": std} + lines.append( + f"{name}.png: size={w}x{h} brightness={brightness:.1f} R={mean[0]:.1f} G={mean[1]:.1f} B={mean[2]:.1f} warmth(R-B)={warmth:+.1f} std={std:.1f} dark_block={dark}" + ) + if (w, h) != (1280, 720): + failures.append(f"{name}.png size {w}x{h} != 1280x720") + if std < 8.0: + failures.append(f"{name}.png looks blank (std={std:.1f})") + if dark > 0: + failures.append(f"{name}.png has {DARK_SIDE}x{DARK_SIDE} pure-black block") + lines.append("") + + have = [n for n in ORDER if n in stats] + for i in range(len(have)): + for j in range(i + 1, len(have)): + a = np.asarray(Image.open(rec / f"{have[i]}.png").convert("RGB"), dtype=np.int16) + b = np.asarray(Image.open(rec / f"{have[j]}.png").convert("RGB"), dtype=np.int16) + differing = int((np.abs(a - b).sum(axis=2) > 0).sum()) + lines.append(f"diff {have[i]} vs {have[j]}: differing_pixels={differing}") + if differing == 0: + failures.append(f"{have[i]} identical to {have[j]}") + + if all(n in stats for n in ORDER): + if not (stats["p73-dusk"]["warmth"] > stats["p73-noon"]["warmth"]): + failures.append("dusk is not warmer than noon (R-B)") + if not (stats["p73-night"]["warmth"] < stats["p73-noon"]["warmth"]): + failures.append("night is not cooler than noon (R-B)") + if not (stats["p73-night"]["brightness"] < stats["p73-dusk"]["brightness"]): + failures.append("night is not darker than dusk") + if not (stats["p73-night"]["brightness"] < stats["p73-morning"]["brightness"]): + failures.append("night is not darker than morning") + lines.append("") + lines.append( + f"brightness order: " + + " > ".join(f"{n.split('-')[1]}({stats[n]['brightness']:.1f})" for n in sorted(ORDER, key=lambda n: -stats[n]["brightness"])) + ) + lines.append( + f"warmth order: " + + " > ".join(f"{n.split('-')[1]}({stats[n]['warmth']:+.1f})" for n in sorted(ORDER, key=lambda n: -stats[n]["warmth"])) + ) + + lines.append("") + if failures: + lines.append("verdict=FAIL") + for f in failures: + lines.append(" - " + f) + else: + lines.append("verdict=PASS") + lines.append(" four periods have distinct parameters; noon identity, dusk lamps on, night lamps full;") + lines.append(" real frames render 1280x720, differ pairwise, night darker / noon brighter / dusk warmer.") + 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()) |
