betty0's picture
Deploy fall-detection-pose Gradio demo
11fab85 verified
Raw
History Blame Contribute Delete
10.9 kB
"""``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())