diff options
Diffstat (limited to 'scripts/analyze-p47.py')
| -rw-r--r-- | scripts/analyze-p47.py | 226 |
1 files changed, 147 insertions, 79 deletions
diff --git a/scripts/analyze-p47.py b/scripts/analyze-p47.py index ae9ade1..6937413 100644 --- a/scripts/analyze-p47.py +++ b/scripts/analyze-p47.py @@ -1,13 +1,16 @@ #!/usr/bin/env python3 -"""P47 scene-detail analysis. +"""P47 scene-detail analysis (leader rework). Reads the `LV_P47_SHOT=1` day/night/closeup frames + <record-dir>/log.txt and writes docs/evidence/p47-analysis.txt with: - 1. reflection luma correlation (bank column profile vs vertically-mirrored water - band) and reflection-vs-control-water luma delta; - 2. element-count metrics (bank reeds / floating props / reflection streaks); - 3. no big black block (min 64px square) / no white block / opaque frames. + 1. per-frame vertical reflection streaks: max run of consecutive tiles in one + column (leader ①: length >= 3 tiles) and reflection-vs-background-water + luma + warmth deltas (leader ① / ②: warm vertical column, not blue ripple); + 2. per-frame foreground roof lit-face vs shadow-face luma delta (leader ③: + two light tiers from one unified light direction); + 3. element counts (bank reeds / floating props / reflection streaks); + 4. no big black block / no white block / opaque frames. Usage: python3 scripts/analyze-p47.py <record-dir> <out-txt> @@ -22,14 +25,21 @@ from PIL import Image TILE = 32 MIN_BLOCK = 64 -REFLECTION_CORR_MIN = 0.20 -REFLECTION_DELTA_MIN = 10.0 -REFLECTION_RE = re.compile(r"p47-reflection src=(\d+),(\d+) dst=(\d+),(\d+) alpha=(\d+)") -ELEMENTS_RE = re.compile(r"p47-elements reeds=(\d+) floats=(\d+) reflections=(\d+) sources=(\d+)") +MIN_VERTICAL_RUN = 3 +REFLECTION_DL_MIN = 8.0 +REFLECTION_WARM_MIN = 20.0 +ROOF_DELTA_MIN = 16.0 +REFLECTION_RE = re.compile( + r"p47-reflection (\S+) src=(\d+),(\d+) dst=(\d+),(\d+) ctrl=(-?\d+),(-?\d+) alpha=(\d+)") +ROOF_RE = re.compile(r"p47-roof (\S+) x=(\d+) y=(\d+) w=(\d+)") +ELEMENTS_RE = re.compile(r"p47-elements reeds=(\d+) floats=(\d+) reflections=(\d+) sources=(\d+) maxstreak=(\d+)") -def luma(path): - a = np.asarray(Image.open(path).convert("RGB"), dtype=np.float64) +def rgb(path): + return np.asarray(Image.open(path).convert("RGB"), dtype=np.float64) + + +def luma(a): return 0.299 * a[:, :, 0] + 0.587 * a[:, :, 1] + 0.114 * a[:, :, 2] @@ -48,6 +58,79 @@ def has_solid_block(mask, min_side): return None +def parse_log(log_text, frame): + reflections, roofs = [], [] + for line in log_text.splitlines(): + m = REFLECTION_RE.search(line) + if m and m.group(1) == frame: + _, sx, sy, dx, dy, cx, cy, alpha = m.groups() + reflections.append((int(sx), int(sy), int(dx), int(dy), int(cx), int(cy), int(alpha))) + continue + m = ROOF_RE.search(line) + if m and m.group(1) == frame: + _, x, y, w = m.groups() + roofs.append((int(x), int(y), int(w))) + return reflections, roofs + + +def max_vertical_run(reflections): + cols = {} + for (_, _, dx, dy, _, _, _) in reflections: + cols.setdefault(dx, set()).add(dy) + best = 0 + for ys in cols.values(): + ys = sorted(ys) + cur = 1 + for i in range(1, len(ys)): + cur = cur + 1 if ys[i] == ys[i - 1] + TILE else 1 + best = max(best, cur) + return best + + +def reflection_metrics(img, reflections): + """Reflection tiles vs a background-water baseline taken from the same rows, + masking out +/-24px around every reflection column.""" + h, w, _ = img.shape + if not reflections: + return [] + rows = sorted({dy for (_, _, _, dy, _, _, _) in reflections}) + cols = sorted({dx for (_, _, dx, _, _, _, _) in reflections}) + mask = np.zeros((h, w), dtype=bool) + for dx in cols: + mask[:, max(0, dx - 24):min(w, dx + TILE + 24)] = True + background = [] + for dy in rows: + band = img[dy:dy + TILE] + m = ~mask[dy:dy + TILE] + if m.any(): + background.append(band[m].reshape(-1, 3)) + if not background: + return [] + bg = np.concatenate(background, axis=0) + bg_luma = (0.299 * bg[:, 0] + 0.587 * bg[:, 1] + 0.114 * bg[:, 2]).mean() + bg_warm = (bg[:, 0] - bg[:, 2]).mean() + out = [] + for (_, _, dx, dy, _, _, _) in reflections: + if dx < 0 or dy < 0 or dx + TILE > w or dy + TILE > h: + continue + tile = img[dy:dy + TILE, dx:dx + TILE].reshape(-1, 3) + tile_luma = (0.299 * tile[:, 0] + 0.587 * tile[:, 1] + 0.114 * tile[:, 2]).mean() + out.append((tile_luma - bg_luma, (tile[:, 0] - tile[:, 2]).mean() - bg_warm)) + return out + + +def roof_metrics(img, roofs): + h, w, _ = img.shape + lit, shadow = [], [] + for (x, y, ww) in roofs: + if x < 0 or x + ww > w or y - TILE < 0 or y + TILE > h or ww < 2: + continue + half = ww // 2 + lit.append(luma(img[y - TILE:y + TILE, x:x + half]).mean()) + shadow.append(luma(img[y - TILE:y + TILE, x + half:x + ww]).mean()) + return lit, shadow + + def main(argv): if len(argv) != 3: sys.stderr.write(__doc__) @@ -60,95 +143,80 @@ def main(argv): sys.stderr.write(f"expected p47-day/night/close, found {frames}\n") return 1 - images = {n: luma(record_dir / f"{n}.png") for n in frames} failures = [] - lines = ["P47 scene-detail analysis (LV_P47_SHOT=1)", ""] - - # visible reflections (unique source tiles) and their dst tiles. - seen = set() - reflections = [] - for line in log_text.splitlines(): - m = REFLECTION_RE.search(line) - if m: - sx, sy, dx, dy, alpha = (int(m.group(i)) for i in range(1, 6)) - if (sx, sy, dx, dy) in seen: - continue - seen.add((sx, sy, dx, dy)) - reflections.append((sx, sy, dx, dy, alpha)) - - # 1) reflection luma correlation: bank column profile vs vertically-mirrored band. - lines.append("1) 倒影区 luma 相关性 (bank column profile vs vertically-mirrored water band)") - for name in ["p47-day", "p47-night"]: - img = images[name] - above, below = [], [] - used = set() - for (sx, sy, _, _, _) in reflections: - if (sx, sy) in used: - continue - used.add((sx, sy)) - half, up, down = 64, TILE, 3 * TILE - if sx - half < 0 or sx + half > img.shape[1] or sy + up + down > img.shape[0]: - continue - above.append(img[sy:sy + up, sx - half:sx + half].mean(axis=0)) - below.append(img[sy + up:sy + up + down, sx - half:sx + half].mean(axis=0)) - if not above: - failures.append(f"{name}: no fully-visible reflection to correlate") - lines.append(f" {name}: no fully-visible reflection") + lines = ["P47 scene-detail analysis (LV_P47_SHOT=1, leader rework)", ""] + + # 1) reflection streaks: vertical length + warm vertical column vs background water. + lines.append("1) 倒影:垂直条纹长度 + 相对背景水面的 luma/暖色差") + for name in frames: + img = rgb(record_dir / f"{name}.png") + reflections, _ = parse_log(log_text, name) + run = max_vertical_run(reflections) + metrics = reflection_metrics(img, reflections) + if not metrics: + failures.append(f"{name}: no visible reflection tile to measure") + lines.append(f" {name}: no visible reflection tile") continue - a = np.concatenate(above) - b = np.concatenate(below) - a = a - a.mean() - b = b - b.mean() - r = float(np.corrcoef(a, b)[0, 1]) if a.std() > 0 and b.std() > 0 else 0.0 - lines.append(f" {name}: pearson r = {r:.3f} (required >= {REFLECTION_CORR_MIN})") - if r < REFLECTION_CORR_MIN: - failures.append(f"{name} reflection correlation {r:.3f} < {REFLECTION_CORR_MIN}") - - # reflection adds warm light vs neighbouring open water. - night = images["p47-night"] - deltas = [] - for (_, _, dx, dy, _) in reflections: - if dy + TILE > night.shape[0] or dx - 96 < 0: + dl = float(np.mean([m[0] for m in metrics])) + warm = float(np.mean([m[1] for m in metrics])) + lines.append( + f" {name}: tiles={len(reflections)} max_vertical_run={run} (required >= {MIN_VERTICAL_RUN}) " + f"luma_delta={dl:+.2f} (required >= {REFLECTION_DL_MIN}) warm_rb_delta={warm:+.2f} (required >= {REFLECTION_WARM_MIN})") + if run < MIN_VERTICAL_RUN: + failures.append(f"{name} vertical reflection run {run} < {MIN_VERTICAL_RUN}") + if dl < REFLECTION_DL_MIN: + failures.append(f"{name} reflection luma delta {dl:.2f} < {REFLECTION_DL_MIN}") + if warm < REFLECTION_WARM_MIN: + failures.append(f"{name} reflection warm delta {warm:.2f} < {REFLECTION_WARM_MIN}") + lines.append("") + + # 2) foreground roofs: one lit face + one shadow face (>= two luma tiers). + lines.append("2) 近景屋顶受光面 / 背光面 luma 差") + for name in frames: + img = rgb(record_dir / f"{name}.png") + _, roofs = parse_log(log_text, name) + lit, shadow = roof_metrics(img, roofs) + if not lit: + failures.append(f"{name}: no fully-visible foreground roof to measure") + lines.append(f" {name}: no fully-visible roof") continue - refl = night[dy:dy + TILE, dx:dx + TILE].mean() - ctrl = night[dy:dy + TILE, dx - 96:dx - 96 + TILE].mean() - deltas.append(float(refl - ctrl)) - if deltas: - lines.append(f" reflection vs control water luma delta (night) = {np.mean(deltas):.2f} (required >= {REFLECTION_DELTA_MIN})") - if float(np.mean(deltas)) < REFLECTION_DELTA_MIN: - failures.append(f"reflection luma delta {np.mean(deltas):.2f} < {REFLECTION_DELTA_MIN}") - else: - failures.append("no visible reflection tile to measure luma delta") + delta = float(np.mean(lit) - np.mean(shadow)) + lines.append( + f" {name}: roofs={len(lit)} lit={np.mean(lit):.2f} shadow={np.mean(shadow):.2f} " + f"delta={delta:+.2f} (required >= {ROOF_DELTA_MIN})") + if delta < ROOF_DELTA_MIN: + failures.append(f"{name} roof lit/shadow delta {delta:.2f} < {ROOF_DELTA_MIN}") lines.append("") - # 2) element-count metrics. - lines.append("2) 元素数量指标") + # 3) element-count metrics. + lines.append("3) 元素数量指标") seen_counts = set() for line in log_text.splitlines(): m = ELEMENTS_RE.search(line) if m: - counts = tuple(int(m.group(i)) for i in range(1, 5)) + counts = tuple(int(m.group(i)) for i in range(1, 6)) if counts in seen_counts: continue seen_counts.add(counts) - lines.append(f" reeds={counts[0]} floats={counts[1]} reflections={counts[2]} sources={counts[3]}") + lines.append(f" reeds={counts[0]} floats={counts[1]} reflections={counts[2]} sources={counts[3]} maxstreak={counts[4]}") if not seen_counts: failures.append("no p47-elements line in log") else: - reeds, floats, refl, sources = next(iter(seen_counts)) + reeds, floats, refl, sources, maxstreak = min(seen_counts) if min(reeds, floats, refl, sources) <= 0: failures.append("element counts must all be positive") + if maxstreak < MIN_VERTICAL_RUN: + failures.append(f"logged maxstreak {maxstreak} < {MIN_VERTICAL_RUN}") lines.append("") - # 3) no big black block / no white block / opaque. - lines.append("3) 无大黑块 / 无白块 / 不透明校验") - names = ["p47-day", "p47-night", "p47-close"] - for name in names: + # 4) no big black block / no white block / opaque. + lines.append("4) 无大黑块 / 无白块 / 不透明校验") + for name in frames: a = np.asarray(Image.open(record_dir / f"{name}.png").convert("RGBA"), dtype=np.uint8) - rgb = a[:, :, :3] + rgbv = a[:, :, :3] alpha_min = int(a[:, :, 3].min()) - black = (rgb.max(axis=2) < 6) - white = (rgb.min(axis=2) >= 250) + black = (rgbv.max(axis=2) < 6) + white = (rgbv.min(axis=2) >= 250) bb = has_solid_block(black, MIN_BLOCK) wb = has_solid_block(white, MIN_BLOCK) lines.append(f" {name}: alpha_min={alpha_min} black_px={int(black.sum())} black_block={bb} white_block={wb}") |
