import ctypes import glob import os import sys import time from pathlib import Path from typing import Dict, List, Optional, Tuple import cv2 import numpy as np # Preload NVIDIA CUDA/cuDNN pip libraries if installed so CUDAExecutionProvider works out of the box on T4 GPUs def _preload_nvidia_cuda_libs(): site_dirs = [p for p in sys.path if "site-packages" in p] lib_dirs = [] for sd in site_dirs: for cand in sorted(glob.glob(os.path.join(sd, "nvidia", "*", "lib"))): if os.path.isdir(cand): lib_dirs.append(cand) if lib_dirs: cur_ld = os.environ.get("LD_LIBRARY_PATH", "") os.environ["LD_LIBRARY_PATH"] = ":".join(lib_dirs + ([cur_ld] if cur_ld else [])) for d in lib_dirs: for so_file in sorted(glob.glob(os.path.join(d, "*.so*"))): try: ctypes.CDLL(so_file, mode=ctypes.RTLD_GLOBAL) except Exception: pass _preload_nvidia_cuda_libs() try: import onnxruntime as ort HAVE_ONNX = True except ImportError: HAVE_ONNX = False MODEL_DIR = Path(__file__).resolve().parent / "models" YOLO_MODEL_PATH = MODEL_DIR / "yolov8n.onnx" POSE_MODEL_PATH = ( MODEL_DIR / "pose" / "onnx" / "model.onnx" if (MODEL_DIR / "pose" / "onnx" / "model.onnx").exists() else MODEL_DIR / "pose" / "onnx" / "model_quantized.onnx" ) # 80 COCO Classes COCO_CLASSES = [ "person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck", "boat", "traffic light", "fire hydrant", "stop sign", "parking meter", "bench", "bird", "cat", "dog", "horse", "sheep", "cow", "elephant", "bear", "zebra", "giraffe", "backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee", "skis", "snowboard", "sports ball", "kite", "baseball bat", "baseball glove", "skateboard", "surfboard", "tennis racket", "bottle", "wine glass", "cup", "fork", "knife", "spoon", "bowl", "banana", "apple", "sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza", "donut", "cake", "chair", "couch", "potted plant", "bed", "dining table", "toilet", "tv", "laptop", "mouse", "remote", "keyboard", "cell phone", "microwave", "oven", "toaster", "sink", "refrigerator", "book", "clock", "vase", "scissors", "teddy bear", "hair drier", "toothbrush" ] # 17 COCO Keypoints KPT_NAMES = [ "nose", "left_eye", "right_eye", "left_ear", "right_ear", "left_shoulder", "right_shoulder", "left_elbow", "right_elbow", "left_wrist", "right_wrist", "left_hip", "right_hip", "left_knee", "right_knee", "left_ankle", "right_ankle" ] SKELETON_PAIRS = [ (0, 1), (0, 2), (1, 3), (2, 4), # Head / Face (5, 6), (5, 7), (7, 9), (6, 8), (8, 10), # Arms (5, 11), (6, 12), (11, 12), # Torso (11, 13), (13, 15), (12, 14), (14, 16) # Legs ] # Styling Palette COLOR_PALETTE = { "person": (50, 225, 120), # Green "head / face": (0, 235, 255), # Yellow/Gold "left hand": (255, 200, 0), # Cyan "right hand": (255, 200, 0), "left arm": (255, 175, 50), # Sky Blue "right arm": (255, 175, 50), "torso / chest": (80, 220, 120),# Emerald "chest / torso": (80, 220, 120),# Emerald "hips / pelvic": (180, 105, 255),# Coral Pink "pelvic / hips": (180, 105, 255),# Coral Pink "left leg": (255, 120, 200), # Magenta "right leg": (255, 120, 200), "left foot": (0, 165, 255), # Orange "right foot": (0, 165, 255), "cell phone": (255, 180, 0), "laptop": (255, 140, 0), "bottle": (180, 180, 180), "cup": (200, 150, 255), "chair": (255, 100, 180), "cat": (0, 180, 255), "dog": (0, 165, 255) } DEFAULT_COLOR = (0, 215, 255) def compute_box_iou(boxA: List[int], boxB: List[int]) -> float: xA = max(boxA[0], boxB[0]) yA = max(boxA[1], boxB[1]) xB = min(boxA[2], boxB[2]) yB = min(boxA[3], boxB[3]) inter = max(0, xB - xA) * max(0, yB - yA) areaA = max(0, boxA[2] - boxA[0]) * max(0, boxA[3] - boxA[1]) areaB = max(0, boxB[2] - boxB[0]) * max(0, boxB[3] - boxB[1]) union = float(areaA + areaB - inter) return inter / union if union > 0 else 0.0 class TrackedBox: def __init__(self, track_id: str, base_label: str, label: str, confidence: float, box: List[int]): self.track_id = track_id self.base_label = base_label self.label = label self.confidence = confidence self.box = list(box) self.missed_frames = 0 self.hit_count = 1 def update(self, label: str, confidence: float, new_box: List[int]): alpha = 0.60 self.label = label self.box[0] = int(self.box[0] * (1 - alpha) + new_box[0] * alpha) self.box[1] = int(self.box[1] * (1 - alpha) + new_box[1] * alpha) self.box[2] = int(self.box[2] * (1 - alpha) + new_box[2] * alpha) self.box[3] = int(self.box[3] * (1 - alpha) + new_box[3] * alpha) self.confidence = round(self.confidence * (1 - alpha) + confidence * alpha, 1) self.missed_frames = 0 self.hit_count += 1 class ObjectDetector: def __init__(self): self.yolo_session: Optional[any] = None self.pose_session: Optional[any] = None self.providers: List[str] = ["CPUExecutionProvider"] self.active_provider: str = "CPUExecutionProvider" self.gpu_active: bool = False self.loaded_yolo = False self.loaded_pose = False self.confidence_threshold = 0.45 self.nms_threshold = 0.45 # Tracking & Hysteresis self.tracked_objects: List[TrackedBox] = [] self.max_missed_frames = 3 self.next_track_num = 1 # Cache self.latest_detections: List[dict] = [] self.latest_body_parts: List[dict] = [] self.latest_keypoints: List[dict] = [] self.latest_telemetry: dict = { "gpu_active": False, "provider": "CPUExecutionProvider", "keypoints_count": 0, "objects_count": 0, "inference_ms": 0.0 } self.last_detection_time: float = 0.0 self._load_models() def _load_models(self): if not HAVE_ONNX: return opts = ort.SessionOptions() opts.intra_op_num_threads = min(4, os.cpu_count() or 2) opts.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL opts.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL available_providers = ort.get_available_providers() if "CUDAExecutionProvider" in available_providers: self.providers = ["CUDAExecutionProvider", "CPUExecutionProvider"] else: self.providers = ["CPUExecutionProvider"] # 1. YOLOv8n Object Detection if YOLO_MODEL_PATH.exists(): try: self.yolo_session = ort.InferenceSession(str(YOLO_MODEL_PATH), opts, providers=self.providers) self.loaded_yolo = True sess_provs = self.yolo_session.get_providers() if sess_provs: self.active_provider = sess_provs[0] self.gpu_active = ("CUDA" in self.active_provider) except Exception: try: self.yolo_session = ort.InferenceSession(str(YOLO_MODEL_PATH), opts, providers=["CPUExecutionProvider"]) self.loaded_yolo = True except Exception: pass # 2. YOLOv8n-Pose (Body Parts & Keypoints) if POSE_MODEL_PATH.exists(): try: self.pose_session = ort.InferenceSession(str(POSE_MODEL_PATH), opts, providers=self.providers) self.loaded_pose = True sess_provs = self.pose_session.get_providers() if sess_provs and "CUDA" in sess_provs[0]: self.active_provider = sess_provs[0] self.gpu_active = True except Exception: try: self.pose_session = ort.InferenceSession(str(POSE_MODEL_PATH), opts, providers=["CPUExecutionProvider"]) self.loaded_pose = True except Exception: pass self.latest_telemetry["gpu_active"] = self.gpu_active self.latest_telemetry["provider"] = self.active_provider def detect(self, frame: np.ndarray) -> Tuple[List[dict], dict]: """Detects 80 COCO objects, specific body parts, and 17 pose keypoints.""" t0 = time.perf_counter() if frame is None or not HAVE_ONNX: return [], dict(self.latest_telemetry) h, w = frame.shape[:2] raw_items: List[dict] = [] skeletons: List[dict] = [] img_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) resized_640 = cv2.resize(img_rgb, (640, 640), interpolation=cv2.INTER_LINEAR) blob_640 = resized_640.transpose(2, 0, 1)[None].astype(np.float32) / 255.0 x_factor = w / 640.0 y_factor = h / 640.0 # --- 1. YOLOv8 80-Class General Object Detection --- if self.loaded_yolo and self.yolo_session: try: outputs = self.yolo_session.run(['output0'], {'images': blob_640})[0][0].T boxes = outputs[:, :4] scores = outputs[:, 4:] class_ids = np.argmax(scores, axis=1) confidences = np.max(scores, axis=1) valid_mask = confidences >= self.confidence_threshold boxes = boxes[valid_mask] confidences = confidences[valid_mask] class_ids = class_ids[valid_mask] cv_boxes = [] for box in boxes: cx, cy, bw, bh = box cv_boxes.append([ int((cx - bw / 2.0) * x_factor), int((cy - bh / 2.0) * y_factor), int(bw * x_factor), int(bh * y_factor) ]) indices = cv2.dnn.NMSBoxes(cv_boxes, confidences.tolist(), self.confidence_threshold, self.nms_threshold) if len(indices) > 0: for idx in indices: i = idx if isinstance(idx, (int, np.integer)) else idx[0] cid = int(class_ids[i]) if 0 <= cid < len(COCO_CLASSES): label = COCO_CLASSES[cid] bx = cv_boxes[i] raw_items.append({ "base_label": label, "label": label, "confidence": round(float(confidences[i]) * 100, 1), "box": [max(0, bx[0]), max(0, bx[1]), min(w - 1, bx[0] + bx[2]), min(h - 1, bx[1] + bx[3])], "is_body_part": False }) except Exception: pass # --- 2. YOLOv8-Pose Body Parts & Keypoints Extraction --- if self.loaded_pose and self.pose_session: try: pose_out = self.pose_session.run(['output0'], {'images': blob_640})[0][0].T p_boxes = pose_out[:, :4] p_scores = pose_out[:, 4] p_kpts = pose_out[:, 5:].reshape(-1, 17, 3) p_mask = p_scores >= 0.35 p_boxes = p_boxes[p_mask] p_scores = p_scores[p_mask] p_kpts = p_kpts[p_mask] p_cv_boxes = [] for b in p_boxes: cx, cy, bw, bh = b p_cv_boxes.append([ int((cx - bw / 2.0) * x_factor), int((cy - bh / 2.0) * y_factor), int(bw * x_factor), int(bh * y_factor) ]) p_indices = cv2.dnn.NMSBoxes(p_cv_boxes, p_scores.tolist(), 0.35, 0.45) for idx in p_indices: i = idx if isinstance(idx, (int, np.integer)) else idx[0] kpts = p_kpts[i] kpts_scaled = [] for kx, ky, kc in kpts: kpts_scaled.append([int(kx * x_factor), int(ky * y_factor), float(kc)]) skeletons.append(kpts_scaled) # A. Head & Face (Nose:0, Eyes:1,2, Ears:3,4) head_pts = [kpts_scaled[j] for j in range(5) if kpts_scaled[j][2] > 0.30] if len(head_pts) >= 2: xs = [pt[0] for pt in head_pts] ys = [pt[1] for pt in head_pts] pad_x = max(25, int((max(xs) - min(xs)) * 0.7)) pad_y = max(30, int((max(ys) - min(ys)) * 1.0)) face_box = [ max(0, min(xs) - pad_x), max(0, min(ys) - pad_y), min(w - 1, max(xs) + pad_x), min(h - 1, max(ys) + pad_y // 2) ] conf = round(float(np.mean([pt[2] for pt in head_pts])) * 100, 1) raw_items.append({"base_label": "head / face", "label": "head / face", "confidence": conf, "box": face_box, "is_body_part": True}) # B. Left Hand / Wrist (Point 9) if kpts_scaled[9][2] > 0.35: x, y = kpts_scaled[9][:2] pad = 32 raw_items.append({ "base_label": "left hand", "label": "left hand", "confidence": round(kpts_scaled[9][2] * 100, 1), "box": [max(0, x - pad), max(0, y - pad), min(w - 1, x + pad), min(h - 1, y + pad)], "is_body_part": True }) # C. Right Hand / Wrist (Point 10) if kpts_scaled[10][2] > 0.35: x, y = kpts_scaled[10][:2] pad = 32 raw_items.append({ "base_label": "right hand", "label": "right hand", "confidence": round(kpts_scaled[10][2] * 100, 1), "box": [max(0, x - pad), max(0, y - pad), min(w - 1, x + pad), min(h - 1, y + pad)], "is_body_part": True }) # D. Chest / Upper Torso (Shoulders:5,6, Hips:11,12) torso_pts = [kpts_scaled[j] for j in [5, 6, 11, 12] if kpts_scaled[j][2] > 0.20] if len(torso_pts) >= 2: xs = [pt[0] for pt in torso_pts] ys = [pt[1] for pt in torso_pts] span_x = max(120, max(xs) - min(xs)) tx1 = max(0, min(xs) - int(span_x * 0.18)) tx2 = min(w - 1, max(xs) + int(span_x * 0.18)) ty1 = max(0, min(ys) - 20) ty2 = min(h - 1, max(max(ys) + 25, min(ys) + int(span_x * 0.85))) if ty2 - ty1 < 90: ty1 = max(0, ty2 - 110) raw_items.append({ "base_label": "chest / torso", "label": "chest / torso", "confidence": round(float(np.mean([pt[2] for pt in torso_pts])) * 100, 1), "box": [tx1, ty1, tx2, ty2], "is_body_part": True }) # E. Hips / Pelvic Region (Hips:11,12) hip_pts = [kpts_scaled[j] for j in [11, 12] if kpts_scaled[j][2] > 0.18] if len(hip_pts) >= 1: xs = [pt[0] for pt in hip_pts] ys = [pt[1] for pt in hip_pts] hx1, hy1 = max(0, min(xs) - 55), max(0, min(ys) - 35) hx2, hy2 = min(w - 1, max(xs) + 55), min(h - 1, max(ys) + 65) if hy2 - hy1 < 75: hy1 = max(0, hy2 - 85) raw_items.append({ "base_label": "pelvic / hips", "label": "pelvic / hips", "confidence": round(float(np.mean([pt[2] for pt in hip_pts])) * 100, 1), "box": [hx1, hy1, hx2, hy2], "is_body_part": True }) # F. Left & Right Arms (Shoulders/Elbows/Wrists) for arm_name, idxs in [("left arm", [5, 7, 9]), ("right arm", [6, 8, 10])]: apts = [kpts_scaled[j] for j in idxs if kpts_scaled[j][2] > 0.30] if len(apts) >= 2: xs, ys = [p[0] for p in apts], [p[1] for p in apts] raw_items.append({ "base_label": arm_name, "label": arm_name, "confidence": round(float(np.mean([p[2] for p in apts])) * 100, 1), "box": [max(0, min(xs) - 18), max(0, min(ys) - 18), min(w - 1, max(xs) + 18), min(h - 1, max(ys) + 18)], "is_body_part": True }) # G. Left & Right Legs (Hips/Knees/Ankles) for leg_name, idxs in [("left leg", [11, 13, 15]), ("right leg", [12, 14, 16])]: lpts = [kpts_scaled[j] for j in idxs if kpts_scaled[j][2] > 0.25] if len(lpts) >= 2: xs, ys = [p[0] for p in lpts], [p[1] for p in lpts] raw_items.append({ "base_label": leg_name, "label": leg_name, "confidence": round(float(np.mean([p[2] for p in lpts])) * 100, 1), "box": [max(0, min(xs) - 22), max(0, min(ys) - 18), min(w - 1, max(xs) + 22), min(h - 1, max(ys) + 18)], "is_body_part": True }) # H. Left & Right Feet / Ankles (Points 15, 16) for foot_name, pt_idx in [("left foot", 15), ("right foot", 16)]: if kpts_scaled[pt_idx][2] > 0.22: fx, fy = kpts_scaled[pt_idx][:2] fx1, fy1 = max(0, fx - 36), max(0, fy - 28) fx2, fy2 = min(w - 1, fx + 36), min(h - 1, fy + 38) raw_items.append({ "base_label": foot_name, "label": foot_name, "confidence": round(kpts_scaled[pt_idx][2] * 100, 1), "box": [fx1, fy1, fx2, fy2], "is_body_part": True }) except Exception: pass self._update_tracks(raw_items) self.latest_keypoints = skeletons formatted = [ { "id": t.track_id, "base_label": t.base_label, "label": t.label, "confidence": t.confidence, "box": list(t.box), "center": [ round((t.box[0] + t.box[2]) / (2.0 * w), 3), round((t.box[1] + t.box[3]) / (2.0 * h), 3) ] } for t in self.tracked_objects if t.missed_frames == 0 or t.hit_count >= 2 ] elapsed_ms = round((time.perf_counter() - t0) * 1000.0, 2) telemetry = { "gpu_active": self.gpu_active, "provider": self.active_provider, "keypoints_count": len(skeletons) * 17, "objects_count": len(formatted), "inference_ms": elapsed_ms } self.latest_detections = formatted self.latest_telemetry = telemetry self.last_detection_time = time.time() return formatted, telemetry def _update_tracks(self, raw_items: List[dict]): matched_track_idx = set() matched_raw_idx = set() for r_idx, r in enumerate(raw_items): r_base = r.get("base_label", r["label"]) best_iou = 0.30 best_t_idx = -1 for t_idx, track in enumerate(self.tracked_objects): if t_idx in matched_track_idx or track.base_label != r_base: continue iou = compute_box_iou(track.box, r["box"]) if iou > best_iou: best_iou = iou best_t_idx = t_idx if best_t_idx >= 0: self.tracked_objects[best_t_idx].update( r["label"], r["confidence"], r["box"] ) matched_track_idx.add(best_t_idx) matched_raw_idx.add(r_idx) for r_idx, r in enumerate(raw_items): if r_idx not in matched_raw_idx: track_id = f"obj_{self.next_track_num}" self.next_track_num += 1 self.tracked_objects.append( TrackedBox( track_id, r.get("base_label", r["label"]), r["label"], r["confidence"], r["box"] ) ) self.tracked_objects = [ t for i, t in enumerate(self.tracked_objects) if (i in matched_track_idx) or (t.missed_frames + 1 <= self.max_missed_frames) ] for i, t in enumerate(self.tracked_objects): if i not in matched_track_idx: t.missed_frames += 1 def render_overlay( self, frame: np.ndarray, detections: Optional[List[dict]] = None, telemetry: Optional[dict] = None, crop_meta: Optional[Tuple[int, int, int, int]] = None, active_target_id: Optional[str] = None ) -> np.ndarray: """Renders HUD overlays, skeletal wireframe, and targeted object highlight directly on frame.""" if frame is None: return frame out = frame.copy() h, w = out.shape[:2] dets = detections if detections is not None else self.latest_detections telem = telemetry if telemetry is not None else self.latest_telemetry # Crop transformation factors if rendered onto a zoomed crop if crop_meta is not None: cx1, cy1, cw, ch = crop_meta sx = w / float(cw) sy = h / float(ch) else: cx1, cy1, cw, ch = 0, 0, w, h sx, sy = 1.0, 1.0 # Draw Skeletal Lines & Keypoints for kpts in self.latest_keypoints: # Draw Bones for p1, p2 in SKELETON_PAIRS: x1, y1, c1 = kpts[p1] x2, y2, c2 = kpts[p2] if c1 > 0.35 and c2 > 0.35: mx1 = int((x1 - cx1) * sx) my1 = int((y1 - cy1) * sy) mx2 = int((x2 - cx1) * sx) my2 = int((y2 - cy1) * sy) bone_color = (0, 240, 255) if p1 < 5 else ((255, 200, 0) if p1 < 11 else (50, 255, 100)) cv2.line(out, (mx1, my1), (mx2, my2), bone_color, 2, cv2.LINE_AA) # Draw Joints for kx, ky, kc in kpts: if kc > 0.35: mx = int((kx - cx1) * sx) my = int((ky - cy1) * sy) if 0 <= mx < w and 0 <= my < h: cv2.circle(out, (mx, my), 4, (0, 255, 255), -1, cv2.LINE_AA) cv2.circle(out, (mx, my), 2, (20, 20, 20), -1, cv2.LINE_AA) # Draw Stabilized Object Bounding Boxes & Target Focus HUD for d in dets: label = d["label"] conf = d["confidence"] orig_box = d["box"] is_active_target = (active_target_id and d.get("id") == active_target_id) x1 = int((orig_box[0] - cx1) * sx) y1 = int((orig_box[1] - cy1) * sy) x2 = int((orig_box[2] - cx1) * sx) y2 = int((orig_box[3] - cy1) * sy) if x2 < 0 or x1 >= w or y2 < 0 or y1 >= h: continue x1 = max(0, min(w - 1, x1)) y1 = max(0, min(h - 1, y1)) x2 = max(0, min(w - 1, x2)) y2 = max(0, min(h - 1, y2)) base_lbl = d.get("base_label", label) if is_active_target: color = (0, 100, 255) else: color = COLOR_PALETTE.get(base_lbl, DEFAULT_COLOR) thick = 3 if is_active_target else 2 # Main Box & Corner Highlights cv2.rectangle(out, (x1, y1), (x2, y2), color, 1, lineType=cv2.LINE_AA) c_len = min(22, max(8, int((x2 - x1) * 0.2))) cv2.line(out, (x1, y1), (x1 + c_len, y1), color, thick) cv2.line(out, (x1, y1), (x1, y1 + c_len), color, thick) cv2.line(out, (x2, y1), (x2 - c_len, y1), color, thick) cv2.line(out, (x2, y1), (x2, y1 + c_len), color, thick) cv2.line(out, (x1, y2), (x1 + c_len, y2), color, thick) cv2.line(out, (x1, y2), (x1, y2 - c_len), color, thick) cv2.line(out, (x2, y2), (x2 - c_len, y2), color, thick) cv2.line(out, (x2, y2), (x2, y2 - c_len), color, thick) # Targeted Object Reticle if is_active_target: mcx = (x1 + x2) // 2 mcy = (y1 + y2) // 2 cv2.drawMarker(out, (mcx, mcy), (0, 100, 255), cv2.MARKER_CROSS, 20, 2) cv2.putText(out, "[LOCKED TARGET]", (x1, max(15, y1 - 22)), cv2.FONT_HERSHEY_SIMPLEX, 0.42, (0, 100, 255), 1, cv2.LINE_AA) # Label Pill text = f"{label.upper()} {int(conf)}%" (tw, th), _ = cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX, 0.42, 1) ty = max(y1, th + 8) cv2.rectangle(out, (x1, ty - th - 5), (x1 + tw + 8, ty + 4), color, -1) cv2.putText(out, text, (x1 + 4, ty - 2), cv2.FONT_HERSHEY_SIMPLEX, 0.42, (10, 10, 10), 1, cv2.LINE_AA) # AI Tracker Telemetry HUD Pill (Top Right) is_gpu = telem.get("gpu_active", self.gpu_active) accel_tag = "T4 CUDA GPU" if is_gpu else "ONNX ENGINE" badge_text = f"AI TRACKER: {accel_tag} ({len(dets)} TARGETS)" badge_bg = (35, 120, 45) text_color = (220, 255, 220) (bw, bh), _ = cv2.getTextSize(badge_text, cv2.FONT_HERSHEY_SIMPLEX, 0.48, 1) badge_x = w - bw - 20 badge_y = 28 cv2.rectangle(out, (badge_x - 10, badge_y - bh - 6), (badge_x + bw + 10, badge_y + 6), (15, 15, 15), -1) cv2.rectangle(out, (badge_x - 8, badge_y - bh - 4), (badge_x + bw + 8, badge_y + 4), badge_bg, -1) cv2.putText(out, badge_text, (badge_x, badge_y), cv2.FONT_HERSHEY_SIMPLEX, 0.48, text_color, 1, cv2.LINE_AA) return out def detect_and_draw(self, frame: np.ndarray, draw_boxes: bool = True) -> Tuple[np.ndarray, List[dict]]: dets, telem = self.detect(frame) if draw_boxes: out = self.render_overlay(frame, dets, telem) return out, dets return frame, dets