feat: custom bot-sort based tracker
This commit is contained in:
parent
bd201acac1
commit
791f611f7d
8 changed files with 649 additions and 282 deletions
408
core/tracking/bot_sort_tracker.py
Normal file
408
core/tracking/bot_sort_tracker.py
Normal file
|
@ -0,0 +1,408 @@
|
|||
"""
|
||||
BoT-SORT Multi-Object Tracker with Camera Isolation
|
||||
Based on BoT-SORT: Robust Associations Multi-Pedestrian Tracking
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
import numpy as np
|
||||
from typing import Dict, List, Optional, Tuple, Any
|
||||
from dataclasses import dataclass
|
||||
from scipy.optimize import linear_sum_assignment
|
||||
from filterpy.kalman import KalmanFilter
|
||||
import cv2
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TrackState:
|
||||
"""Track state enumeration"""
|
||||
TENTATIVE = "tentative" # New track, not confirmed yet
|
||||
CONFIRMED = "confirmed" # Confirmed track
|
||||
DELETED = "deleted" # Track to be deleted
|
||||
|
||||
|
||||
class Track:
|
||||
"""
|
||||
Individual track representation with Kalman filter for motion prediction
|
||||
"""
|
||||
|
||||
def __init__(self, detection, track_id: int, camera_id: str):
|
||||
"""
|
||||
Initialize a new track
|
||||
|
||||
Args:
|
||||
detection: Initial detection (bbox, confidence, class)
|
||||
track_id: Unique track identifier within camera
|
||||
camera_id: Camera identifier
|
||||
"""
|
||||
self.track_id = track_id
|
||||
self.camera_id = camera_id
|
||||
self.state = TrackState.TENTATIVE
|
||||
|
||||
# Time tracking
|
||||
self.start_time = time.time()
|
||||
self.last_update_time = time.time()
|
||||
|
||||
# Appearance and motion
|
||||
self.bbox = detection.bbox # [x1, y1, x2, y2]
|
||||
self.confidence = detection.confidence
|
||||
self.class_name = detection.class_name
|
||||
|
||||
# Track management
|
||||
self.hit_streak = 1
|
||||
self.time_since_update = 0
|
||||
self.age = 1
|
||||
|
||||
# Kalman filter for motion prediction
|
||||
self.kf = self._create_kalman_filter()
|
||||
self._update_kalman_filter(detection.bbox)
|
||||
|
||||
# Track history
|
||||
self.history = [detection.bbox]
|
||||
self.max_history = 10
|
||||
|
||||
def _create_kalman_filter(self) -> KalmanFilter:
|
||||
"""Create Kalman filter for bbox tracking (x, y, w, h, vx, vy, vw, vh)"""
|
||||
kf = KalmanFilter(dim_x=8, dim_z=4)
|
||||
|
||||
# State transition matrix (constant velocity model)
|
||||
kf.F = np.array([
|
||||
[1, 0, 0, 0, 1, 0, 0, 0],
|
||||
[0, 1, 0, 0, 0, 1, 0, 0],
|
||||
[0, 0, 1, 0, 0, 0, 1, 0],
|
||||
[0, 0, 0, 1, 0, 0, 0, 1],
|
||||
[0, 0, 0, 0, 1, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 1, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 1, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 1]
|
||||
])
|
||||
|
||||
# Measurement matrix (observe x, y, w, h)
|
||||
kf.H = np.array([
|
||||
[1, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 1, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 1, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 1, 0, 0, 0, 0]
|
||||
])
|
||||
|
||||
# Process noise
|
||||
kf.Q *= 0.01
|
||||
|
||||
# Measurement noise
|
||||
kf.R *= 10
|
||||
|
||||
# Initial covariance
|
||||
kf.P *= 100
|
||||
|
||||
return kf
|
||||
|
||||
def _update_kalman_filter(self, bbox: List[float]):
|
||||
"""Update Kalman filter with new bbox"""
|
||||
# Convert [x1, y1, x2, y2] to [cx, cy, w, h]
|
||||
x1, y1, x2, y2 = bbox
|
||||
cx = (x1 + x2) / 2
|
||||
cy = (y1 + y2) / 2
|
||||
w = x2 - x1
|
||||
h = y2 - y1
|
||||
|
||||
# Properly assign to column vector
|
||||
self.kf.x[:4, 0] = [cx, cy, w, h]
|
||||
|
||||
def predict(self) -> np.ndarray:
|
||||
"""Predict next position using Kalman filter"""
|
||||
self.kf.predict()
|
||||
|
||||
# Convert back to [x1, y1, x2, y2] format
|
||||
cx, cy, w, h = self.kf.x[:4, 0] # Extract from column vector
|
||||
x1 = cx - w/2
|
||||
y1 = cy - h/2
|
||||
x2 = cx + w/2
|
||||
y2 = cy + h/2
|
||||
|
||||
return np.array([x1, y1, x2, y2])
|
||||
|
||||
def update(self, detection):
|
||||
"""Update track with new detection"""
|
||||
self.last_update_time = time.time()
|
||||
self.time_since_update = 0
|
||||
self.hit_streak += 1
|
||||
self.age += 1
|
||||
|
||||
# Update track properties
|
||||
self.bbox = detection.bbox
|
||||
self.confidence = detection.confidence
|
||||
|
||||
# Update Kalman filter
|
||||
x1, y1, x2, y2 = detection.bbox
|
||||
cx = (x1 + x2) / 2
|
||||
cy = (y1 + y2) / 2
|
||||
w = x2 - x1
|
||||
h = y2 - y1
|
||||
|
||||
self.kf.update([cx, cy, w, h])
|
||||
|
||||
# Update history
|
||||
self.history.append(detection.bbox)
|
||||
if len(self.history) > self.max_history:
|
||||
self.history.pop(0)
|
||||
|
||||
# Update state
|
||||
if self.state == TrackState.TENTATIVE and self.hit_streak >= 3:
|
||||
self.state = TrackState.CONFIRMED
|
||||
|
||||
def mark_missed(self):
|
||||
"""Mark track as missed in this frame"""
|
||||
self.time_since_update += 1
|
||||
self.age += 1
|
||||
|
||||
if self.time_since_update > 5: # Delete after 5 missed frames
|
||||
self.state = TrackState.DELETED
|
||||
|
||||
def is_confirmed(self) -> bool:
|
||||
"""Check if track is confirmed"""
|
||||
return self.state == TrackState.CONFIRMED
|
||||
|
||||
def is_deleted(self) -> bool:
|
||||
"""Check if track should be deleted"""
|
||||
return self.state == TrackState.DELETED
|
||||
|
||||
|
||||
class CameraTracker:
|
||||
"""
|
||||
BoT-SORT tracker for a single camera
|
||||
"""
|
||||
|
||||
def __init__(self, camera_id: str, max_disappeared: int = 10):
|
||||
"""
|
||||
Initialize camera tracker
|
||||
|
||||
Args:
|
||||
camera_id: Unique camera identifier
|
||||
max_disappeared: Maximum frames a track can be missed before deletion
|
||||
"""
|
||||
self.camera_id = camera_id
|
||||
self.max_disappeared = max_disappeared
|
||||
|
||||
# Track management
|
||||
self.tracks: Dict[int, Track] = {}
|
||||
self.next_id = 1
|
||||
self.frame_count = 0
|
||||
|
||||
logger.info(f"Initialized BoT-SORT tracker for camera {camera_id}")
|
||||
|
||||
def update(self, detections: List) -> List[Track]:
|
||||
"""
|
||||
Update tracker with new detections
|
||||
|
||||
Args:
|
||||
detections: List of Detection objects
|
||||
|
||||
Returns:
|
||||
List of active confirmed tracks
|
||||
"""
|
||||
self.frame_count += 1
|
||||
|
||||
# Predict all existing tracks
|
||||
for track in self.tracks.values():
|
||||
track.predict()
|
||||
|
||||
# Associate detections to tracks
|
||||
matched_tracks, unmatched_detections, unmatched_tracks = self._associate(detections)
|
||||
|
||||
# Update matched tracks
|
||||
for track_id, detection in matched_tracks:
|
||||
self.tracks[track_id].update(detection)
|
||||
|
||||
# Mark unmatched tracks as missed
|
||||
for track_id in unmatched_tracks:
|
||||
self.tracks[track_id].mark_missed()
|
||||
|
||||
# Create new tracks for unmatched detections
|
||||
for detection in unmatched_detections:
|
||||
track = Track(detection, self.next_id, self.camera_id)
|
||||
self.tracks[self.next_id] = track
|
||||
self.next_id += 1
|
||||
|
||||
# Remove deleted tracks
|
||||
tracks_to_remove = [tid for tid, track in self.tracks.items() if track.is_deleted()]
|
||||
for tid in tracks_to_remove:
|
||||
del self.tracks[tid]
|
||||
|
||||
# Return confirmed tracks
|
||||
confirmed_tracks = [track for track in self.tracks.values() if track.is_confirmed()]
|
||||
|
||||
return confirmed_tracks
|
||||
|
||||
def _associate(self, detections: List) -> Tuple[List[Tuple[int, Any]], List[Any], List[int]]:
|
||||
"""
|
||||
Associate detections to existing tracks using IoU distance
|
||||
|
||||
Returns:
|
||||
(matched_tracks, unmatched_detections, unmatched_tracks)
|
||||
"""
|
||||
if not detections or not self.tracks:
|
||||
return [], detections, list(self.tracks.keys())
|
||||
|
||||
# Calculate IoU distance matrix
|
||||
track_ids = list(self.tracks.keys())
|
||||
cost_matrix = np.zeros((len(track_ids), len(detections)))
|
||||
|
||||
for i, track_id in enumerate(track_ids):
|
||||
track = self.tracks[track_id]
|
||||
predicted_bbox = track.predict()
|
||||
|
||||
for j, detection in enumerate(detections):
|
||||
iou = self._calculate_iou(predicted_bbox, detection.bbox)
|
||||
cost_matrix[i, j] = 1 - iou # Convert IoU to distance
|
||||
|
||||
# Solve assignment problem
|
||||
row_indices, col_indices = linear_sum_assignment(cost_matrix)
|
||||
|
||||
# Filter matches by IoU threshold
|
||||
iou_threshold = 0.3
|
||||
matched_tracks = []
|
||||
matched_detection_indices = set()
|
||||
matched_track_indices = set()
|
||||
|
||||
for row, col in zip(row_indices, col_indices):
|
||||
if cost_matrix[row, col] <= (1 - iou_threshold):
|
||||
track_id = track_ids[row]
|
||||
detection = detections[col]
|
||||
matched_tracks.append((track_id, detection))
|
||||
matched_detection_indices.add(col)
|
||||
matched_track_indices.add(row)
|
||||
|
||||
# Find unmatched detections and tracks
|
||||
unmatched_detections = [detections[i] for i in range(len(detections))
|
||||
if i not in matched_detection_indices]
|
||||
unmatched_tracks = [track_ids[i] for i in range(len(track_ids))
|
||||
if i not in matched_track_indices]
|
||||
|
||||
return matched_tracks, unmatched_detections, unmatched_tracks
|
||||
|
||||
def _calculate_iou(self, bbox1: np.ndarray, bbox2: List[float]) -> float:
|
||||
"""Calculate IoU between two bounding boxes"""
|
||||
x1_1, y1_1, x2_1, y2_1 = bbox1
|
||||
x1_2, y1_2, x2_2, y2_2 = bbox2
|
||||
|
||||
# Calculate intersection area
|
||||
x1_i = max(x1_1, x1_2)
|
||||
y1_i = max(y1_1, y1_2)
|
||||
x2_i = min(x2_1, x2_2)
|
||||
y2_i = min(y2_1, y2_2)
|
||||
|
||||
if x2_i <= x1_i or y2_i <= y1_i:
|
||||
return 0.0
|
||||
|
||||
intersection = (x2_i - x1_i) * (y2_i - y1_i)
|
||||
|
||||
# Calculate union area
|
||||
area1 = (x2_1 - x1_1) * (y2_1 - y1_1)
|
||||
area2 = (x2_2 - x1_2) * (y2_2 - y1_2)
|
||||
union = area1 + area2 - intersection
|
||||
|
||||
return intersection / union if union > 0 else 0.0
|
||||
|
||||
|
||||
class MultiCameraBoTSORT:
|
||||
"""
|
||||
Multi-camera BoT-SORT tracker with complete camera isolation
|
||||
"""
|
||||
|
||||
def __init__(self, trigger_classes: List[str], min_confidence: float = 0.6):
|
||||
"""
|
||||
Initialize multi-camera tracker
|
||||
|
||||
Args:
|
||||
trigger_classes: List of class names to track
|
||||
min_confidence: Minimum detection confidence threshold
|
||||
"""
|
||||
self.trigger_classes = trigger_classes
|
||||
self.min_confidence = min_confidence
|
||||
|
||||
# Camera-specific trackers
|
||||
self.camera_trackers: Dict[str, CameraTracker] = {}
|
||||
|
||||
logger.info(f"Initialized MultiCameraBoTSORT with classes={trigger_classes}, "
|
||||
f"min_confidence={min_confidence}")
|
||||
|
||||
def get_or_create_tracker(self, camera_id: str) -> CameraTracker:
|
||||
"""Get or create tracker for specific camera"""
|
||||
if camera_id not in self.camera_trackers:
|
||||
self.camera_trackers[camera_id] = CameraTracker(camera_id)
|
||||
logger.info(f"Created new tracker for camera {camera_id}")
|
||||
|
||||
return self.camera_trackers[camera_id]
|
||||
|
||||
def update(self, camera_id: str, inference_result) -> List[Dict]:
|
||||
"""
|
||||
Update tracker for specific camera with detections
|
||||
|
||||
Args:
|
||||
camera_id: Camera identifier
|
||||
inference_result: InferenceResult with detections
|
||||
|
||||
Returns:
|
||||
List of track information dictionaries
|
||||
"""
|
||||
# Filter detections by confidence and trigger classes
|
||||
filtered_detections = []
|
||||
|
||||
if hasattr(inference_result, 'detections') and inference_result.detections:
|
||||
for detection in inference_result.detections:
|
||||
if (detection.confidence >= self.min_confidence and
|
||||
detection.class_name in self.trigger_classes):
|
||||
filtered_detections.append(detection)
|
||||
|
||||
# Get camera tracker and update
|
||||
tracker = self.get_or_create_tracker(camera_id)
|
||||
confirmed_tracks = tracker.update(filtered_detections)
|
||||
|
||||
# Convert tracks to output format
|
||||
track_results = []
|
||||
for track in confirmed_tracks:
|
||||
track_results.append({
|
||||
'track_id': track.track_id,
|
||||
'camera_id': track.camera_id,
|
||||
'bbox': track.bbox,
|
||||
'confidence': track.confidence,
|
||||
'class_name': track.class_name,
|
||||
'hit_streak': track.hit_streak,
|
||||
'age': track.age
|
||||
})
|
||||
|
||||
return track_results
|
||||
|
||||
def get_statistics(self) -> Dict[str, Any]:
|
||||
"""Get tracking statistics across all cameras"""
|
||||
stats = {}
|
||||
total_tracks = 0
|
||||
|
||||
for camera_id, tracker in self.camera_trackers.items():
|
||||
camera_stats = {
|
||||
'active_tracks': len([t for t in tracker.tracks.values() if t.is_confirmed()]),
|
||||
'total_tracks': len(tracker.tracks),
|
||||
'frame_count': tracker.frame_count
|
||||
}
|
||||
stats[camera_id] = camera_stats
|
||||
total_tracks += camera_stats['active_tracks']
|
||||
|
||||
stats['summary'] = {
|
||||
'total_cameras': len(self.camera_trackers),
|
||||
'total_active_tracks': total_tracks
|
||||
}
|
||||
|
||||
return stats
|
||||
|
||||
def reset_camera(self, camera_id: str):
|
||||
"""Reset tracking for specific camera"""
|
||||
if camera_id in self.camera_trackers:
|
||||
del self.camera_trackers[camera_id]
|
||||
logger.info(f"Reset tracking for camera {camera_id}")
|
||||
|
||||
def reset_all(self):
|
||||
"""Reset all camera trackers"""
|
||||
self.camera_trackers.clear()
|
||||
logger.info("Reset all camera trackers")
|
|
@ -63,7 +63,7 @@ class TrackingPipelineIntegration:
|
|||
self.pending_processing_data: Dict[str, Dict] = {} # display_id -> processing data (waiting for session ID)
|
||||
|
||||
# Additional validators for enhanced flow control
|
||||
self.permanently_processed: Dict[int, float] = {} # track_id -> process_time (never process again)
|
||||
self.permanently_processed: Dict[str, float] = {} # "camera_id:track_id" -> process_time (never process again)
|
||||
self.progression_stages: Dict[str, str] = {} # session_id -> current_stage
|
||||
self.last_detection_time: Dict[str, float] = {} # display_id -> last_detection_timestamp
|
||||
self.abandonment_timeout = 3.0 # seconds to wait before declaring car abandoned
|
||||
|
@ -183,7 +183,7 @@ class TrackingPipelineIntegration:
|
|||
|
||||
# Run tracking model
|
||||
if self.tracking_model:
|
||||
# Run inference with tracking
|
||||
# Run detection-only (tracking handled by our own tracker)
|
||||
tracking_results = self.tracking_model.track(
|
||||
frame,
|
||||
confidence_threshold=self.tracker.min_confidence,
|
||||
|
@ -486,7 +486,10 @@ class TrackingPipelineIntegration:
|
|||
self.session_vehicles[session_id] = track_id
|
||||
|
||||
# Mark vehicle as permanently processed (won't process again even after session clear)
|
||||
self.permanently_processed[track_id] = time.time()
|
||||
# Use composite key to distinguish same track IDs across different cameras
|
||||
camera_id = display_id # Using display_id as camera_id for isolation
|
||||
permanent_key = f"{camera_id}:{track_id}"
|
||||
self.permanently_processed[permanent_key] = time.time()
|
||||
|
||||
# Remove from pending
|
||||
del self.pending_vehicles[display_id]
|
||||
|
@ -667,6 +670,7 @@ class TrackingPipelineIntegration:
|
|||
self.executor.shutdown(wait=False)
|
||||
self.reset_tracking()
|
||||
|
||||
|
||||
# Cleanup detection pipeline
|
||||
if self.detection_pipeline:
|
||||
self.detection_pipeline.cleanup()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"""
|
||||
Vehicle Tracking Module - Continuous tracking with front_rear_detection model
|
||||
Implements vehicle identification, persistence, and motion analysis.
|
||||
Vehicle Tracking Module - BoT-SORT based tracking with camera isolation
|
||||
Implements vehicle identification, persistence, and motion analysis using external tracker.
|
||||
"""
|
||||
import logging
|
||||
import time
|
||||
|
@ -10,6 +10,8 @@ from dataclasses import dataclass, field
|
|||
import numpy as np
|
||||
from threading import Lock
|
||||
|
||||
from .bot_sort_tracker import MultiCameraBoTSORT
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -17,6 +19,7 @@ logger = logging.getLogger(__name__)
|
|||
class TrackedVehicle:
|
||||
"""Represents a tracked vehicle with all its state information."""
|
||||
track_id: int
|
||||
camera_id: str
|
||||
first_seen: float
|
||||
last_seen: float
|
||||
session_id: Optional[str] = None
|
||||
|
@ -30,6 +33,8 @@ class TrackedVehicle:
|
|||
processed_pipeline: bool = False
|
||||
last_position_history: List[Tuple[float, float]] = field(default_factory=list)
|
||||
avg_confidence: float = 0.0
|
||||
hit_streak: int = 0
|
||||
age: int = 0
|
||||
|
||||
def update_position(self, bbox: Tuple[int, int, int, int], confidence: float):
|
||||
"""Update vehicle position and confidence."""
|
||||
|
@ -73,7 +78,7 @@ class TrackedVehicle:
|
|||
|
||||
class VehicleTracker:
|
||||
"""
|
||||
Main vehicle tracking implementation using YOLO tracking capabilities.
|
||||
Main vehicle tracking implementation using BoT-SORT with camera isolation.
|
||||
Manages continuous tracking, vehicle identification, and state persistence.
|
||||
"""
|
||||
|
||||
|
@ -88,18 +93,19 @@ class VehicleTracker:
|
|||
self.trigger_classes = self.config.get('trigger_classes', self.config.get('triggerClasses', ['frontal']))
|
||||
self.min_confidence = self.config.get('minConfidence', 0.6)
|
||||
|
||||
# Tracking state
|
||||
self.tracked_vehicles: Dict[int, TrackedVehicle] = {}
|
||||
self.next_track_id = 1
|
||||
# BoT-SORT multi-camera tracker
|
||||
self.bot_sort = MultiCameraBoTSORT(self.trigger_classes, self.min_confidence)
|
||||
|
||||
# Tracking state - maintain compatibility with existing code
|
||||
self.tracked_vehicles: Dict[str, Dict[int, TrackedVehicle]] = {} # camera_id -> {track_id: vehicle}
|
||||
self.lock = Lock()
|
||||
|
||||
# Tracking parameters
|
||||
self.stability_threshold = 0.7
|
||||
self.min_stable_frames = 5
|
||||
self.position_tolerance = 50 # pixels
|
||||
self.timeout_seconds = 2.0
|
||||
|
||||
logger.info(f"VehicleTracker initialized with trigger_classes={self.trigger_classes}, "
|
||||
logger.info(f"VehicleTracker initialized with BoT-SORT: trigger_classes={self.trigger_classes}, "
|
||||
f"min_confidence={self.min_confidence}")
|
||||
|
||||
def process_detections(self,
|
||||
|
@ -107,10 +113,10 @@ class VehicleTracker:
|
|||
display_id: str,
|
||||
frame: np.ndarray) -> List[TrackedVehicle]:
|
||||
"""
|
||||
Process YOLO detection results and update tracking state.
|
||||
Process detection results using BoT-SORT tracking.
|
||||
|
||||
Args:
|
||||
results: YOLO detection results with tracking
|
||||
results: Detection results (InferenceResult)
|
||||
display_id: Display identifier for this stream
|
||||
frame: Current frame being processed
|
||||
|
||||
|
@ -118,108 +124,67 @@ class VehicleTracker:
|
|||
List of currently tracked vehicles
|
||||
"""
|
||||
current_time = time.time()
|
||||
active_tracks = []
|
||||
|
||||
# Extract camera_id from display_id for tracking isolation
|
||||
camera_id = display_id # Using display_id as camera_id for isolation
|
||||
|
||||
with self.lock:
|
||||
# Clean up expired tracks
|
||||
expired_ids = [
|
||||
track_id for track_id, vehicle in self.tracked_vehicles.items()
|
||||
if vehicle.is_expired(self.timeout_seconds)
|
||||
]
|
||||
for track_id in expired_ids:
|
||||
logger.debug(f"Removing expired track {track_id}")
|
||||
del self.tracked_vehicles[track_id]
|
||||
# Update BoT-SORT tracker
|
||||
track_results = self.bot_sort.update(camera_id, results)
|
||||
|
||||
# Process new detections from InferenceResult
|
||||
if hasattr(results, 'detections') and results.detections:
|
||||
# Process detections from InferenceResult
|
||||
for detection in results.detections:
|
||||
# Skip if confidence is too low
|
||||
if detection.confidence < self.min_confidence:
|
||||
continue
|
||||
# Ensure camera tracking dict exists
|
||||
if camera_id not in self.tracked_vehicles:
|
||||
self.tracked_vehicles[camera_id] = {}
|
||||
|
||||
# Check if class is in trigger classes
|
||||
if detection.class_name not in self.trigger_classes:
|
||||
continue
|
||||
# Update tracked vehicles based on BoT-SORT results
|
||||
current_tracks = {}
|
||||
active_tracks = []
|
||||
|
||||
# Use track_id if available, otherwise generate one
|
||||
track_id = detection.track_id if detection.track_id is not None else self.next_track_id
|
||||
if detection.track_id is None:
|
||||
self.next_track_id += 1
|
||||
for track_result in track_results:
|
||||
track_id = track_result['track_id']
|
||||
|
||||
# Get bounding box from Detection object
|
||||
x1, y1, x2, y2 = detection.bbox
|
||||
bbox = (int(x1), int(y1), int(x2), int(y2))
|
||||
# Create or update TrackedVehicle
|
||||
if track_id in self.tracked_vehicles[camera_id]:
|
||||
# Update existing vehicle
|
||||
vehicle = self.tracked_vehicles[camera_id][track_id]
|
||||
vehicle.update_position(track_result['bbox'], track_result['confidence'])
|
||||
vehicle.hit_streak = track_result['hit_streak']
|
||||
vehicle.age = track_result['age']
|
||||
|
||||
# Update or create tracked vehicle
|
||||
confidence = detection.confidence
|
||||
if track_id in self.tracked_vehicles:
|
||||
# Update existing track
|
||||
vehicle = self.tracked_vehicles[track_id]
|
||||
vehicle.update_position(bbox, confidence)
|
||||
vehicle.display_id = display_id
|
||||
# Update stability based on hit_streak
|
||||
if vehicle.hit_streak >= self.min_stable_frames:
|
||||
vehicle.is_stable = True
|
||||
vehicle.stable_frames = vehicle.hit_streak
|
||||
|
||||
# Check stability
|
||||
stability = vehicle.calculate_stability()
|
||||
if stability > self.stability_threshold:
|
||||
vehicle.stable_frames += 1
|
||||
if vehicle.stable_frames >= self.min_stable_frames:
|
||||
vehicle.is_stable = True
|
||||
else:
|
||||
vehicle.stable_frames = max(0, vehicle.stable_frames - 1)
|
||||
if vehicle.stable_frames < self.min_stable_frames:
|
||||
vehicle.is_stable = False
|
||||
logger.debug(f"Updated track {track_id}: conf={vehicle.confidence:.2f}, "
|
||||
f"stable={vehicle.is_stable}, hit_streak={vehicle.hit_streak}")
|
||||
else:
|
||||
# Create new vehicle
|
||||
x1, y1, x2, y2 = track_result['bbox']
|
||||
vehicle = TrackedVehicle(
|
||||
track_id=track_id,
|
||||
camera_id=camera_id,
|
||||
first_seen=current_time,
|
||||
last_seen=current_time,
|
||||
display_id=display_id,
|
||||
confidence=track_result['confidence'],
|
||||
bbox=tuple(track_result['bbox']),
|
||||
center=((x1 + x2) / 2, (y1 + y2) / 2),
|
||||
total_frames=1,
|
||||
hit_streak=track_result['hit_streak'],
|
||||
age=track_result['age']
|
||||
)
|
||||
vehicle.last_position_history.append(vehicle.center)
|
||||
logger.info(f"New vehicle tracked: ID={track_id}, camera={camera_id}, display={display_id}")
|
||||
|
||||
logger.debug(f"Updated track {track_id}: conf={confidence:.2f}, "
|
||||
f"stable={vehicle.is_stable}, stability={stability:.2f}")
|
||||
else:
|
||||
# Create new track
|
||||
vehicle = TrackedVehicle(
|
||||
track_id=track_id,
|
||||
first_seen=current_time,
|
||||
last_seen=current_time,
|
||||
display_id=display_id,
|
||||
confidence=confidence,
|
||||
bbox=bbox,
|
||||
center=((x1 + x2) / 2, (y1 + y2) / 2),
|
||||
total_frames=1
|
||||
)
|
||||
vehicle.last_position_history.append(vehicle.center)
|
||||
self.tracked_vehicles[track_id] = vehicle
|
||||
logger.info(f"New vehicle tracked: ID={track_id}, display={display_id}")
|
||||
current_tracks[track_id] = vehicle
|
||||
active_tracks.append(vehicle)
|
||||
|
||||
active_tracks.append(self.tracked_vehicles[track_id])
|
||||
# Update the camera's tracked vehicles
|
||||
self.tracked_vehicles[camera_id] = current_tracks
|
||||
|
||||
return active_tracks
|
||||
|
||||
def _find_closest_track(self, center: Tuple[float, float]) -> Optional[TrackedVehicle]:
|
||||
"""
|
||||
Find the closest existing track to a given position.
|
||||
|
||||
Args:
|
||||
center: Center position to match
|
||||
|
||||
Returns:
|
||||
Closest tracked vehicle if within tolerance, None otherwise
|
||||
"""
|
||||
min_distance = float('inf')
|
||||
closest_track = None
|
||||
|
||||
for vehicle in self.tracked_vehicles.values():
|
||||
if vehicle.is_expired(0.5): # Shorter timeout for matching
|
||||
continue
|
||||
|
||||
distance = np.sqrt(
|
||||
(center[0] - vehicle.center[0]) ** 2 +
|
||||
(center[1] - vehicle.center[1]) ** 2
|
||||
)
|
||||
|
||||
if distance < min_distance and distance < self.position_tolerance:
|
||||
min_distance = distance
|
||||
closest_track = vehicle
|
||||
|
||||
return closest_track
|
||||
|
||||
def get_stable_vehicles(self, display_id: Optional[str] = None) -> List[TrackedVehicle]:
|
||||
"""
|
||||
Get all stable vehicles, optionally filtered by display.
|
||||
|
@ -231,11 +196,15 @@ class VehicleTracker:
|
|||
List of stable tracked vehicles
|
||||
"""
|
||||
with self.lock:
|
||||
stable = [
|
||||
v for v in self.tracked_vehicles.values()
|
||||
if v.is_stable and not v.is_expired(self.timeout_seconds)
|
||||
and (display_id is None or v.display_id == display_id)
|
||||
]
|
||||
stable = []
|
||||
camera_id = display_id # Using display_id as camera_id
|
||||
|
||||
if camera_id in self.tracked_vehicles:
|
||||
for vehicle in self.tracked_vehicles[camera_id].values():
|
||||
if (vehicle.is_stable and not vehicle.is_expired(self.timeout_seconds) and
|
||||
(display_id is None or vehicle.display_id == display_id)):
|
||||
stable.append(vehicle)
|
||||
|
||||
return stable
|
||||
|
||||
def get_vehicle_by_session(self, session_id: str) -> Optional[TrackedVehicle]:
|
||||
|
@ -249,9 +218,11 @@ class VehicleTracker:
|
|||
Tracked vehicle if found, None otherwise
|
||||
"""
|
||||
with self.lock:
|
||||
for vehicle in self.tracked_vehicles.values():
|
||||
if vehicle.session_id == session_id:
|
||||
return vehicle
|
||||
# Search across all cameras
|
||||
for camera_vehicles in self.tracked_vehicles.values():
|
||||
for vehicle in camera_vehicles.values():
|
||||
if vehicle.session_id == session_id:
|
||||
return vehicle
|
||||
return None
|
||||
|
||||
def mark_processed(self, track_id: int, session_id: str):
|
||||
|
@ -263,11 +234,14 @@ class VehicleTracker:
|
|||
session_id: Session ID assigned to this vehicle
|
||||
"""
|
||||
with self.lock:
|
||||
if track_id in self.tracked_vehicles:
|
||||
vehicle = self.tracked_vehicles[track_id]
|
||||
vehicle.processed_pipeline = True
|
||||
vehicle.session_id = session_id
|
||||
logger.info(f"Marked vehicle {track_id} as processed with session {session_id}")
|
||||
# Search across all cameras for the track_id
|
||||
for camera_vehicles in self.tracked_vehicles.values():
|
||||
if track_id in camera_vehicles:
|
||||
vehicle = camera_vehicles[track_id]
|
||||
vehicle.processed_pipeline = True
|
||||
vehicle.session_id = session_id
|
||||
logger.info(f"Marked vehicle {track_id} as processed with session {session_id}")
|
||||
return
|
||||
|
||||
def clear_session(self, session_id: str):
|
||||
"""
|
||||
|
@ -277,30 +251,43 @@ class VehicleTracker:
|
|||
session_id: Session ID to clear
|
||||
"""
|
||||
with self.lock:
|
||||
for vehicle in self.tracked_vehicles.values():
|
||||
if vehicle.session_id == session_id:
|
||||
logger.info(f"Clearing session {session_id} from vehicle {vehicle.track_id}")
|
||||
vehicle.session_id = None
|
||||
# Keep processed_pipeline=True to prevent re-processing
|
||||
# Search across all cameras
|
||||
for camera_vehicles in self.tracked_vehicles.values():
|
||||
for vehicle in camera_vehicles.values():
|
||||
if vehicle.session_id == session_id:
|
||||
logger.info(f"Clearing session {session_id} from vehicle {vehicle.track_id}")
|
||||
vehicle.session_id = None
|
||||
# Keep processed_pipeline=True to prevent re-processing
|
||||
|
||||
def reset_tracking(self):
|
||||
"""Reset all tracking state."""
|
||||
with self.lock:
|
||||
self.tracked_vehicles.clear()
|
||||
self.next_track_id = 1
|
||||
self.bot_sort.reset_all()
|
||||
logger.info("Vehicle tracking state reset")
|
||||
|
||||
def get_statistics(self) -> Dict:
|
||||
"""Get tracking statistics."""
|
||||
with self.lock:
|
||||
total = len(self.tracked_vehicles)
|
||||
stable = sum(1 for v in self.tracked_vehicles.values() if v.is_stable)
|
||||
processed = sum(1 for v in self.tracked_vehicles.values() if v.processed_pipeline)
|
||||
total = 0
|
||||
stable = 0
|
||||
processed = 0
|
||||
all_confidences = []
|
||||
|
||||
# Aggregate stats across all cameras
|
||||
for camera_vehicles in self.tracked_vehicles.values():
|
||||
total += len(camera_vehicles)
|
||||
for vehicle in camera_vehicles.values():
|
||||
if vehicle.is_stable:
|
||||
stable += 1
|
||||
if vehicle.processed_pipeline:
|
||||
processed += 1
|
||||
all_confidences.append(vehicle.avg_confidence)
|
||||
|
||||
return {
|
||||
'total_tracked': total,
|
||||
'stable_vehicles': stable,
|
||||
'processed_vehicles': processed,
|
||||
'avg_confidence': np.mean([v.avg_confidence for v in self.tracked_vehicles.values()])
|
||||
if self.tracked_vehicles else 0.0
|
||||
'avg_confidence': np.mean(all_confidences) if all_confidences else 0.0,
|
||||
'bot_sort_stats': self.bot_sort.get_statistics()
|
||||
}
|
|
@ -354,25 +354,28 @@ class StableCarValidator:
|
|||
def should_skip_same_car(self,
|
||||
vehicle: TrackedVehicle,
|
||||
session_cleared: bool = False,
|
||||
permanently_processed: Dict[int, float] = None) -> bool:
|
||||
permanently_processed: Dict[str, float] = None) -> bool:
|
||||
"""
|
||||
Determine if we should skip processing for the same car after session clear.
|
||||
|
||||
Args:
|
||||
vehicle: The tracked vehicle
|
||||
session_cleared: Whether the session was recently cleared
|
||||
permanently_processed: Dict of permanently processed vehicles
|
||||
permanently_processed: Dict of permanently processed vehicles (camera_id:track_id -> time)
|
||||
|
||||
Returns:
|
||||
True if we should skip this vehicle
|
||||
"""
|
||||
# Check if this vehicle was permanently processed (never process again)
|
||||
if permanently_processed and vehicle.track_id in permanently_processed:
|
||||
process_time = permanently_processed[vehicle.track_id]
|
||||
time_since = time.time() - process_time
|
||||
logger.debug(f"Skipping permanently processed vehicle {vehicle.track_id} "
|
||||
f"(processed {time_since:.1f}s ago)")
|
||||
return True
|
||||
if permanently_processed:
|
||||
# Create composite key using camera_id and track_id
|
||||
permanent_key = f"{vehicle.camera_id}:{vehicle.track_id}"
|
||||
if permanent_key in permanently_processed:
|
||||
process_time = permanently_processed[permanent_key]
|
||||
time_since = time.time() - process_time
|
||||
logger.debug(f"Skipping permanently processed vehicle {vehicle.track_id} on camera {vehicle.camera_id} "
|
||||
f"(processed {time_since:.1f}s ago)")
|
||||
return True
|
||||
|
||||
# If vehicle has a session_id but it was cleared, skip for a period
|
||||
if vehicle.session_id is None and vehicle.processed_pipeline and session_cleared:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue