"""Geo-trax vehicle detector for Hugging Face Spaces. A Gradio demo for the geo-trax YOLOv8s detector (rfonod/geo-trax). It detects vehicles in high-altitude, top-down (bird's-eye view) aerial/drone imagery. Two tabs: • Image: run detection on a single uploaded image. • Short video: run detection frame-by-frame on a short clip (capped for the free CPU tier). Primary classes (0–3): Car, Bus, Truck, Motorcycle — evaluated, reliable. Experimental classes (4–5): Pedestrian, Bicycle — trained but poor performance, not evaluated; available as opt-in but off by default. The full video → track → stabilize → georeference pipeline lives in the `geo-trax` package (https://github.com/rfonod/geo-trax); this Space is a detection-only showcase of the model. """ import tempfile from collections import Counter import cv2 import gradio as gr from huggingface_hub import hf_hub_download from ultralytics import YOLO # --- Model ----------------------------------------------------------------------------------- # Download the weights once into the HF hub cache (subsequent calls/imports reuse the cached # file instead of re-downloading), then keep a single shared model instance for all requests. MODEL_REPO, MODEL_FILE = "rfonod/geo-trax", "geotrax_hbb_yolov8s_1920_v1.pt" model = YOLO(hf_hub_download(repo_id=MODEL_REPO, filename=MODEL_FILE)) # Four evaluated classes (on by default) + two experimental classes (off by default). # Pedestrian (4) and bicycle (5) were trained but have poor performance and have not been # formally evaluated — available as opt-in only. SUPPORTED = {0: "Car", 1: "Bus", 2: "Truck", 3: "Motorcycle"} EXPERIMENTAL = {4: "Pedestrian", 5: "Bicycle"} ALL_SUPPORTED = {**SUPPORTED, **EXPERIMENTAL} CLASS_CHOICES = list(SUPPORTED.values()) # checked by default EXPERIMENTAL_CHOICES = list(EXPERIMENTAL.values()) # unchecked by default ALL_CLASS_CHOICES = list(ALL_SUPPORTED.values()) # Defaults mirror geo-trax's bundled config (geotrax/cfg/default.yaml → ultralytics:). DEFAULT_CONF, DEFAULT_IOU, MAX_DET = 0.25, 0.7, 1000 IMGSZ_CHOICES = ["640", "960", "1280", "1600", "1920"] # 1920 = the model's native resolution. # Slimmer annotations than Ultralytics' size-scaled defaults (which are thick on large frames). LINE_WIDTH, FONT_SIZE = 2, 16 # Video tab is capped so a run finishes in reasonable time on the free CPU tier. MAX_VIDEO_FRAMES = 90 # Example assets live in this Space's repo under examples/, but they are stored with Git LFS/Xet, # so the repo tree (and the Space container) holds a small text *pointer* rather than the image — # which left gr.Examples thumbnails broken. Reference them via the HF "resolve" URL instead: that # endpoint always returns the real bytes (LFS resolved server-side). Same approach the Ultralytics # demo uses with remote example URLs. EXAMPLES_BASE = "https://huggingface.co/spaces/rfonod/geo-trax/resolve/main/examples" # --- Helpers --------------------------------------------------------------------------------- def _class_ids(selected_labels): """Map the checkbox labels back to class ids; fall back to the four primary classes if none picked.""" ids = [cid for cid, name in ALL_SUPPORTED.items() if name in (selected_labels or [])] return ids or list(SUPPORTED) def _count_rows(detected_ids, active_ids): """Build a [class, count] table for the active classes only (with a total row).""" counts = Counter(int(c) for c in detected_ids) rows = [[ALL_SUPPORTED.get(cid, str(cid)), counts.get(cid, 0)] for cid in active_ids] rows.append(["Total", sum(counts.values())]) return rows # --- Inference ------------------------------------------------------------------------------- def detect_image(image, conf, iou, imgsz, selected_labels, show_labels, show_conf): """Detect vehicles in a single image. Returns (annotated RGB image, count table).""" if image is None: return None, [["Total", 0]] active = _class_ids(selected_labels) result = model.predict( source=image, # PIL image (RGB); Ultralytics handles channel order correctly. imgsz=int(imgsz), conf=float(conf), iou=float(iou), classes=active, max_det=MAX_DET, verbose=False, )[0] annotated = result.plot( line_width=LINE_WIDTH, font_size=FONT_SIZE, labels=show_labels, conf=show_conf )[:, :, ::-1] # BGR → RGB for display. return annotated, _count_rows(result.boxes.cls.tolist(), active) def detect_video( video_path, conf, iou, imgsz, selected_labels, show_labels, show_conf, progress=gr.Progress() ): """Detect vehicles frame-by-frame on a short clip (first MAX_VIDEO_FRAMES frames). Returns (annotated mp4 path, count table of total detections across processed frames). """ if not video_path: return None, [["Total", 0]] class_ids = _class_ids(selected_labels) cap = cv2.VideoCapture(video_path) fps = cap.get(cv2.CAP_PROP_FPS) or 30.0 total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0) n_frames = min(total, MAX_VIDEO_FRAMES) if total > 0 else MAX_VIDEO_FRAMES out_path = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False).name writer = None agg = [] for _ in progress.tqdm(range(n_frames), desc="Processing frames"): ok, frame = cap.read() if not ok: break result = model.predict( source=frame, # BGR numpy from OpenCV, Ultralytics' expected order. imgsz=int(imgsz), conf=float(conf), iou=float(iou), classes=class_ids, max_det=MAX_DET, verbose=False, )[0] annotated = result.plot( line_width=LINE_WIDTH, font_size=FONT_SIZE, labels=show_labels, conf=show_conf ) # BGR if writer is None: h, w = annotated.shape[:2] writer = cv2.VideoWriter(out_path, cv2.VideoWriter_fourcc(*"mp4v"), fps, (w, h)) writer.write(annotated) agg.extend(result.boxes.cls.tolist()) cap.release() if writer is not None: writer.release() out_path = _to_browser_mp4(out_path) return out_path, _count_rows(agg, class_ids) def _to_browser_mp4(path): """Best-effort re-encode to H.264/yuv420p so the clip plays inline; fall back to the input.""" import shutil import subprocess if shutil.which("ffmpeg") is None: return path encoded = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False).name try: subprocess.run( ["ffmpeg", "-y", "-i", path, "-vcodec", "libx264", "-pix_fmt", "yuv420p", encoded], check=True, capture_output=True, ) return encoded except Exception: # noqa: BLE001 (keep the original mp4v file if ffmpeg is unavailable) return path # --- UI -------------------------------------------------------------------------------------- HEADER = """
Detect vehicles (car · bus · truck · motorcycle) in high-altitude bird's-eye-view drone imagery, powered by the geo-trax YOLOv8s model.
> **Optimized for high-altitude, top-down (bird's-eye-view) aerial and drone footage.** This is a > detection-only demo; the full track → stabilize → georeference pipeline lives in the > [`geo-trax`](https://github.com/rfonod/geo-trax) package. """ FOOTER = """ --- ### 🎬 Beyond detection: the full Geo-trax pipeline This Space runs only the **detector**. From raw drone video (plus orthophotos), the full **[Geo-trax](https://github.com/rfonod/geo-trax)** pipeline extracts **georeferenced vehicle trajectories**: real-world coordinates, lane and road-section assignment, speeds and accelerations, and estimated vehicle dimensions.
⭐ Star on GitHub · 📺 Watch the 4-min demo · 📦 pip install geo-trax · 📄 Read the paper