forked from arti/vt1200
106 lines
3.6 KiB
Python
106 lines
3.6 KiB
Python
"""CLI: video -> vector frames -> tile deltas -> movie.bin + manifest."""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
|
|
import numpy as np
|
|
|
|
from .cost import CostConstants, estimate_record_us, select_records
|
|
from .frames import extract_binary
|
|
from .movie import MovieHeader, write_movie
|
|
from .preview import write_previews
|
|
from .vectorize import vectorize_frame
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
|
|
def _stats(values) -> dict:
|
|
if not values:
|
|
return {"mean": 0.0, "p50": 0, "p95": 0, "max": 0}
|
|
arr = np.asarray(values, dtype=float)
|
|
return {
|
|
"mean": float(arr.mean()),
|
|
"p50": int(np.percentile(arr, 50)),
|
|
"p95": int(np.percentile(arr, 95)),
|
|
"max": int(arr.max()),
|
|
}
|
|
|
|
|
|
def build_movie(video: str, out_dir: str, params: dict | None = None) -> dict:
|
|
with open(os.path.join(HERE, "params.json"), encoding="utf-8") as handle:
|
|
p = json.load(handle)
|
|
if params:
|
|
p.update(params)
|
|
|
|
frames = []
|
|
for index, frame in enumerate(extract_binary(
|
|
video, p["start"], p["duration"], p["fps"],
|
|
p["width"], p["height"], p)):
|
|
frames.append(vectorize_frame(frame, p))
|
|
if (index + 1) % 60 == 0:
|
|
print(" vectorised %d frames" % (index + 1))
|
|
if not frames:
|
|
raise RuntimeError("no frames extracted from %s" % video)
|
|
|
|
constants = CostConstants()
|
|
records = select_records(frames, constants, budget_us=13900.0, max_skip=2)
|
|
|
|
height, width = frames[0].shape
|
|
header = MovieHeader(
|
|
width=width, height=height, scale=int(p.get("scale", 2)),
|
|
tile=int(p.get("tile", 32)), tiles_x=width // int(p.get("tile", 32)),
|
|
tiles_y=height // int(p.get("tile", 32)), frame_count=len(records),
|
|
source_fps=int(p.get("fps", 60)), display_hz=72)
|
|
|
|
os.makedirs(out_dir, exist_ok=True)
|
|
data_bytes = write_movie(os.path.join(out_dir, "movie.bin"), records, header)
|
|
|
|
dirty = [len(record.tiles) for record in records]
|
|
rects = [sum(len(tile.rects) for tile in record.tiles) for record in records]
|
|
costs = [estimate_record_us(record, constants) for record in records]
|
|
manifest = {
|
|
"video": video,
|
|
"data_bytes": data_bytes,
|
|
"frame_count": len(records),
|
|
"source_frames": len(frames),
|
|
"dropped": len(frames) - len(records),
|
|
"width": width,
|
|
"height": height,
|
|
"scale": header.scale,
|
|
"tile": header.tile,
|
|
"source_fps": header.source_fps,
|
|
"display_hz": header.display_hz,
|
|
"dirty_tiles": _stats(dirty),
|
|
"rects": _stats(rects),
|
|
"cost_us": _stats(costs),
|
|
}
|
|
with open(os.path.join(out_dir, "manifest.json"), "w",
|
|
encoding="utf-8") as handle:
|
|
json.dump(manifest, handle, indent=2, sort_keys=True)
|
|
write_previews(os.path.join(out_dir, "preview"), frames,
|
|
[0, 60, 300, 900, 1799])
|
|
return manifest
|
|
|
|
|
|
def main(argv=None) -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--video", required=True)
|
|
parser.add_argument("--out", default="data")
|
|
parser.add_argument("--duration", type=float, default=None)
|
|
parser.add_argument("--start", type=float, default=None)
|
|
args = parser.parse_args(argv)
|
|
overrides = {}
|
|
if args.duration is not None:
|
|
overrides["duration"] = args.duration
|
|
if args.start is not None:
|
|
overrides["start"] = args.start
|
|
manifest = build_movie(args.video, args.out, overrides)
|
|
print(json.dumps(manifest, indent=2, sort_keys=True))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|