Spaces:
Running
Running
Zhen Ye
Claude Opus 4.6
commited on
Commit
·
a70bcf5
1
Parent(s):
0d3be57
Add diagnostic logging to pinpoint tracker dropping detections
Browse files4 targeted log points across inference.py and utils/tracker.py to
diagnose why first-frame preview shows bounding boxes but the
processed video has none — the tracker is the prime suspect.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- inference.py +13 -0
- models/detectors/drone_yolo.py +7 -2
- models/detectors/yolov8.py +7 -2
- utils/tracker.py +21 -0
inference.py
CHANGED
|
@@ -947,6 +947,7 @@ def run_inference(
|
|
| 947 |
depth_stats.update(dep_res.depth_map)
|
| 948 |
|
| 949 |
# --- POST PROCESSING ---
|
|
|
|
| 950 |
for i, (idx, frame, dep_res) in enumerate(zip(indices, frames, depth_results)):
|
| 951 |
# 1. Detections — use cached for frame 0 if available
|
| 952 |
detections = []
|
|
@@ -958,6 +959,7 @@ def run_inference(
|
|
| 958 |
detections = _build_detection_records(
|
| 959 |
d_res.boxes, d_res.scores, d_res.labels, queries, d_res.label_names
|
| 960 |
)
|
|
|
|
| 961 |
|
| 962 |
# 2. Frame Rendering
|
| 963 |
processed = frame.copy()
|
|
@@ -981,6 +983,13 @@ def run_inference(
|
|
| 981 |
raise RuntimeError("Writer thread died unexpectedly")
|
| 982 |
if job_id: _check_cancellation(job_id)
|
| 983 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 984 |
batch_accum.clear()
|
| 985 |
logging.info(f"Worker {gpu_idx} finished flushing batch")
|
| 986 |
|
|
@@ -1152,7 +1161,11 @@ def run_inference(
|
|
| 1152 |
|
| 1153 |
# --- SEQUENTIAL TRACKING ---
|
| 1154 |
# Run tracker FIRST so detections get real track_id from ByteTracker
|
|
|
|
| 1155 |
dets = tracker.update(dets)
|
|
|
|
|
|
|
|
|
|
| 1156 |
speed_est.estimate(dets)
|
| 1157 |
|
| 1158 |
# --- RELEVANCE GATE (deterministic, fast — stays in writer) ---
|
|
|
|
| 947 |
depth_stats.update(dep_res.depth_map)
|
| 948 |
|
| 949 |
# --- POST PROCESSING ---
|
| 950 |
+
batch_det_summary = []
|
| 951 |
for i, (idx, frame, dep_res) in enumerate(zip(indices, frames, depth_results)):
|
| 952 |
# 1. Detections — use cached for frame 0 if available
|
| 953 |
detections = []
|
|
|
|
| 959 |
detections = _build_detection_records(
|
| 960 |
d_res.boxes, d_res.scores, d_res.labels, queries, d_res.label_names
|
| 961 |
)
|
| 962 |
+
batch_det_summary.append((idx, len(detections)))
|
| 963 |
|
| 964 |
# 2. Frame Rendering
|
| 965 |
processed = frame.copy()
|
|
|
|
| 983 |
raise RuntimeError("Writer thread died unexpectedly")
|
| 984 |
if job_id: _check_cancellation(job_id)
|
| 985 |
|
| 986 |
+
total_dets = sum(c for _, c in batch_det_summary)
|
| 987 |
+
if total_dets == 0 or indices[0] % 90 == 0:
|
| 988 |
+
logging.info("Worker %d batch [frames %s]: %d total detections %s",
|
| 989 |
+
gpu_idx,
|
| 990 |
+
f"{indices[0]}-{indices[-1]}",
|
| 991 |
+
total_dets,
|
| 992 |
+
[(idx, cnt) for idx, cnt in batch_det_summary if cnt > 0])
|
| 993 |
batch_accum.clear()
|
| 994 |
logging.info(f"Worker {gpu_idx} finished flushing batch")
|
| 995 |
|
|
|
|
| 1161 |
|
| 1162 |
# --- SEQUENTIAL TRACKING ---
|
| 1163 |
# Run tracker FIRST so detections get real track_id from ByteTracker
|
| 1164 |
+
pre_track_count = len(dets)
|
| 1165 |
dets = tracker.update(dets)
|
| 1166 |
+
if (next_idx % 30 == 0) or (pre_track_count > 0 and len(dets) == 0):
|
| 1167 |
+
logging.info("Writer frame %d: %d detections in -> %d tracked out",
|
| 1168 |
+
next_idx, pre_track_count, len(dets))
|
| 1169 |
speed_est.estimate(dets)
|
| 1170 |
|
| 1171 |
# --- RELEVANCE GATE (deterministic, fast — stays in writer) ---
|
models/detectors/drone_yolo.py
CHANGED
|
@@ -20,10 +20,15 @@ class DroneYoloDetector(ObjectDetector):
|
|
| 20 |
def __init__(self, score_threshold: float = 0.3, device: str = None) -> None:
|
| 21 |
self.name = "drone_yolo"
|
| 22 |
self.score_threshold = score_threshold
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
if device:
|
| 24 |
-
self.device = device
|
| 25 |
else:
|
| 26 |
-
self.device = "cuda:0" if torch.cuda.is_available() else "cpu"
|
| 27 |
logging.info(
|
| 28 |
"Loading drone YOLO from HuggingFace Hub: %s onto %s",
|
| 29 |
self.REPO_ID,
|
|
|
|
| 20 |
def __init__(self, score_threshold: float = 0.3, device: str = None) -> None:
|
| 21 |
self.name = "drone_yolo"
|
| 22 |
self.score_threshold = score_threshold
|
| 23 |
+
# CRITICAL: Store device as torch.device, NOT a string.
|
| 24 |
+
# Ultralytics' select_device() sets CUDA_VISIBLE_DEVICES when it
|
| 25 |
+
# receives a string like "cuda:0", restricting the entire process to
|
| 26 |
+
# one GPU. Passing a torch.device object causes select_device() to
|
| 27 |
+
# return immediately without touching the environment.
|
| 28 |
if device:
|
| 29 |
+
self.device = torch.device(device)
|
| 30 |
else:
|
| 31 |
+
self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
|
| 32 |
logging.info(
|
| 33 |
"Loading drone YOLO from HuggingFace Hub: %s onto %s",
|
| 34 |
self.REPO_ID,
|
models/detectors/yolov8.py
CHANGED
|
@@ -21,10 +21,15 @@ class HuggingFaceYoloV8Detector(ObjectDetector):
|
|
| 21 |
def __init__(self, score_threshold: float = 0.3, device: str = None) -> None:
|
| 22 |
self.name = "hf_yolov8"
|
| 23 |
self.score_threshold = score_threshold
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
if device:
|
| 25 |
-
self.device = device
|
| 26 |
else:
|
| 27 |
-
self.device = "cuda:0" if torch.cuda.is_available() else "cpu"
|
| 28 |
logging.info(
|
| 29 |
"Loading Hugging Face YOLOv8 weights %s/%s onto %s",
|
| 30 |
self.REPO_ID,
|
|
|
|
| 21 |
def __init__(self, score_threshold: float = 0.3, device: str = None) -> None:
|
| 22 |
self.name = "hf_yolov8"
|
| 23 |
self.score_threshold = score_threshold
|
| 24 |
+
# CRITICAL: Store device as torch.device, NOT a string.
|
| 25 |
+
# Ultralytics' select_device() sets CUDA_VISIBLE_DEVICES when it
|
| 26 |
+
# receives a string like "cuda:0", restricting the entire process to
|
| 27 |
+
# one GPU. Passing a torch.device object causes select_device() to
|
| 28 |
+
# return immediately without touching the environment.
|
| 29 |
if device:
|
| 30 |
+
self.device = torch.device(device)
|
| 31 |
else:
|
| 32 |
+
self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
|
| 33 |
logging.info(
|
| 34 |
"Loading Hugging Face YOLOv8 weights %s/%s onto %s",
|
| 35 |
self.REPO_ID,
|
utils/tracker.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
import numpy as np
|
| 2 |
from scipy.optimize import linear_sum_assignment
|
| 3 |
import scipy.linalg
|
|
@@ -509,14 +510,23 @@ class ByteTracker:
|
|
| 509 |
# 4. Init new tracks from unmatched high score detections
|
| 510 |
# Note: Unmatched low score detections are ignored (noise)
|
| 511 |
unmatched_dets = [detections[i] for i in u_detection]
|
|
|
|
| 512 |
for track in unmatched_dets:
|
| 513 |
if track.score < self.new_track_thresh:
|
|
|
|
| 514 |
continue # Not confident enough to start a new track
|
| 515 |
|
| 516 |
track.activate(self.kalman_filter, self.frame_id)
|
| 517 |
activated_stracks.append(track)
|
| 518 |
self._sync_data(track, track) # Sync self
|
| 519 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 520 |
# 5. Update state
|
| 521 |
self.tracked_stracks = [t for t in self.tracked_stracks if t.state == 2]
|
| 522 |
self.tracked_stracks = join_stracks(self.tracked_stracks, activated_stracks)
|
|
@@ -580,6 +590,17 @@ class ByteTracker:
|
|
| 580 |
|
| 581 |
results.append(d_out)
|
| 582 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 583 |
return results
|
| 584 |
|
| 585 |
def _sync_data(self, track, det_source):
|
|
|
|
| 1 |
+
import logging
|
| 2 |
import numpy as np
|
| 3 |
from scipy.optimize import linear_sum_assignment
|
| 4 |
import scipy.linalg
|
|
|
|
| 510 |
# 4. Init new tracks from unmatched high score detections
|
| 511 |
# Note: Unmatched low score detections are ignored (noise)
|
| 512 |
unmatched_dets = [detections[i] for i in u_detection]
|
| 513 |
+
rejected_by_thresh = 0
|
| 514 |
for track in unmatched_dets:
|
| 515 |
if track.score < self.new_track_thresh:
|
| 516 |
+
rejected_by_thresh += 1
|
| 517 |
continue # Not confident enough to start a new track
|
| 518 |
|
| 519 |
track.activate(self.kalman_filter, self.frame_id)
|
| 520 |
activated_stracks.append(track)
|
| 521 |
self._sync_data(track, track) # Sync self
|
| 522 |
|
| 523 |
+
if rejected_by_thresh > 0 and self.frame_id <= 5:
|
| 524 |
+
logging.warning(
|
| 525 |
+
"Tracker frame %d: %d detections rejected by new_track_thresh=%.2f (scores: %s)",
|
| 526 |
+
self.frame_id, rejected_by_thresh, self.new_track_thresh,
|
| 527 |
+
[f"{t.score:.3f}" for t in unmatched_dets if t.score < self.new_track_thresh]
|
| 528 |
+
)
|
| 529 |
+
|
| 530 |
# 5. Update state
|
| 531 |
self.tracked_stracks = [t for t in self.tracked_stracks if t.state == 2]
|
| 532 |
self.tracked_stracks = join_stracks(self.tracked_stracks, activated_stracks)
|
|
|
|
| 590 |
|
| 591 |
results.append(d_out)
|
| 592 |
|
| 593 |
+
if self.frame_id % 30 == 0:
|
| 594 |
+
logging.info(
|
| 595 |
+
"Tracker frame %d: %d tracked (%d activated), %d lost, %d new this frame, output=%d",
|
| 596 |
+
self.frame_id,
|
| 597 |
+
len(self.tracked_stracks),
|
| 598 |
+
sum(1 for t in self.tracked_stracks if t.is_activated),
|
| 599 |
+
len(self.lost_stracks),
|
| 600 |
+
len(activated_stracks),
|
| 601 |
+
len(results),
|
| 602 |
+
)
|
| 603 |
+
|
| 604 |
return results
|
| 605 |
|
| 606 |
def _sync_data(self, track, det_source):
|