Refactor: PHASE 2: Core Module Extraction
This commit is contained in:
parent
96bedae80a
commit
4e9ae6bcc4
7 changed files with 3684 additions and 0 deletions
481
detector_worker/detection/tracking_manager.py
Normal file
481
detector_worker/detection/tracking_manager.py
Normal file
|
@ -0,0 +1,481 @@
|
|||
"""
|
||||
Object tracking management and state handling.
|
||||
|
||||
This module provides tracking state management, session handling,
|
||||
and occupancy detection functionality for the detection pipeline.
|
||||
"""
|
||||
|
||||
import time
|
||||
import logging
|
||||
from typing import Dict, List, Any, Optional, Tuple, Set
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
|
||||
from ..core.constants import SESSION_TIMEOUT_SECONDS
|
||||
from ..core.exceptions import TrackingError, create_detection_error
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SessionState:
|
||||
"""Session state for tracking management."""
|
||||
active: bool = True
|
||||
waiting_for_backend_session: bool = False
|
||||
wait_start_time: float = 0.0
|
||||
reset_tracker_on_resume: bool = False
|
||||
occupancy_mode: bool = False
|
||||
occupancy_enabled_at: Optional[float] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary format."""
|
||||
return {
|
||||
"active": self.active,
|
||||
"waiting_for_backend_session": self.waiting_for_backend_session,
|
||||
"wait_start_time": self.wait_start_time,
|
||||
"reset_tracker_on_resume": self.reset_tracker_on_resume,
|
||||
"occupancy_mode": self.occupancy_mode,
|
||||
"occupancy_enabled_at": self.occupancy_enabled_at
|
||||
}
|
||||
|
||||
def from_dict(self, data: Dict[str, Any]) -> None:
|
||||
"""Update from dictionary data."""
|
||||
self.active = data.get("active", True)
|
||||
self.waiting_for_backend_session = data.get("waiting_for_backend_session", False)
|
||||
self.wait_start_time = data.get("wait_start_time", 0.0)
|
||||
self.reset_tracker_on_resume = data.get("reset_tracker_on_resume", False)
|
||||
self.occupancy_mode = data.get("occupancy_mode", False)
|
||||
self.occupancy_enabled_at = data.get("occupancy_enabled_at")
|
||||
|
||||
|
||||
@dataclass
|
||||
class TrackingData:
|
||||
"""Complete tracking data for a camera and model."""
|
||||
track_stability_counters: Dict[int, int] = field(default_factory=dict)
|
||||
stable_tracks: Set[int] = field(default_factory=set)
|
||||
session_state: SessionState = field(default_factory=SessionState)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary format."""
|
||||
return {
|
||||
"track_stability_counters": dict(self.track_stability_counters),
|
||||
"stable_tracks": list(self.stable_tracks),
|
||||
"session_state": self.session_state.to_dict()
|
||||
}
|
||||
|
||||
|
||||
class TrackingManager:
|
||||
"""
|
||||
Manages object tracking state and session handling across cameras and models.
|
||||
|
||||
This class provides centralized tracking state management including:
|
||||
- Track stability counters
|
||||
- Session state management
|
||||
- Backend session timeout handling
|
||||
- Occupancy detection mode
|
||||
"""
|
||||
|
||||
def __init__(self, session_timeout_seconds: int = SESSION_TIMEOUT_SECONDS):
|
||||
"""
|
||||
Initialize tracking manager.
|
||||
|
||||
Args:
|
||||
session_timeout_seconds: Timeout for backend session waiting
|
||||
"""
|
||||
self.session_timeout_seconds = session_timeout_seconds
|
||||
self._camera_tracking_data: Dict[str, Dict[str, TrackingData]] = {}
|
||||
self._lock = None
|
||||
|
||||
def _ensure_thread_safety(self):
|
||||
"""Initialize thread safety if not already done."""
|
||||
if self._lock is None:
|
||||
import threading
|
||||
self._lock = threading.RLock()
|
||||
|
||||
def get_camera_tracking_data(self, camera_id: str, model_id: str) -> TrackingData:
|
||||
"""
|
||||
Get or create tracking data for a specific camera and model.
|
||||
|
||||
Args:
|
||||
camera_id: Unique camera identifier
|
||||
model_id: Model identifier
|
||||
|
||||
Returns:
|
||||
Tracking data for the camera and model
|
||||
"""
|
||||
self._ensure_thread_safety()
|
||||
|
||||
with self._lock:
|
||||
if camera_id not in self._camera_tracking_data:
|
||||
self._camera_tracking_data[camera_id] = {}
|
||||
|
||||
if model_id not in self._camera_tracking_data[camera_id]:
|
||||
logger.warning(f"🔄 Camera {camera_id}: Creating NEW tracking data for {model_id} - this will reset any cooldown!")
|
||||
self._camera_tracking_data[camera_id][model_id] = TrackingData()
|
||||
|
||||
return self._camera_tracking_data[camera_id][model_id]
|
||||
|
||||
def check_stable_tracks(self,
|
||||
camera_id: str,
|
||||
model_id: str,
|
||||
regions_dict: Dict[str, Any]) -> Tuple[bool, List[Dict[str, Any]]]:
|
||||
"""
|
||||
Check if any stable tracks match the detected classes for a specific camera.
|
||||
|
||||
Args:
|
||||
camera_id: Unique camera identifier
|
||||
model_id: Model identifier
|
||||
regions_dict: Dictionary mapping class names to detection regions
|
||||
|
||||
Returns:
|
||||
Tuple of (has_stable_tracks, stable_detections)
|
||||
"""
|
||||
self._ensure_thread_safety()
|
||||
|
||||
with self._lock:
|
||||
tracking_data = self.get_camera_tracking_data(camera_id, model_id)
|
||||
stable_tracks = tracking_data.stable_tracks
|
||||
|
||||
if not stable_tracks:
|
||||
return False, []
|
||||
|
||||
# Check for track-based stability
|
||||
stable_detections = []
|
||||
|
||||
for class_name, region_data in regions_dict.items():
|
||||
detection = region_data.get("detection", {})
|
||||
track_id = detection.get("id")
|
||||
|
||||
if track_id in stable_tracks:
|
||||
stable_detections.append(detection)
|
||||
|
||||
has_stable = len(stable_detections) > 0
|
||||
logger.debug(f"Camera {camera_id}: Stable track check - stable_tracks: {list(stable_tracks)}, found: {has_stable}")
|
||||
|
||||
return has_stable, stable_detections
|
||||
|
||||
def reset_tracking_state(self,
|
||||
camera_id: str,
|
||||
model_id: str,
|
||||
reason: str = "session ended") -> None:
|
||||
"""
|
||||
Reset tracking state after session completion or timeout.
|
||||
|
||||
Args:
|
||||
camera_id: Unique camera identifier
|
||||
model_id: Model identifier
|
||||
reason: Reason for reset (for logging)
|
||||
"""
|
||||
self._ensure_thread_safety()
|
||||
|
||||
with self._lock:
|
||||
tracking_data = self.get_camera_tracking_data(camera_id, model_id)
|
||||
session_state = tracking_data.session_state
|
||||
|
||||
# Clear all tracking data for fresh start
|
||||
tracking_data.track_stability_counters.clear()
|
||||
tracking_data.stable_tracks.clear()
|
||||
session_state.active = True
|
||||
session_state.waiting_for_backend_session = False
|
||||
session_state.wait_start_time = 0.0
|
||||
session_state.reset_tracker_on_resume = True
|
||||
|
||||
logger.info(f"Camera {camera_id}: 🔄 Reset tracking state - {reason}")
|
||||
logger.info(f"Camera {camera_id}: 🧹 Cleared stability counters and stable tracks for fresh session")
|
||||
|
||||
def is_camera_active(self, camera_id: str, model_id: str) -> bool:
|
||||
"""
|
||||
Check if camera should be processing detections.
|
||||
|
||||
Args:
|
||||
camera_id: Unique camera identifier
|
||||
model_id: Model identifier
|
||||
|
||||
Returns:
|
||||
True if camera should process detections, False otherwise
|
||||
"""
|
||||
self._ensure_thread_safety()
|
||||
|
||||
with self._lock:
|
||||
tracking_data = self.get_camera_tracking_data(camera_id, model_id)
|
||||
session_state = tracking_data.session_state
|
||||
|
||||
# Check if waiting for backend sessionId has timed out
|
||||
if session_state.waiting_for_backend_session:
|
||||
current_time = time.time()
|
||||
wait_start_time = session_state.wait_start_time
|
||||
elapsed_time = current_time - wait_start_time
|
||||
|
||||
if elapsed_time >= self.session_timeout_seconds:
|
||||
logger.warning(f"Camera {camera_id}: Backend sessionId timeout ({self.session_timeout_seconds}s) - resetting tracking")
|
||||
self.reset_tracking_state(camera_id, model_id, "backend sessionId timeout")
|
||||
return True
|
||||
else:
|
||||
remaining_time = self.session_timeout_seconds - elapsed_time
|
||||
logger.debug(f"Camera {camera_id}: Still waiting for backend sessionId ({remaining_time:.1f}s remaining)")
|
||||
return False
|
||||
|
||||
return session_state.active
|
||||
|
||||
def set_waiting_for_backend_session(self,
|
||||
camera_id: str,
|
||||
model_id: str,
|
||||
waiting: bool = True) -> None:
|
||||
"""
|
||||
Set whether camera is waiting for backend session ID.
|
||||
|
||||
Args:
|
||||
camera_id: Unique camera identifier
|
||||
model_id: Model identifier
|
||||
waiting: Whether to wait for backend session
|
||||
"""
|
||||
self._ensure_thread_safety()
|
||||
|
||||
with self._lock:
|
||||
tracking_data = self.get_camera_tracking_data(camera_id, model_id)
|
||||
session_state = tracking_data.session_state
|
||||
|
||||
session_state.waiting_for_backend_session = waiting
|
||||
if waiting:
|
||||
session_state.wait_start_time = time.time()
|
||||
logger.debug(f"Camera {camera_id}: Started waiting for backend sessionId")
|
||||
else:
|
||||
session_state.wait_start_time = 0.0
|
||||
logger.debug(f"Camera {camera_id}: Stopped waiting for backend sessionId")
|
||||
|
||||
def cleanup_camera_tracking(self, camera_id: str) -> None:
|
||||
"""
|
||||
Clean up tracking data when a camera is disconnected.
|
||||
|
||||
Args:
|
||||
camera_id: Unique camera identifier
|
||||
"""
|
||||
self._ensure_thread_safety()
|
||||
|
||||
with self._lock:
|
||||
if camera_id in self._camera_tracking_data:
|
||||
del self._camera_tracking_data[camera_id]
|
||||
logger.info(f"Cleaned up tracking data for camera {camera_id}")
|
||||
|
||||
def set_occupancy_mode(self,
|
||||
camera_id: str,
|
||||
model_id: str,
|
||||
enable: bool = True) -> bool:
|
||||
"""
|
||||
Enable or disable occupancy detection mode.
|
||||
|
||||
Occupancy mode stops model inference after pipeline completion
|
||||
while backend session handling continues in background.
|
||||
|
||||
Args:
|
||||
camera_id: Unique camera identifier
|
||||
model_id: Model identifier
|
||||
enable: True to enable occupancy mode, False to disable
|
||||
|
||||
Returns:
|
||||
Current occupancy mode state
|
||||
"""
|
||||
self._ensure_thread_safety()
|
||||
|
||||
with self._lock:
|
||||
tracking_data = self.get_camera_tracking_data(camera_id, model_id)
|
||||
session_state = tracking_data.session_state
|
||||
|
||||
if enable:
|
||||
session_state.occupancy_mode = True
|
||||
session_state.occupancy_enabled_at = time.time()
|
||||
logger.debug(f"Camera {camera_id}: Occupancy mode ENABLED - model will stop after pipeline completion")
|
||||
else:
|
||||
session_state.occupancy_mode = False
|
||||
session_state.occupancy_enabled_at = None
|
||||
logger.debug(f"Camera {camera_id}: Occupancy mode DISABLED - model will continue running")
|
||||
|
||||
return session_state.occupancy_mode
|
||||
|
||||
def is_occupancy_mode_enabled(self, camera_id: str, model_id: str) -> bool:
|
||||
"""
|
||||
Check if occupancy mode is enabled for a camera.
|
||||
|
||||
Args:
|
||||
camera_id: Unique camera identifier
|
||||
model_id: Model identifier
|
||||
|
||||
Returns:
|
||||
True if occupancy mode is enabled
|
||||
"""
|
||||
self._ensure_thread_safety()
|
||||
|
||||
with self._lock:
|
||||
tracking_data = self.get_camera_tracking_data(camera_id, model_id)
|
||||
return tracking_data.session_state.occupancy_mode
|
||||
|
||||
def get_occupancy_duration(self, camera_id: str, model_id: str) -> Optional[float]:
|
||||
"""
|
||||
Get duration since occupancy mode was enabled.
|
||||
|
||||
Args:
|
||||
camera_id: Unique camera identifier
|
||||
model_id: Model identifier
|
||||
|
||||
Returns:
|
||||
Duration in seconds since occupancy mode was enabled, or None if not enabled
|
||||
"""
|
||||
self._ensure_thread_safety()
|
||||
|
||||
with self._lock:
|
||||
tracking_data = self.get_camera_tracking_data(camera_id, model_id)
|
||||
session_state = tracking_data.session_state
|
||||
|
||||
if not session_state.occupancy_mode or not session_state.occupancy_enabled_at:
|
||||
return None
|
||||
|
||||
return time.time() - session_state.occupancy_enabled_at
|
||||
|
||||
def get_session_state(self, camera_id: str, model_id: str) -> SessionState:
|
||||
"""
|
||||
Get session state for a camera and model.
|
||||
|
||||
Args:
|
||||
camera_id: Unique camera identifier
|
||||
model_id: Model identifier
|
||||
|
||||
Returns:
|
||||
Session state object
|
||||
"""
|
||||
self._ensure_thread_safety()
|
||||
|
||||
with self._lock:
|
||||
tracking_data = self.get_camera_tracking_data(camera_id, model_id)
|
||||
return tracking_data.session_state
|
||||
|
||||
def update_session_state(self,
|
||||
camera_id: str,
|
||||
model_id: str,
|
||||
**kwargs) -> None:
|
||||
"""
|
||||
Update session state properties.
|
||||
|
||||
Args:
|
||||
camera_id: Unique camera identifier
|
||||
model_id: Model identifier
|
||||
**kwargs: Session state properties to update
|
||||
"""
|
||||
self._ensure_thread_safety()
|
||||
|
||||
with self._lock:
|
||||
tracking_data = self.get_camera_tracking_data(camera_id, model_id)
|
||||
session_state = tracking_data.session_state
|
||||
|
||||
for key, value in kwargs.items():
|
||||
if hasattr(session_state, key):
|
||||
setattr(session_state, key, value)
|
||||
logger.debug(f"Camera {camera_id}: Updated session state {key} = {value}")
|
||||
else:
|
||||
logger.warning(f"Camera {camera_id}: Unknown session state property: {key}")
|
||||
|
||||
def get_tracking_statistics(self, camera_id: str, model_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get comprehensive tracking statistics for a camera and model.
|
||||
|
||||
Args:
|
||||
camera_id: Unique camera identifier
|
||||
model_id: Model identifier
|
||||
|
||||
Returns:
|
||||
Dictionary with tracking statistics
|
||||
"""
|
||||
self._ensure_thread_safety()
|
||||
|
||||
with self._lock:
|
||||
tracking_data = self.get_camera_tracking_data(camera_id, model_id)
|
||||
current_time = time.time()
|
||||
|
||||
stats = tracking_data.to_dict()
|
||||
|
||||
# Add computed statistics
|
||||
stats["total_tracked_objects"] = len(tracking_data.track_stability_counters)
|
||||
stats["stable_track_count"] = len(tracking_data.stable_tracks)
|
||||
|
||||
session_state = tracking_data.session_state
|
||||
if session_state.waiting_for_backend_session and session_state.wait_start_time > 0:
|
||||
stats["backend_wait_duration"] = current_time - session_state.wait_start_time
|
||||
stats["backend_wait_remaining"] = max(0, self.session_timeout_seconds - stats["backend_wait_duration"])
|
||||
|
||||
if session_state.occupancy_mode and session_state.occupancy_enabled_at:
|
||||
stats["occupancy_duration"] = current_time - session_state.occupancy_enabled_at
|
||||
|
||||
return stats
|
||||
|
||||
def get_all_camera_stats(self) -> Dict[str, Dict[str, Dict[str, Any]]]:
|
||||
"""
|
||||
Get tracking statistics for all monitored cameras.
|
||||
|
||||
Returns:
|
||||
Nested dictionary: {camera_id: {model_id: stats}}
|
||||
"""
|
||||
self._ensure_thread_safety()
|
||||
|
||||
with self._lock:
|
||||
all_stats = {}
|
||||
for camera_id, models in self._camera_tracking_data.items():
|
||||
all_stats[camera_id] = {}
|
||||
for model_id in models.keys():
|
||||
all_stats[camera_id][model_id] = self.get_tracking_statistics(camera_id, model_id)
|
||||
|
||||
return all_stats
|
||||
|
||||
|
||||
# Global tracking manager instance
|
||||
tracking_manager = TrackingManager()
|
||||
|
||||
|
||||
# ===== CONVENIENCE FUNCTIONS =====
|
||||
# These provide the same interface as the original functions in pympta.py
|
||||
|
||||
def check_stable_tracks(camera_id: str, model_id: str, regions_dict: Dict[str, Any]) -> Tuple[bool, List[Dict[str, Any]]]:
|
||||
"""Check if any stable tracks match the detected classes for a specific camera."""
|
||||
return tracking_manager.check_stable_tracks(camera_id, model_id, regions_dict)
|
||||
|
||||
|
||||
def reset_tracking_state(camera_id: str, model_id: str, reason: str = "session ended") -> None:
|
||||
"""Reset tracking state after session completion or timeout."""
|
||||
tracking_manager.reset_tracking_state(camera_id, model_id, reason)
|
||||
|
||||
|
||||
def is_camera_active(camera_id: str, model_id: str) -> bool:
|
||||
"""Check if camera should be processing detections."""
|
||||
return tracking_manager.is_camera_active(camera_id, model_id)
|
||||
|
||||
|
||||
def cleanup_camera_stability(camera_id: str) -> None:
|
||||
"""Clean up tracking data when a camera is disconnected."""
|
||||
tracking_manager.cleanup_camera_tracking(camera_id)
|
||||
|
||||
|
||||
def occupancy_detector(camera_id: str, model_id: str, enable: bool = True) -> bool:
|
||||
"""
|
||||
Enable or disable occupancy detection mode.
|
||||
|
||||
When enabled:
|
||||
- Model stops inference after completing full pipeline
|
||||
- Backend sessionId handling continues in background
|
||||
|
||||
Note: This is a temporary function that will be changed in the future.
|
||||
"""
|
||||
return tracking_manager.set_occupancy_mode(camera_id, model_id, enable)
|
||||
|
||||
|
||||
def get_camera_tracking_data(camera_id: str, model_id: str) -> Dict[str, Any]:
|
||||
"""Get tracking data for a specific camera and model."""
|
||||
tracking_data = tracking_manager.get_camera_tracking_data(camera_id, model_id)
|
||||
return tracking_data.to_dict()
|
||||
|
||||
|
||||
def set_waiting_for_backend_session(camera_id: str, model_id: str, waiting: bool = True) -> None:
|
||||
"""Set whether camera is waiting for backend session ID."""
|
||||
tracking_manager.set_waiting_for_backend_session(camera_id, model_id, waiting)
|
||||
|
||||
|
||||
def get_tracking_statistics(camera_id: str, model_id: str) -> Dict[str, Any]:
|
||||
"""Get comprehensive tracking statistics for a camera and model."""
|
||||
return tracking_manager.get_tracking_statistics(camera_id, model_id)
|
Loading…
Add table
Add a link
Reference in a new issue