mirror of
https://git.k-space.ee/arti/vt1200.git
synced 2026-11-01 12:14:56 +02:00
feat(badapple): stylise the clip into blocks, and let the sender widen its keyframe cadence
Two knobs for the 60 fps attempt, both measured on Bad Apple: clip_from_video.py --block N averages each NxN block, thresholds the average and draws the block flat. The point is destination words: at 60 fps the clip changes 80 tiles/frame unstylised, 54 at 8x8 and 29 at 16x16, against the ~44-tile budget that MAME's ~3.4 us/word allows in 16.7 ms. A side effect worth knowing: with 16-pixel blocks every 32x32 tile is one of exactly 2^4 combinations, so the 256 entry dictionary represents the stylised clip with zero error (measured: 16 entries used, "reconstruction 0 bit(s)", PSNR inf, mean delta 141 B against 496 B for the unstylised clip). vidsend.py --keyframe-every N exposes the cadence that TileSender already had as a constructor argument. A keyframe repaints all 1,280 tiles (~264 ms of MAME CPU at the measured 0.22 ms/tile), so the default 60-frame cadence costs ~4.4 ms on average per frame at 60 fps -- a tenth of the budget spent on repairs. The 13,167-frame stylised clip ships 220 keyframes at the default; 600 measured 6 in the same window. Measured on the stylised 60 fps cache (13,167 frames, 16 dictionary entries): --fps 60, default cadence 643 of 805 accepted (80%), drops=8 --fps 45 484 of 520 accepted, drops=0, sender PASS --fps 60, --keyframe-every 600 451 of 519 accepted, drops=5 So the stylised clip roughly doubles acceptance against the same 60 fps send (40% -> 80%) but MAME still caps the clean rate around 36-45 fps.
This commit is contained in:
1 parent
9139322892
commit
c5526332f0
2 files changed
+27
-4
No files matched your search
@@ -42,7 +42,7 @@ def pack_row(pixels) -> bytes:
|
||||
|
||||
|
||||
def frames(video, out_dir, fps=15, width=ROW_PIXELS, height=960, threshold=127,
|
||||
invert=False):
|
||||
invert=False, block=1):
|
||||
"""Yield ``(frame_no, packed_row_bytes)`` for every video frame."""
|
||||
import numpy as np
|
||||
|
||||
@@ -61,7 +61,18 @@ def frames(video, out_dir, fps=15, width=ROW_PIXELS, height=960, threshold=127,
|
||||
if len(buf) < size:
|
||||
break
|
||||
gray = np.frombuffer(buf, dtype=np.uint8).reshape(height, width)
|
||||
yield n, np.packbits(gray > threshold, axis=1, bitorder="little")
|
||||
if block > 1:
|
||||
# Stylise: average each block, threshold the average, then draw
|
||||
# the block flat. Fewer, larger shapes mean fewer changed tiles
|
||||
# per frame -- measured on Bad Apple at 60 fps: 80 changed tiles
|
||||
# unstylised, 54 at 8x8, 29 at 16x16, against a budget of ~44.
|
||||
h, w = height // block, width // block
|
||||
small = gray.reshape(h, block, w, block).mean(axis=(1, 3))
|
||||
bits = np.repeat(np.repeat(small > threshold, block, axis=0),
|
||||
block, axis=1)
|
||||
else:
|
||||
bits = gray > threshold
|
||||
yield n, np.packbits(bits, axis=1, bitorder="little")
|
||||
n += 1
|
||||
finally:
|
||||
proc.stdout.close()
|
||||
@@ -84,6 +95,8 @@ def main(argv=None) -> int:
|
||||
ap.add_argument("--threshold", type=int, default=127,
|
||||
help="grey level above which a pixel is ink "
|
||||
"(default %(default)s)")
|
||||
ap.add_argument("--block", type=int, default=1,
|
||||
help="stylise to NxN blocks (default 1 = no stylisation)")
|
||||
ap.add_argument("--invert", action="store_true",
|
||||
help="treat bright pixels as paper (negate first)")
|
||||
args = ap.parse_args(argv)
|
||||
@@ -95,6 +108,9 @@ def main(argv=None) -> int:
|
||||
print("error: --width must be a multiple of 8, at most %d" % ROW_PIXELS,
|
||||
file=sys.stderr)
|
||||
return 2
|
||||
if args.block < 1 or args.block > 64:
|
||||
print("error: --block must be 1..64", file=sys.stderr)
|
||||
return 2
|
||||
if args.height % 32 or args.height > 1024:
|
||||
print("error: --height must be a multiple of 32, at most 1024",
|
||||
file=sys.stderr)
|
||||
@@ -108,7 +124,8 @@ def main(argv=None) -> int:
|
||||
written = 0
|
||||
for n, packed in frames(args.video, args.out, fps=args.fps,
|
||||
width=args.width, height=args.height,
|
||||
threshold=args.threshold, invert=args.invert):
|
||||
threshold=args.threshold, invert=args.invert,
|
||||
block=args.block):
|
||||
path = os.path.join(args.out, "%05d.bin" % n)
|
||||
with open(path, "wb") as handle:
|
||||
handle.write(packed.tobytes())
|
||||
|
||||
@@ -710,6 +710,11 @@ def main(argv=None) -> int:
|
||||
help="stop after N frames (0 = until interrupted)")
|
||||
ap.add_argument("--pattern", default="bars", choices=sorted(PATTERNS),
|
||||
help="test picture (default bars)")
|
||||
ap.add_argument("--keyframe-every", type=int, default=KEYFRAME_EVERY,
|
||||
metavar="N",
|
||||
help="force a full frame every N frames (default %(default)s; "
|
||||
"a keyframe repaints all 1,280 tiles, so a short cadence "
|
||||
"is a large per-frame cost at high rates)")
|
||||
ap.add_argument("--chunk", type=int, default=CHUNK,
|
||||
help="bytes of frame per datagram (default %d)" % CHUNK)
|
||||
ap.add_argument("--dst", type=parse_mac, default=BROADCAST,
|
||||
@@ -788,7 +793,8 @@ def main(argv=None) -> int:
|
||||
fps=args.fps, frames=args.frames or None,
|
||||
chunk=args.chunk, dst=args.dst,
|
||||
report_every=report_every,
|
||||
status_wait=args.status_wait)
|
||||
status_wait=args.status_wait,
|
||||
every=args.keyframe_every)
|
||||
return Sender(transport, src=src, receive=receive, rows=args.rows,
|
||||
stretch=args.stretch, fps=args.fps,
|
||||
frames=args.frames or None, pattern=args.pattern,
|
||||
|
||||
Reference in new issue
Block a user