"""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)