Spaces:
Running
Running
| """``fdp`` 命令列入口。 | |
| 子命令(依 pipeline 順序): | |
| extract 影片(或目錄)→ keypoint cache parquet【唯一 GPU 步驟】 | |
| detect cache → events.json(純 CPU,毫秒級;--debug 另輸出特徵 JSONL) | |
| annotate 影片 + cache → 標註影片(H.264)+ events.json | |
| pipeline extract → detect → annotate 一條龍(--source 0 可用 webcam 錄一段再處理) | |
| bench 影片 → FPS benchmark(可攜:任何機器都能補跑一列,不綁 Colab) | |
| 重依賴(torch/ultralytics/cv2)一律延遲到子命令內 import: | |
| ``fdp detect`` 在只裝核心依賴的環境(無 GPU)也能跑。 | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import sys | |
| from pathlib import Path | |
| VIDEO_EXTS = {".mp4", ".avi", ".mov", ".mkv"} | |
| def _write_debug_jsonl(path: str | Path, records: list[dict]) -> None: | |
| path = Path(path) | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| with open(path, "w", encoding="utf-8") as f: | |
| for rec in records: | |
| f.write(json.dumps(rec, ensure_ascii=False) + "\n") | |
| def _run_rules(cache_path: str, config_path: str, collect_debug: bool): | |
| from .config import load_config | |
| from .io.cache import read_cache | |
| from .rules import run_engine | |
| cfg = load_config(config_path) | |
| df, meta = read_cache(cache_path) | |
| events, debug = run_engine(df, meta.fps, cfg, collect_debug=collect_debug) | |
| return cfg, df, meta, events, debug | |
| def cmd_extract(args: argparse.Namespace) -> int: | |
| """影片 → keypoint cache。--source 可為單一影片或裝滿影片的目錄。""" | |
| from .config import load_config | |
| from .inference.extract import extract_batch, extract_video | |
| cfg = load_config(args.config) | |
| if args.model: | |
| cfg.model.name = args.model | |
| src = Path(args.source) | |
| if src.is_dir(): | |
| videos = sorted(p for p in src.iterdir() if p.suffix.lower() in VIDEO_EXTS) | |
| if not videos: | |
| print(f"{src} 內沒有影片", file=sys.stderr) | |
| return 1 | |
| extract_batch(videos, args.out, cfg, device=args.device) | |
| else: | |
| out = Path(args.out) | |
| out_path = out / f"{src.stem}.parquet" if out.suffix == "" else out | |
| meta = extract_video(src, out_path, cfg, device=args.device) | |
| print(f"cache → {out_path}({meta.n_frames} 幀,model={meta.model_name})") | |
| return 0 | |
| def cmd_detect(args: argparse.Namespace) -> int: | |
| """cache → events.json;--debug 另存 per-frame 特徵 JSONL(失敗分析用)。""" | |
| from .events.schema import write_events_json | |
| cfg, _, meta, events, debug = _run_rules(args.cache, args.config, args.debug is not None) | |
| write_events_json(args.out, events, source=meta.video_path, fps=meta.fps) | |
| if args.debug: | |
| _write_debug_jsonl(args.debug, debug) | |
| print(f"{Path(args.cache).stem}: {len(events)} 個事件 → {args.out}") | |
| for ev in events: | |
| print( | |
| f" tracks={ev.track_ids} frames=[{ev.start_frame},{ev.end_frame}] " | |
| f"t=[{ev.start_time_s:.2f},{ev.end_time_s:.2f}]s rules={ev.rules_fired}" | |
| ) | |
| return 0 | |
| def cmd_annotate(args: argparse.Namespace) -> int: | |
| """影片 + cache → 標註影片(H.264);同時輸出 events.json。""" | |
| from .events.schema import write_events_json | |
| from .viz.annotate import annotate_video | |
| cfg, df, meta, events, debug = _run_rules(args.cache, args.config, True) | |
| out = annotate_video(args.video, df, meta.fps, cfg, events, debug, args.out) | |
| if args.events_out: | |
| write_events_json(args.events_out, events, source=str(args.video), fps=meta.fps) | |
| print(f"標註影片 → {out}({len(events)} 個事件)") | |
| return 0 | |
| def _record_webcam(index: int, duration_s: float, out_path: Path) -> Path: | |
| """webcam 錄一段到暫存影片(離線 pipeline 的 webcam 模式)。""" | |
| import cv2 | |
| cap = cv2.VideoCapture(index) | |
| if not cap.isOpened(): | |
| raise RuntimeError(f"無法開啟 webcam {index}") | |
| fps = cap.get(cv2.CAP_PROP_FPS) or 30.0 | |
| w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) | |
| h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) | |
| out_path.parent.mkdir(parents=True, exist_ok=True) | |
| writer = cv2.VideoWriter(str(out_path), cv2.VideoWriter_fourcc(*"mp4v"), fps, (w, h)) | |
| n = int(duration_s * fps) | |
| print(f"webcam 錄製 {duration_s:.0f}s({n} 幀)…") | |
| for _ in range(n): | |
| ok, frame = cap.read() | |
| if not ok: | |
| break | |
| writer.write(frame) | |
| cap.release() | |
| writer.release() | |
| return out_path | |
| def cmd_pipeline(args: argparse.Namespace) -> int: | |
| """extract → detect → annotate 一條龍,輸出標註影片與 events.json。""" | |
| import tempfile | |
| from .config import load_config | |
| from .events.schema import write_events_json | |
| from .inference.extract import extract_video | |
| from .rules import run_engine | |
| from .viz.annotate import annotate_video | |
| cfg = load_config(args.config) | |
| if args.model: | |
| cfg.model.name = args.model | |
| if str(args.source).isdigit(): | |
| video = _record_webcam( | |
| int(args.source), args.duration, Path(tempfile.mkstemp(suffix=".mp4")[1]) | |
| ) | |
| else: | |
| video = Path(args.source) | |
| out_dir = Path(args.out_dir) | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| cache_path = out_dir / f"{video.stem}.parquet" | |
| extract_video(video, cache_path, cfg, device=args.device) | |
| from .io.cache import read_cache | |
| df, meta = read_cache(cache_path) | |
| events, debug = run_engine(df, meta.fps, cfg, collect_debug=True) | |
| events_path = out_dir / f"{video.stem}.events.json" | |
| write_events_json(events_path, events, source=str(video), fps=meta.fps) | |
| if args.debug: | |
| _write_debug_jsonl(out_dir / f"{video.stem}.debug.jsonl", debug) | |
| annotated = annotate_video( | |
| video, df, meta.fps, cfg, events, debug, out_dir / f"{video.stem}_annotated.mp4" | |
| ) | |
| print(f"完成:{annotated}、{events_path}({len(events)} 個事件)") | |
| return 0 | |
| def cmd_bench(args: argparse.Namespace) -> int: | |
| """影片 → FPS benchmark(純推論 + 端到端 FPS、p50/p95 延遲)。 | |
| 可攜:任何機器都能對同一支(或任一支)影片補跑一列,不綁定 Colab—— | |
| --model 可重複指定多次,一次跑完整個模型清單。 | |
| """ | |
| import platform | |
| from .bench.benchmark import benchmark, load_frames | |
| frames = load_frames(args.video, n_frames=args.n_frames) | |
| print(f"{args.video}: 載入 {len(frames)} 幀(要求 {args.n_frames})") | |
| results = [] | |
| for model_name in args.model: | |
| r = benchmark( | |
| frames, | |
| model_name=model_name, | |
| device=args.device, | |
| quantize=args.quantize, | |
| n_runs=args.n_runs, | |
| warmup=args.warmup, | |
| ) | |
| results.append(r.to_dict()) | |
| print( | |
| f" {model_name}: 純推論 {r.pure_inference_fps} FPS、端到端 {r.end_to_end_fps} FPS、" | |
| f"p50 {r.p50_latency_ms}ms、p95 {r.p95_latency_ms}ms" | |
| ) | |
| if args.out: | |
| import torch | |
| import ultralytics | |
| payload = { | |
| "video": str(args.video), | |
| "platform": platform.platform(), | |
| "torch_version": torch.__version__, | |
| "ultralytics_version": ultralytics.__version__, | |
| "gpu": torch.cuda.get_device_name(0) if torch.cuda.is_available() else None, | |
| "results": results, | |
| } | |
| Path(args.out).parent.mkdir(parents=True, exist_ok=True) | |
| Path(args.out).write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8") | |
| print(f"→ {args.out}") | |
| return 0 | |
| def build_parser() -> argparse.ArgumentParser: | |
| parser = argparse.ArgumentParser(prog="fdp", description=__doc__) | |
| sub = parser.add_subparsers(dest="command", required=True) | |
| p = sub.add_parser("extract", help="影片 → keypoint cache(GPU)") | |
| p.add_argument("--source", required=True, help="影片檔或影片目錄") | |
| p.add_argument("--out", required=True, help="輸出 parquet 或目錄") | |
| p.add_argument("--config", default="config.yaml") | |
| p.add_argument("--model", default=None, help="覆寫 config 的模型名(如 yolo26s-pose.pt)") | |
| p.add_argument("--device", default=None, help="cuda:0 / cpu(預設由 ultralytics 自選)") | |
| p.set_defaults(func=cmd_extract) | |
| p = sub.add_parser("detect", help="cache → events.json(純 CPU)") | |
| p.add_argument("--cache", required=True) | |
| p.add_argument("--out", required=True, help="events.json 輸出路徑") | |
| p.add_argument("--config", default="config.yaml") | |
| p.add_argument("--debug", default=None, help="per-frame 特徵 JSONL 輸出路徑") | |
| p.set_defaults(func=cmd_detect) | |
| p = sub.add_parser("annotate", help="影片 + cache → 標註影片(H.264)") | |
| p.add_argument("--video", required=True) | |
| p.add_argument("--cache", required=True) | |
| p.add_argument("--out", required=True, help="標註影片輸出路徑") | |
| p.add_argument("--events-out", default=None, help="events.json 輸出路徑(可選)") | |
| p.add_argument("--config", default="config.yaml") | |
| p.set_defaults(func=cmd_annotate) | |
| p = sub.add_parser("pipeline", help="extract → detect → annotate 一條龍") | |
| p.add_argument("--source", required=True, help="影片檔,或 webcam 索引(如 0)") | |
| p.add_argument("--out-dir", default="outputs") | |
| p.add_argument("--config", default="config.yaml") | |
| p.add_argument("--model", default=None) | |
| p.add_argument("--device", default=None) | |
| p.add_argument("--duration", type=float, default=10.0, help="webcam 錄製秒數") | |
| p.add_argument("--debug", action="store_true", help="輸出 per-frame 特徵 JSONL") | |
| p.set_defaults(func=cmd_pipeline) | |
| p = sub.add_parser("bench", help="影片 → FPS benchmark(可攜,任何機器都能補跑)") | |
| p.add_argument("--video", required=True, help="固定用來計時的影片") | |
| p.add_argument("--model", action="append", required=True, help="模型名,可重複指定多次") | |
| p.add_argument("--device", default=None, help="cuda:0 / cpu(預設由 ultralytics 自選)") | |
| p.add_argument("--quantize", default=None, help="16 或 fp16 啟用 FP16 推論(僅 GPU 有意義)") | |
| p.add_argument("--n-frames", type=int, default=300, dest="n_frames") | |
| p.add_argument("--n-runs", type=int, default=3, dest="n_runs") | |
| p.add_argument("--warmup", type=int, default=20) | |
| p.add_argument("--out", default=None, help="bench.json 輸出路徑(可選)") | |
| p.set_defaults(func=cmd_bench) | |
| return parser | |
| def main(argv: list[str] | None = None) -> int: | |
| args = build_parser().parse_args(argv) | |
| return args.func(args) | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |