mirror of
https://git.k-space.ee/arti/vt1200.git
synced 2026-11-03 12:33:16 +02:00
82 lines
2.8 KiB
Python
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)
|