diff options
Diffstat (limited to 'scripts/check-no-white-blocks.py')
| -rw-r--r-- | scripts/check-no-white-blocks.py | 93 |
1 files changed, 93 insertions, 0 deletions
diff --git a/scripts/check-no-white-blocks.py b/scripts/check-no-white-blocks.py new file mode 100644 index 0000000..609c832 --- /dev/null +++ b/scripts/check-no-white-blocks.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +"""Fail if any recorded frame contains a near-white opaque block. + +This is the image-level regression guard for the P13/P14 lantern blow-out: a +premultiplied-alpha mistake painted ~75x75 pure white (255,255,255) squares +where the night lantern glow should fall off softly. + +A "white block" is any window of at least MIN_SIDE x MIN_SIDE pixels where every +pixel is fully opaque (alpha >= 250) and every colour channel is >= 250. + +Usage: + python3 scripts/check-no-white-blocks.py <png-or-dir> [more...] + +Exit code 0 when clean, 1 when a block is found, 2 on usage/read error. +""" + +import os +import sys + +import numpy as np +from PIL import Image + +MIN_SIDE = 64 +CHANNEL_MIN = 250 +ALPHA_MIN = 250 + + +def first_white_block(path, min_side=MIN_SIDE): + image = Image.open(path) + rgba = np.asarray(image.convert("RGBA"), dtype=np.uint8) + if rgba.shape[0] < min_side or rgba.shape[1] < min_side: + return None + white_opaque = ( + (rgba[:, :, 0] >= CHANNEL_MIN) + & (rgba[:, :, 1] >= CHANNEL_MIN) + & (rgba[:, :, 2] >= CHANNEL_MIN) + & (rgba[:, :, 3] >= ALPHA_MIN) + ).astype(np.int64) + # Summed-area table so any k x k window is one lookup. + integral = np.zeros((white_opaque.shape[0] + 1, white_opaque.shape[1] + 1), dtype=np.int64) + integral[1:, 1:] = white_opaque.cumsum(0).cumsum(1) + rows, cols = white_opaque.shape + for y in range(0, rows - min_side + 1): + for x in range(0, cols - min_side + 1): + total = ( + integral[y + min_side, x + min_side] + - integral[y, x + min_side] + - integral[y + min_side, x] + + integral[y, x] + ) + if total == min_side * min_side: + return (x, y, min_side, min_side) + return None + + +def iter_pngs(targets): + for target in targets: + if os.path.isdir(target): + for root, _, files in os.walk(target): + for name in sorted(files): + if name.lower().endswith(".png"): + yield os.path.join(root, name) + else: + yield target + + +def main(argv): + if len(argv) < 2: + sys.stderr.write(__doc__) + return 2 + checked = 0 + failures = [] + for path in iter_pngs(argv[1:]): + try: + block = first_white_block(path) + except Exception as error: # noqa: BLE001 - report and continue + sys.stderr.write(f"unreadable: {path}: {error}\n") + return 2 + checked += 1 + if block is not None: + x, y, side, _ = block + failures.append(f"{path}: white block {side}x{side} at ({x},{y})") + for line in failures: + print(f"FAIL {line}") + if failures: + print(f"checked={checked} failures={len(failures)}") + return 1 + print(f"checked={checked} failures=0 OK") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv)) |
