count-model / run.py
RehamAAhmed's picture
Upload 8 files
67a0341 verified
Raw
History Blame Contribute Delete
13.9 kB
# -*- coding: utf-8 -*-
"""
run.py - Entry point for SAM 2 Robust Tracker
==============================================
Usage examples:
python run.py --video my_video.mp4 --prompt "person . car . dog"
python run.py --video my_video.mp4 --prompt "person . car" --fps 5 --chunk 100
python run.py --video my_video.mp4 --prompt prompts.txt
"""
import os
import sys
import argparse
import shutil
import torch
# Resolve SAM 2 checkpoint path
HOME_DIR = os.path.expanduser("~")
DEFAULT_SAM2_CKPT = os.path.join(
HOME_DIR, ".cache", "torch", "hub", "checkpoints", "sam2.1_hiera_small.pt"
)
DEFAULT_SAM2_CFG = "configs/sam2.1/sam2.1_hiera_s.yaml"
# Default detection prompt
DEFAULT_PROMPT = (
"# Structures & Openings\n"
"room door, door handle, wardrobe, window, stairs, socket, light switch\n\n"
"# Furniture\n"
"sofa, armchair, chair, table, bed, cabinet, bookcase, shelf, desk, nightstand\n\n"
"# Kitchen & Bathroom\n"
"sink, faucet, refrigerator, stove, oven, microwave, toilet, bathtub, shower\n\n"
"# Lighting & Decor\n"
"curtain, rug, lamp, painting, vase, clock, plant\n\n"
"# Electronics\n"
"tv, monitor, air conditioner, fan, laptop, phone, remote control"
)
# ──────────────────────────────────────────────────────────────────────────────
def parse_args():
p = argparse.ArgumentParser(
description="SAM 2 + Grounding DINO Robust Object Tracker",
formatter_class=argparse.RawTextHelpFormatter
)
p.add_argument("--video", required=True,
help="Path to input video file.")
p.add_argument("--prompt", default=DEFAULT_PROMPT,
help="Detection prompt: comma/dot separated labels, or path to .txt file.")
p.add_argument("--output", default="tracked_output.mp4",
help="Path for output annotated video. (default: tracked_output.mp4)")
p.add_argument("--fps", type=float, default=2.0,
help="Target processing FPS. 2.0 = 1 frame every 0.5s. (default: 2.0)")
p.add_argument("--max-size", type=int, default=720,
help="Longest edge in pixels after resize. (default: 720)")
p.add_argument("--chunk", type=int, default=120,
help="SAM 2 sliding window size in frames. (default: 120)\n"
" 4 GB VRAM → 80 | 6 GB → 120 | 8 GB+ → 200")
p.add_argument("--box-thresh", type=float, default=0.30,
help="DINO box confidence threshold. (default: 0.30)")
p.add_argument("--text-thresh", type=float, default=0.25,
help="DINO text confidence threshold. (default: 0.25)")
p.add_argument("--iou-thresh", type=float, default=0.45,
help="NMS IoU threshold. (default: 0.45)")
p.add_argument("--max-objects", type=int, default=15,
help="Max objects to track (VRAM guard). (default: 15)")
p.add_argument("--stabilize", action="store_true", default=False,
help="Apply ORB-based video stabilization before tracking.")
p.add_argument("--blur-thresh", type=float, default=60.0,
help="Skip blurry frames below this Laplacian variance. 0 = off.")
p.add_argument("--sam2-ckpt", default=DEFAULT_SAM2_CKPT,
help=f"Path to SAM 2 checkpoint .pt file.\n(default: {DEFAULT_SAM2_CKPT})")
p.add_argument("--sam2-cfg", default=DEFAULT_SAM2_CFG,
help=f"SAM 2 model config yaml.\n(default: {DEFAULT_SAM2_CFG})")
p.add_argument("--frames-dir", default="__frames_temp__",
help="Temp directory for extracted frames. (default: __frames_temp__)")
p.add_argument("--keep-frames", action="store_true", default=False,
help="Keep extracted frames on disk after tracking.")
return p.parse_args()
# ──────────────────────────────────────────────────────────────────────────────
def load_prompt(prompt_arg: str) -> str:
"""If prompt_arg is a path to a .txt file, read it; otherwise return as-is."""
if os.path.isfile(prompt_arg):
with open(prompt_arg, "r", encoding="utf-8") as f:
return f.read()
return prompt_arg
# ──────────────────────────────────────────────────────────────────────────────
def print_banner(args):
vram_str = (f"{torch.cuda.get_device_properties(0).total_memory // 1024**2} MB"
if torch.cuda.is_available() else "CPU mode")
print()
print("=" * 65)
print(" SAM 2 Robust Tracker - Grounding DINO + Sliding Window")
print("=" * 65)
print(f" Input : {args.video}")
print(f" Output : {args.output}")
print(f" FPS : {args.fps} (1 frame every {1/args.fps:.2f}s)")
print(f" Chunk : {args.chunk} frames per SAM-2 window")
print(f" MaxObj : {args.max_objects}")
print(f" MaxPx : {args.max_size}px")
print(f" Stab : {args.stabilize}")
print(f" VRAM : {vram_str}")
print("=" * 65)
print()
# ──────────────────────────────────────────────────────────────────────────────
def main():
args = parse_args()
prompt = load_prompt(args.prompt)
if not os.path.exists(args.video):
print(f"[ERROR] Video not found: {args.video}")
sys.exit(1)
if not os.path.exists(args.sam2_ckpt):
print(f"[ERROR] SAM 2 checkpoint not found: {args.sam2_ckpt}")
print(" Download with: python download_models.py")
sys.exit(1)
print_banner(args)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"[Device] {device}\n")
# ── Import our modules ───────────────────────────────────────────────────
from tracker import (
VideoFrameStore,
DinoDetector,
SAM2Tracker,
TrackedObject,
)
# ── Step 1: Extract frames ───────────────────────────────────────────────
print("-" * 50)
print("Step 1/3 - Extract Frames")
print("-" * 50)
store = VideoFrameStore(
video_path = args.video,
output_dir = args.frames_dir,
target_fps = args.fps,
max_size = args.max_size,
blur_threshold = args.blur_thresh,
stabilize = args.stabilize,
)
n_frames = store.extract()
if n_frames == 0:
print("[ERROR] No frames extracted.")
sys.exit(1)
print(f" → {n_frames} frames Âx {store.width}×{store.height}px\n")
# ── Step 2: DINO detection on frame 0 ────────────────────────────────────
print("-" * 50)
print("Step 2/3 - Initial Object Detection (DINO)")
print("-" * 50)
dino = DinoDetector(device)
dino.load()
first_frame = store.frame_paths[0]
boxes, scores, labels = dino.detect(
first_frame, prompt,
box_threshold = args.box_thresh,
text_threshold = args.text_thresh,
iou_threshold = args.iou_thresh,
)
if len(boxes) == 0:
print("[WARN] DINO found no objects in the first frame.")
print(" Tip: lower --box-thresh or widen your --prompt.")
sys.exit(1)
# Cap to max_objects (by score, highest first)
if len(boxes) > args.max_objects:
top_k = scores.argsort()[::-1][:args.max_objects]
boxes = boxes[top_k]
scores = scores[top_k]
labels = [labels[i] for i in top_k]
print(f" [Cap] Keeping top {args.max_objects} objects by confidence.")
# Build TrackedObject list
tracked_objects = []
for i, (box, score, label) in enumerate(zip(boxes, scores, labels)):
obj = TrackedObject(obj_id=i, label=label, box=box)
tracked_objects.append(obj)
print(f" [ID {i:2d}] {label:<30s} conf={score:.3f} box={box.astype(int).tolist()}")
print(f"\n → {len(tracked_objects)} objects registered.\n")
# ── Step 3: SAM 2 sliding-window tracking ────────────────────────────────
print("-" * 50)
print("Step 3/3 - SAM 2 Sliding-Window Tracking")
print("-" * 50)
sam2 = SAM2Tracker(
sam2_checkpoint = args.sam2_ckpt,
sam2_cfg = args.sam2_cfg,
device = device,
chunk_size = args.chunk,
)
sam2.load()
def progress(done, total):
pct = 100 * done / total
filled = int(pct // 2)
bar = "#" * filled + "-" * (50 - filled)
print(f"\r [{bar}] {pct:5.1f}% ({done}/{total} frames)", end="", flush=True)
result_labels = sam2.track_video(
frame_store = store,
tracked_objects = tracked_objects,
dino = dino,
prompt = prompt,
box_threshold = args.box_thresh,
text_threshold = args.text_thresh,
iou_threshold = args.iou_thresh,
output_path = args.output,
progress_cb = progress,
)
print()
# ── Cleanup ──────────────────────────────────────────────────────────────
if not args.keep_frames and os.path.exists(args.frames_dir):
shutil.rmtree(args.frames_dir, ignore_errors=True)
print(f"[Clean] Removed temp frames: {args.frames_dir}")
# ── Summary ──────────────────────────────────────────────────────────────
print()
print("=" * 65)
print(" DONE!")
print("=" * 65)
print(f" Output video : {os.path.abspath(args.output)}")
print(f" Tracked : {len(result_labels)} objects")
for i, label in enumerate(result_labels):
status = "OK" if not tracked_objects[i].lost else "LOST at end"
print(f" ID {i:2d} [{status}] {label}")
print("=" * 65)
if __name__ == "__main__":
main()