Files
vt1200/bad-apple/pipeline/tilepack.py
T

82 lines
2.8 KiB
Python

"""Turn binary frames into per-tile background + rectangle deltas."""
from __future__ import annotations
import numpy as np
from .movie import FrameRecord, Rect, TileRecord
def _row_runs(row: np.ndarray) -> list[tuple[int, int]]:
"""Inclusive (start, end) runs of True in one row."""
padded = np.concatenate(([0], row.astype(np.int8), [0]))
diff = np.diff(padded)
starts = np.where(diff == 1)[0]
ends = np.where(diff == -1)[0] - 1
return list(zip(starts.tolist(), ends.tolist()))
def decompose_tile(tile: np.ndarray) -> tuple[int, list[Rect]]:
"""Majority background plus rectangles of the minority colour."""
tile = tile.astype(bool)
if tile.ndim != 2:
raise ValueError("a tile must be 2-D")
height, width = tile.shape
black = int(tile.sum())
bg = 1 if black > tile.size // 2 else 0
fg = tile if bg == 0 else ~tile
active: dict[tuple[int, int], int] = {}
rects: list[Rect] = []
for y in range(height):
runs = set(_row_runs(fg[y]))
for key in list(active):
if key not in runs:
y0 = active.pop(key)
rects.append(Rect(key[0], y0, key[1] - key[0] + 1, y - y0))
for key in runs:
if key not in active:
active[key] = y
for key, y0 in active.items():
rects.append(Rect(key[0], y0, key[1] - key[0] + 1, height - y0))
rects.sort(key=lambda r: (r.y0, r.x0))
return bg, rects
def diff_tiles(prev: np.ndarray, cur: np.ndarray,
tile: int = 32) -> list[TileRecord]:
"""The tiles whose pixels changed, with the new tile's rectangle list."""
if prev.shape != cur.shape:
raise ValueError("prev and cur must have the same shape")
height, width = cur.shape
if height % tile or width % tile:
raise ValueError("frame dimensions must be a multiple of the tile")
tiles_x = width // tile
out: list[TileRecord] = []
for ty in range(height // tile):
for tx in range(tiles_x):
y = ty * tile
x = tx * tile
old = prev[y:y + tile, x:x + tile]
new = cur[y:y + tile, x:x + tile]
if np.array_equal(old, new):
continue
bg, rects = decompose_tile(new)
out.append(TileRecord(ty * tiles_x + tx, bg, rects))
return out
def apply_record(canvas: np.ndarray, record: FrameRecord,
tile: int = 32) -> None:
"""Apply one frame record to a logical canvas in place."""
height, width = canvas.shape
tiles_x = width // tile
for rec in record.tiles:
ty, tx = divmod(rec.tile_index, tiles_x)
y = ty * tile
x = tx * tile
block = canvas[y:y + tile, x:x + tile]
block[:, :] = bool(rec.bg)
for rect in rec.rects:
block[rect.y0:rect.y0 + rect.h,
rect.x0:rect.x0 + rect.w] = not bool(rec.bg)