From a5c793f39cae932a83f0dde95de192f7232f5a32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=27=C3=A9lectron=20rare?= <108685187+electron-rare@users.noreply.github.com> Date: Wed, 13 May 2026 22:36:19 +0200 Subject: [PATCH] feat(data-only-viz): action capture webcam script Implement Task 10 of action-head plan. Records webcam frames + timestamps for action-head training using OpenCV. Outputs MP4 video + timestamp text file to ~/.cache/av-live-action/raw/ with 672x672 square crop, configurable fps/session/camera, interactive q-quit. --- data_only_viz/scripts/capture_actions.py | 74 ++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 data_only_viz/scripts/capture_actions.py diff --git a/data_only_viz/scripts/capture_actions.py b/data_only_viz/scripts/capture_actions.py new file mode 100644 index 0000000..331858d --- /dev/null +++ b/data_only_viz/scripts/capture_actions.py @@ -0,0 +1,74 @@ +"""Record webcam frames + timestamps for action-head training. + +Usage: + uv run python -m data_only_viz.scripts.capture_actions \ + --session sess03 --duration 600 +""" +from __future__ import annotations + +import argparse +import logging +import time +from pathlib import Path + +import cv2 + +LOG = logging.getLogger("capture_actions") +RAW_DIR = Path("~/.cache/av-live-action/raw").expanduser() + + +def capture(session: str, duration_s: float, + cam_index: int = 0, fps: int = 30, + size: int = 672) -> Path: + RAW_DIR.mkdir(parents=True, exist_ok=True) + out = RAW_DIR / f"{session}.mp4" + ts_out = RAW_DIR / f"{session}.ts.txt" + cap = cv2.VideoCapture(cam_index) + if not cap.isOpened(): + raise RuntimeError(f"cannot open camera {cam_index}") + fourcc = cv2.VideoWriter_fourcc(*"mp4v") + writer = cv2.VideoWriter(str(out), fourcc, fps, (size, size)) + try: + t_start = time.perf_counter() + with ts_out.open("w") as ts_f: + n = 0 + while time.perf_counter() - t_start < duration_s: + ok, frame = cap.read() + if not ok: + LOG.warning("frame read failed") + break + h, w = frame.shape[:2] + side = min(h, w) + y0 = (h - side) // 2 + x0 = (w - side) // 2 + crop = frame[y0:y0 + side, x0:x0 + side] + resized = cv2.resize(crop, (size, size)) + writer.write(resized) + ts_f.write(f"{n} {time.perf_counter() - t_start:.6f}\n") + n += 1 + cv2.imshow("capture (q=quit)", resized) + if cv2.waitKey(1) & 0xFF == ord("q"): + break + LOG.info("wrote %s (%d frames)", out, n) + return out + finally: + cap.release() + writer.release() + cv2.destroyAllWindows() + + +def _cli() -> None: + p = argparse.ArgumentParser() + p.add_argument("--session", required=True) + p.add_argument("--duration", type=float, default=600.0) + p.add_argument("--cam-index", type=int, default=0) + p.add_argument("--fps", type=int, default=30) + args = p.parse_args() + logging.basicConfig(level=logging.INFO, + format="%(asctime)s [%(name)s] %(message)s") + capture(args.session, args.duration, + cam_index=args.cam_index, fps=args.fps) + + +if __name__ == "__main__": + _cli()