Refactor: done phase 4

This commit is contained in:
ziesorx 2025-09-23 17:56:40 +07:00
parent 7e8034c6e5
commit 9e4c23c75c
8 changed files with 1533 additions and 37 deletions

View file

@ -238,32 +238,42 @@ core/
- ✅ **Production Ready**: Stable concurrent streaming from multiple camera sources
- ✅ **Dependencies**: Added opencv-python, numpy, and requests to requirements.txt
## 📋 Phase 4: Vehicle Tracking System
## ✅ Phase 4: Vehicle Tracking System - COMPLETED
### 4.1 Tracking Module (`core/tracking/`)
- [ ] **Create `tracker.py`** - Vehicle tracking implementation
- [ ] Implement continuous tracking with `front_rear_detection_v1.pt`
- [ ] Add vehicle identification and persistence
- [ ] Implement tracking state management
- [ ] Add bounding box tracking and motion analysis
- **Create `tracker.py`** - Vehicle tracking implementation
- ✅ Implement continuous tracking with configurable model (front_rear_detection_v1.pt)
- ✅ Add vehicle identification and persistence with TrackedVehicle dataclass
- ✅ Implement tracking state management with thread-safe operations
- ✅ Add bounding box tracking and motion analysis with position history
- [ ] **Create `validator.py`** - Stable car validation
- [ ] Implement stable car detection algorithm
- [ ] Add passing-by vs. fueling car differentiation
- [ ] Implement validation thresholds and timing
- [ ] Add confidence scoring for validation decisions
- **Create `validator.py`** - Stable car validation
- ✅ Implement stable car detection algorithm with multiple validation criteria
- ✅ Add passing-by vs. fueling car differentiation using velocity and position analysis
- ✅ Implement validation thresholds and timing with configurable parameters
- ✅ Add confidence scoring for validation decisions with state history
- [ ] **Create `integration.py`** - Tracking-pipeline integration
- [ ] Connect tracking system with main pipeline
- [ ] Handle tracking state transitions
- [ ] Implement post-session tracking validation
- [ ] Add same-car validation after sessionId cleared
- **Create `integration.py`** - Tracking-pipeline integration
- ✅ Connect tracking system with main pipeline through TrackingPipelineIntegration
- ✅ Handle tracking state transitions and session management
- ✅ Implement post-session tracking validation with cooldown periods
- ✅ Add same-car validation after sessionId cleared with 30-second cooldown
### 4.2 Testing Phase 4
- [ ] Test continuous vehicle tracking functionality
- [ ] Test stable car validation logic
- [ ] Test integration with existing pipeline
- [ ] Verify tracking performance and accuracy
- ✅ Test continuous vehicle tracking functionality
- ✅ Test stable car validation logic
- ✅ Test integration with existing pipeline
- ✅ Verify tracking performance and accuracy
### 4.3 Phase 4 Results
- ✅ **VehicleTracker**: Complete tracking implementation with YOLO tracking integration, position history, and stability calculations
- ✅ **StableCarValidator**: Sophisticated validation logic using velocity, position variance, and state consistency
- ✅ **TrackingPipelineIntegration**: Full integration with pipeline system including session management and async processing
- ✅ **StreamManager Integration**: Updated streaming manager to process tracking on every frame with proper threading
- ✅ **Thread-Safe Operations**: All tracking operations are thread-safe with proper locking mechanisms
- ✅ **Configurable Parameters**: All tracking parameters are configurable through pipeline.json
- ✅ **Session Management**: Complete session lifecycle management with post-fueling validation
- ✅ **Statistics and Monitoring**: Comprehensive statistics collection for tracking performance
## 📋 Phase 5: Detection Pipeline System

View file

@ -18,6 +18,8 @@ from .models import (
)
from .state import worker_state, SystemMetrics
from ..models import ModelManager
from ..streaming.manager import shared_stream_manager
from ..tracking.integration import TrackingPipelineIntegration
logger = logging.getLogger(__name__)
@ -199,17 +201,8 @@ class WebSocketHandler:
# Phase 2: Download and manage models
await self._ensure_models(message.subscriptions)
# TODO: Phase 3 - Integrate with streaming management
# For now, just log the subscription changes
for subscription in message.subscriptions:
logger.info(f" Subscription: {subscription.subscriptionIdentifier} -> "
f"Model {subscription.modelId} ({subscription.modelName})")
if subscription.rtspUrl:
logger.debug(f" RTSP: {subscription.rtspUrl}")
if subscription.snapshotUrl:
logger.debug(f" Snapshot: {subscription.snapshotUrl} ({subscription.snapshotInterval}ms)")
if subscription.modelUrl:
logger.debug(f" Model: {subscription.modelUrl}")
# Phase 3 & 4: Integrate with streaming management and tracking
await self._update_stream_subscriptions(message.subscriptions)
logger.info("Subscription list updated successfully")
@ -260,6 +253,168 @@ class WebSocketHandler:
logger.info(f"[Model Management] Successfully ensured {success_count}/{len(unique_models)} models")
async def _update_stream_subscriptions(self, subscriptions) -> None:
"""Update streaming subscriptions with tracking integration."""
try:
# Convert subscriptions to the format expected by StreamManager
subscription_payloads = []
for subscription in subscriptions:
payload = {
'subscriptionIdentifier': subscription.subscriptionIdentifier,
'rtspUrl': subscription.rtspUrl,
'snapshotUrl': subscription.snapshotUrl,
'snapshotInterval': subscription.snapshotInterval,
'modelId': subscription.modelId,
'modelUrl': subscription.modelUrl,
'modelName': subscription.modelName
}
# Add crop coordinates if present
if hasattr(subscription, 'cropX1'):
payload.update({
'cropX1': subscription.cropX1,
'cropY1': subscription.cropY1,
'cropX2': subscription.cropX2,
'cropY2': subscription.cropY2
})
subscription_payloads.append(payload)
# Reconcile subscriptions with StreamManager
logger.info("[Streaming] Reconciling stream subscriptions with tracking")
reconcile_result = await self._reconcile_subscriptions_with_tracking(subscription_payloads)
logger.info(f"[Streaming] Subscription reconciliation complete: "
f"added={reconcile_result.get('added', 0)}, "
f"removed={reconcile_result.get('removed', 0)}, "
f"failed={reconcile_result.get('failed', 0)}")
except Exception as e:
logger.error(f"Error updating stream subscriptions: {e}", exc_info=True)
async def _reconcile_subscriptions_with_tracking(self, target_subscriptions) -> dict:
"""Reconcile subscriptions with tracking integration."""
try:
# First, we need to create tracking integrations for each unique model
tracking_integrations = {}
for subscription_payload in target_subscriptions:
model_id = subscription_payload['modelId']
# Create tracking integration if not already created
if model_id not in tracking_integrations:
# Get pipeline configuration for this model
pipeline_parser = model_manager.get_pipeline_config(model_id)
if pipeline_parser:
# Create tracking integration
tracking_integration = TrackingPipelineIntegration(
pipeline_parser, model_manager
)
# Initialize tracking model
success = await tracking_integration.initialize_tracking_model()
if success:
tracking_integrations[model_id] = tracking_integration
logger.info(f"[Tracking] Created tracking integration for model {model_id}")
else:
logger.warning(f"[Tracking] Failed to initialize tracking for model {model_id}")
else:
logger.warning(f"[Tracking] No pipeline config found for model {model_id}")
# Now reconcile with StreamManager, adding tracking integrations
current_subscription_ids = set()
for subscription_info in shared_stream_manager.get_all_subscriptions():
current_subscription_ids.add(subscription_info.subscription_id)
target_subscription_ids = {sub['subscriptionIdentifier'] for sub in target_subscriptions}
# Find subscriptions to remove and add
to_remove = current_subscription_ids - target_subscription_ids
to_add = target_subscription_ids - current_subscription_ids
# Remove old subscriptions
removed_count = 0
for subscription_id in to_remove:
if shared_stream_manager.remove_subscription(subscription_id):
removed_count += 1
logger.info(f"[Streaming] Removed subscription {subscription_id}")
# Add new subscriptions with tracking
added_count = 0
failed_count = 0
for subscription_payload in target_subscriptions:
subscription_id = subscription_payload['subscriptionIdentifier']
if subscription_id in to_add:
success = await self._add_subscription_with_tracking(
subscription_payload, tracking_integrations
)
if success:
added_count += 1
logger.info(f"[Streaming] Added subscription {subscription_id} with tracking")
else:
failed_count += 1
logger.error(f"[Streaming] Failed to add subscription {subscription_id}")
return {
'removed': removed_count,
'added': added_count,
'failed': failed_count,
'total_active': len(shared_stream_manager.get_all_subscriptions())
}
except Exception as e:
logger.error(f"Error in subscription reconciliation with tracking: {e}", exc_info=True)
return {'removed': 0, 'added': 0, 'failed': 0, 'total_active': 0}
async def _add_subscription_with_tracking(self, payload, tracking_integrations) -> bool:
"""Add a subscription with tracking integration."""
try:
from ..streaming.manager import StreamConfig
subscription_id = payload['subscriptionIdentifier']
camera_id = subscription_id.split(';')[-1]
model_id = payload['modelId']
# Get tracking integration for this model
tracking_integration = tracking_integrations.get(model_id)
# Extract crop coordinates if present
crop_coords = None
if all(key in payload for key in ['cropX1', 'cropY1', 'cropX2', 'cropY2']):
crop_coords = (
payload['cropX1'],
payload['cropY1'],
payload['cropX2'],
payload['cropY2']
)
# Create stream configuration
stream_config = StreamConfig(
camera_id=camera_id,
rtsp_url=payload.get('rtspUrl'),
snapshot_url=payload.get('snapshotUrl'),
snapshot_interval=payload.get('snapshotInterval', 5000),
max_retries=3,
save_test_frames=False # Disable frame saving, focus on tracking
)
# Add subscription to StreamManager with tracking
success = shared_stream_manager.add_subscription(
subscription_id=subscription_id,
stream_config=stream_config,
crop_coords=crop_coords,
model_id=model_id,
model_url=payload.get('modelUrl'),
tracking_integration=tracking_integration
)
if success and tracking_integration:
logger.info(f"[Tracking] Subscription {subscription_id} configured with tracking for model {model_id}")
return success
except Exception as e:
logger.error(f"Error adding subscription with tracking: {e}", exc_info=True)
return False
async def _ensure_single_model(self, model_id: int, model_url: str, model_name: str) -> bool:
"""Ensure a single model is downloaded and available."""
try:
@ -303,6 +458,9 @@ class WebSocketHandler:
# Update worker state
worker_state.set_session_id(display_identifier, session_id)
# Update tracking integrations with session ID
shared_stream_manager.set_session_id(display_identifier, session_id)
async def _handle_set_progression_stage(self, message: SetProgressionStageMessage) -> None:
"""Handle setProgressionStage message."""
display_identifier = message.payload.displayIdentifier
@ -313,6 +471,14 @@ class WebSocketHandler:
# Update worker state
worker_state.set_progression_stage(display_identifier, stage)
# If stage indicates session is cleared/finished, clear from tracking
if stage in ['finished', 'cleared', 'idle']:
# Get session ID for this display and clear it
session_id = worker_state.get_session_id(display_identifier)
if session_id:
shared_stream_manager.clear_session_id(session_id)
logger.info(f"[Tracking] Cleared session {session_id} due to progression stage: {stage}")
async def _handle_request_state(self, message: RequestStateMessage) -> None:
"""Handle requestState message by sending immediate state report."""
logger.debug("[RX Processing] requestState - sending immediate state report")

View file

@ -358,4 +358,82 @@ class ModelManager:
Returns:
Set of model IDs that are currently downloaded
"""
return self._downloaded_models.copy()
return self._downloaded_models.copy()
def get_pipeline_config(self, model_id: int) -> Optional[Any]:
"""
Get the pipeline configuration for a model.
Args:
model_id: The model ID
Returns:
PipelineConfig object if found, None otherwise
"""
try:
if model_id not in self._downloaded_models:
logger.warning(f"Model {model_id} not downloaded")
return None
model_path = self._model_paths.get(model_id)
if not model_path:
logger.warning(f"Model path not found for model {model_id}")
return None
# Import here to avoid circular imports
from .pipeline import PipelineParser
# Load pipeline.json
pipeline_file = model_path / "pipeline.json"
if not pipeline_file.exists():
logger.warning(f"No pipeline.json found for model {model_id}")
return None
# Create PipelineParser object and parse the configuration
pipeline_parser = PipelineParser()
success = pipeline_parser.parse(pipeline_file)
if success:
return pipeline_parser
else:
logger.error(f"Failed to parse pipeline.json for model {model_id}")
return None
except Exception as e:
logger.error(f"Error getting pipeline config for model {model_id}: {e}", exc_info=True)
return None
def get_yolo_model(self, model_id: int, model_filename: str) -> Optional[Any]:
"""
Create a YOLOWrapper instance for a specific model file.
Args:
model_id: The model ID
model_filename: The .pt model filename
Returns:
YOLOWrapper instance if successful, None otherwise
"""
try:
# Get the model file path
model_file_path = self.get_model_file_path(model_id, model_filename)
if not model_file_path or not model_file_path.exists():
logger.error(f"Model file {model_filename} not found for model {model_id}")
return None
# Import here to avoid circular imports
from .inference import YOLOWrapper
# Create YOLOWrapper instance
yolo_model = YOLOWrapper(
model_path=model_file_path,
model_id=f"{model_id}_{model_filename}",
device=None # Auto-detect device
)
logger.info(f"Created YOLOWrapper for model {model_id}: {model_filename}")
return yolo_model
except Exception as e:
logger.error(f"Error creating YOLO model for {model_id}:{model_filename}: {e}", exc_info=True)
return None

View file

@ -11,6 +11,7 @@ from collections import defaultdict
from .readers import RTSPReader, HTTPSnapshotReader
from .buffers import shared_cache_buffer, save_frame_for_testing
from ..tracking.integration import TrackingPipelineIntegration
logger = logging.getLogger(__name__)
@ -35,6 +36,9 @@ class SubscriptionInfo:
stream_config: StreamConfig
created_at: float
crop_coords: Optional[tuple] = None
model_id: Optional[str] = None
model_url: Optional[str] = None
tracking_integration: Optional[TrackingPipelineIntegration] = None
class StreamManager:
@ -48,7 +52,10 @@ class StreamManager:
self._lock = threading.RLock()
def add_subscription(self, subscription_id: str, stream_config: StreamConfig,
crop_coords: Optional[tuple] = None) -> bool:
crop_coords: Optional[tuple] = None,
model_id: Optional[str] = None,
model_url: Optional[str] = None,
tracking_integration: Optional[TrackingPipelineIntegration] = None) -> bool:
"""Add a new subscription. Returns True if successful."""
with self._lock:
if subscription_id in self._subscriptions:
@ -63,7 +70,10 @@ class StreamManager:
camera_id=camera_id,
stream_config=stream_config,
created_at=time.time(),
crop_coords=crop_coords
crop_coords=crop_coords,
model_id=model_id,
model_url=model_url,
tracking_integration=tracking_integration
)
self._subscriptions[subscription_id] = subscription_info
@ -175,9 +185,64 @@ class StreamManager:
save_frame_for_testing(camera_id, frame)
break # Only save once per frame
# Process tracking for subscriptions with tracking integration
self._process_tracking_for_camera(camera_id, frame)
except Exception as e:
logger.error(f"Error in frame callback for camera {camera_id}: {e}")
def _process_tracking_for_camera(self, camera_id: str, frame):
"""Process tracking for all subscriptions of a camera."""
try:
with self._lock:
for subscription_id in self._camera_subscribers[camera_id]:
subscription_info = self._subscriptions[subscription_id]
# Skip if no tracking integration
if not subscription_info.tracking_integration:
continue
# Extract display_id from subscription_id
display_id = subscription_id.split(';')[0] if ';' in subscription_id else subscription_id
# Process frame through tracking asynchronously
# Note: This is synchronous for now, can be made async in future
try:
# Create a simple asyncio event loop for this frame
import asyncio
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
result = loop.run_until_complete(
subscription_info.tracking_integration.process_frame(
frame, display_id, subscription_id
)
)
# Log tracking results
if result:
tracked_count = len(result.get('tracked_vehicles', []))
validated_vehicle = result.get('validated_vehicle')
pipeline_result = result.get('pipeline_result')
if tracked_count > 0:
logger.info(f"[Tracking] {camera_id}: {tracked_count} vehicles tracked")
if validated_vehicle:
logger.info(f"[Tracking] {camera_id}: Vehicle {validated_vehicle['track_id']} "
f"validated as {validated_vehicle['state']} "
f"(confidence: {validated_vehicle['confidence']:.2f})")
if pipeline_result:
logger.info(f"[Pipeline] {camera_id}: {pipeline_result.get('status', 'unknown')} - "
f"{pipeline_result.get('message', 'no message')}")
finally:
loop.close()
except Exception as track_e:
logger.error(f"Error in tracking for {subscription_id}: {track_e}")
except Exception as e:
logger.error(f"Error processing tracking for camera {camera_id}: {e}")
def get_frame(self, camera_id: str, crop_coords: Optional[tuple] = None):
"""Get the latest frame for a camera with optional cropping."""
return shared_cache_buffer.get_frame(camera_id, crop_coords)
@ -280,7 +345,13 @@ class StreamManager:
save_test_frames=True # Enable for testing
)
return self.add_subscription(subscription_id, stream_config, crop_coords)
return self.add_subscription(
subscription_id,
stream_config,
crop_coords,
model_id=payload.get('modelId'),
model_url=payload.get('modelUrl')
)
except Exception as e:
logger.error(f"Error adding subscription from payload {subscription_id}: {e}")
@ -300,10 +371,38 @@ class StreamManager:
logger.info("Stopped all streams and cleared all subscriptions")
def set_session_id(self, display_id: str, session_id: str):
"""Set session ID for tracking integration."""
with self._lock:
for subscription_info in self._subscriptions.values():
# Check if this subscription matches the display_id
subscription_display_id = subscription_info.subscription_id.split(';')[0]
if subscription_display_id == display_id and subscription_info.tracking_integration:
subscription_info.tracking_integration.set_session_id(display_id, session_id)
logger.debug(f"Set session {session_id} for display {display_id}")
def clear_session_id(self, session_id: str):
"""Clear session ID from tracking integrations."""
with self._lock:
for subscription_info in self._subscriptions.values():
if subscription_info.tracking_integration:
subscription_info.tracking_integration.clear_session_id(session_id)
logger.debug(f"Cleared session {session_id}")
def get_tracking_stats(self) -> Dict[str, Any]:
"""Get tracking statistics from all subscriptions."""
stats = {}
with self._lock:
for subscription_id, subscription_info in self._subscriptions.items():
if subscription_info.tracking_integration:
stats[subscription_id] = subscription_info.tracking_integration.get_statistics()
return stats
def get_stats(self) -> Dict[str, Any]:
"""Get comprehensive streaming statistics."""
with self._lock:
buffer_stats = shared_cache_buffer.get_stats()
tracking_stats = self.get_tracking_stats()
return {
'active_subscriptions': len(self._subscriptions),
@ -314,7 +413,8 @@ class StreamManager:
camera_id: len(subscribers)
for camera_id, subscribers in self._camera_subscribers.items()
},
'buffer_stats': buffer_stats
'buffer_stats': buffer_stats,
'tracking_stats': tracking_stats
}

View file

@ -1 +1,14 @@
# Tracking module for vehicle tracking and validation
# Tracking module for vehicle tracking and validation
from .tracker import VehicleTracker, TrackedVehicle
from .validator import StableCarValidator, ValidationResult, VehicleState
from .integration import TrackingPipelineIntegration
__all__ = [
'VehicleTracker',
'TrackedVehicle',
'StableCarValidator',
'ValidationResult',
'VehicleState',
'TrackingPipelineIntegration'
]

View file

@ -0,0 +1,369 @@
"""
Tracking-Pipeline Integration Module.
Connects the tracking system with the main detection pipeline and manages the flow.
"""
import logging
import time
import uuid
from typing import Dict, Optional, Any, List, Tuple
import asyncio
from concurrent.futures import ThreadPoolExecutor
import numpy as np
from .tracker import VehicleTracker, TrackedVehicle
from .validator import StableCarValidator, ValidationResult, VehicleState
from ..models.inference import YOLOWrapper
from ..models.pipeline import PipelineParser
logger = logging.getLogger(__name__)
class TrackingPipelineIntegration:
"""
Integrates vehicle tracking with the detection pipeline.
Manages tracking state transitions and pipeline execution triggers.
"""
def __init__(self, pipeline_parser: PipelineParser, model_manager: Any):
"""
Initialize tracking-pipeline integration.
Args:
pipeline_parser: Pipeline parser with loaded configuration
model_manager: Model manager for loading models
"""
self.pipeline_parser = pipeline_parser
self.model_manager = model_manager
# Initialize tracking components
tracking_config = pipeline_parser.tracking_config.__dict__ if pipeline_parser.tracking_config else {}
self.tracker = VehicleTracker(tracking_config)
self.validator = StableCarValidator()
# Tracking model
self.tracking_model: Optional[YOLOWrapper] = None
self.tracking_model_id = None
# Session management
self.active_sessions: Dict[str, str] = {} # display_id -> session_id
self.session_vehicles: Dict[str, int] = {} # session_id -> track_id
self.cleared_sessions: Dict[str, float] = {} # session_id -> clear_time
# Thread pool for pipeline execution
self.executor = ThreadPoolExecutor(max_workers=2)
# Statistics
self.stats = {
'frames_processed': 0,
'vehicles_detected': 0,
'vehicles_validated': 0,
'pipelines_executed': 0
}
logger.info("TrackingPipelineIntegration initialized")
async def initialize_tracking_model(self) -> bool:
"""
Load and initialize the tracking model.
Returns:
True if successful, False otherwise
"""
try:
if not self.pipeline_parser.tracking_config:
logger.warning("No tracking configuration found in pipeline")
return False
model_file = self.pipeline_parser.tracking_config.model_file
model_id = self.pipeline_parser.tracking_config.model_id
if not model_file:
logger.warning("No tracking model file specified")
return False
# Load tracking model
logger.info(f"Loading tracking model: {model_id} ({model_file})")
# Get the model ID from the ModelManager context
# We need the actual model ID, not the model string identifier
# For now, let's extract it from the model manager
pipeline_models = list(self.model_manager.get_all_downloaded_models())
if pipeline_models:
actual_model_id = pipeline_models[0] # Use the first available model
self.tracking_model = self.model_manager.get_yolo_model(actual_model_id, model_file)
else:
logger.error("No models available in ModelManager")
return False
self.tracking_model_id = model_id
if self.tracking_model:
logger.info(f"Tracking model {model_id} loaded successfully")
return True
else:
logger.error(f"Failed to load tracking model {model_id}")
return False
except Exception as e:
logger.error(f"Error initializing tracking model: {e}", exc_info=True)
return False
async def process_frame(self,
frame: np.ndarray,
display_id: str,
subscription_id: str,
session_id: Optional[str] = None) -> Dict[str, Any]:
"""
Process a frame through tracking and potentially the detection pipeline.
Args:
frame: Input frame to process
display_id: Display identifier
subscription_id: Full subscription identifier
session_id: Optional session ID from backend
Returns:
Dictionary with processing results
"""
start_time = time.time()
result = {
'tracked_vehicles': [],
'validated_vehicle': None,
'pipeline_result': None,
'session_id': session_id,
'processing_time': 0.0
}
try:
# Update stats
self.stats['frames_processed'] += 1
# Run tracking model
if self.tracking_model:
# Run inference with tracking
tracking_results = self.tracking_model.track(
frame,
confidence_threshold=self.tracker.min_confidence,
trigger_classes=self.tracker.trigger_classes,
persist=True
)
# Process tracking results
tracked_vehicles = self.tracker.process_detections(
tracking_results,
display_id,
frame
)
result['tracked_vehicles'] = [
{
'track_id': v.track_id,
'bbox': v.bbox,
'confidence': v.confidence,
'is_stable': v.is_stable,
'session_id': v.session_id
}
for v in tracked_vehicles
]
# Log tracking info periodically
if self.stats['frames_processed'] % 30 == 0: # Every 30 frames
logger.debug(f"Tracking: {len(tracked_vehicles)} vehicles, "
f"display={display_id}")
# Get stable vehicles for validation
stable_vehicles = self.tracker.get_stable_vehicles(display_id)
# Validate and potentially process stable vehicles
for vehicle in stable_vehicles:
# Check if vehicle is already processed or has session
if vehicle.processed_pipeline:
continue
# Check for session cleared (post-fueling)
if session_id and vehicle.session_id == session_id:
# Same vehicle with same session, skip
continue
# Check if this was a recently cleared session
session_cleared = False
if vehicle.session_id in self.cleared_sessions:
clear_time = self.cleared_sessions[vehicle.session_id]
if (time.time() - clear_time) < 30: # 30 second cooldown
session_cleared = True
# Skip same car after session clear
if self.validator.should_skip_same_car(vehicle, session_cleared):
continue
# Validate vehicle
validation_result = self.validator.validate_vehicle(vehicle, frame.shape)
if validation_result.is_valid and validation_result.should_process:
logger.info(f"Vehicle {vehicle.track_id} validated for processing: "
f"{validation_result.reason}")
result['validated_vehicle'] = {
'track_id': vehicle.track_id,
'state': validation_result.state.value,
'confidence': validation_result.confidence
}
# Generate session ID if not provided
if not session_id:
session_id = str(uuid.uuid4())
logger.info(f"Generated session ID: {session_id}")
# Mark vehicle as processed
self.tracker.mark_processed(vehicle.track_id, session_id)
self.session_vehicles[session_id] = vehicle.track_id
self.active_sessions[display_id] = session_id
# Execute detection pipeline (placeholder for Phase 5)
pipeline_result = await self._execute_pipeline(
frame,
vehicle,
display_id,
session_id,
subscription_id
)
result['pipeline_result'] = pipeline_result
result['session_id'] = session_id
self.stats['pipelines_executed'] += 1
# Only process one vehicle per frame
break
self.stats['vehicles_detected'] = len(tracked_vehicles)
self.stats['vehicles_validated'] = len(stable_vehicles)
else:
logger.warning("No tracking model available")
except Exception as e:
logger.error(f"Error in tracking pipeline: {e}", exc_info=True)
result['processing_time'] = time.time() - start_time
return result
async def _execute_pipeline(self,
frame: np.ndarray,
vehicle: TrackedVehicle,
display_id: str,
session_id: str,
subscription_id: str) -> Dict[str, Any]:
"""
Execute the main detection pipeline for a validated vehicle.
This is a placeholder for Phase 5 implementation.
Args:
frame: Input frame
vehicle: Validated tracked vehicle
display_id: Display identifier
session_id: Session identifier
subscription_id: Full subscription identifier
Returns:
Pipeline execution results
"""
logger.info(f"Executing pipeline for vehicle {vehicle.track_id}, "
f"session={session_id}, display={display_id}")
# Placeholder for Phase 5 pipeline execution
# This will be implemented when we create the detection module
pipeline_result = {
'status': 'pending',
'message': 'Pipeline execution will be implemented in Phase 5',
'vehicle_id': vehicle.track_id,
'session_id': session_id,
'bbox': vehicle.bbox,
'confidence': vehicle.confidence
}
# Simulate pipeline execution
await asyncio.sleep(0.1)
return pipeline_result
def set_session_id(self, display_id: str, session_id: str):
"""
Set session ID for a display (from backend).
Args:
display_id: Display identifier
session_id: Session identifier
"""
self.active_sessions[display_id] = session_id
logger.info(f"Set session {session_id} for display {display_id}")
# Find vehicle with this session
vehicle = self.tracker.get_vehicle_by_session(session_id)
if vehicle:
self.session_vehicles[session_id] = vehicle.track_id
def clear_session_id(self, session_id: str):
"""
Clear session ID (post-fueling).
Args:
session_id: Session identifier to clear
"""
# Mark session as cleared
self.cleared_sessions[session_id] = time.time()
# Clear from tracker
self.tracker.clear_session(session_id)
# Remove from active sessions
display_to_remove = None
for display_id, sess_id in self.active_sessions.items():
if sess_id == session_id:
display_to_remove = display_id
break
if display_to_remove:
del self.active_sessions[display_to_remove]
if session_id in self.session_vehicles:
del self.session_vehicles[session_id]
logger.info(f"Cleared session {session_id}")
# Clean old cleared sessions (older than 5 minutes)
current_time = time.time()
old_sessions = [
sid for sid, clear_time in self.cleared_sessions.items()
if (current_time - clear_time) > 300
]
for sid in old_sessions:
del self.cleared_sessions[sid]
def get_session_for_display(self, display_id: str) -> Optional[str]:
"""Get active session for a display."""
return self.active_sessions.get(display_id)
def reset_tracking(self):
"""Reset all tracking state."""
self.tracker.reset_tracking()
self.active_sessions.clear()
self.session_vehicles.clear()
self.cleared_sessions.clear()
logger.info("Tracking pipeline integration reset")
def get_statistics(self) -> Dict[str, Any]:
"""Get comprehensive statistics."""
tracker_stats = self.tracker.get_statistics()
validator_stats = self.validator.get_statistics()
return {
'integration': self.stats,
'tracker': tracker_stats,
'validator': validator_stats,
'active_sessions': len(self.active_sessions),
'cleared_sessions': len(self.cleared_sessions)
}
def cleanup(self):
"""Cleanup resources."""
self.executor.shutdown(wait=False)
self.reset_tracking()
logger.info("Tracking pipeline integration cleaned up")

352
core/tracking/tracker.py Normal file
View file

@ -0,0 +1,352 @@
"""
Vehicle Tracking Module - Continuous tracking with front_rear_detection model
Implements vehicle identification, persistence, and motion analysis.
"""
import logging
import time
import uuid
from typing import Dict, List, Optional, Tuple, Any
from dataclasses import dataclass, field
import numpy as np
from threading import Lock
logger = logging.getLogger(__name__)
@dataclass
class TrackedVehicle:
"""Represents a tracked vehicle with all its state information."""
track_id: int
first_seen: float
last_seen: float
session_id: Optional[str] = None
display_id: Optional[str] = None
confidence: float = 0.0
bbox: Tuple[int, int, int, int] = (0, 0, 0, 0) # x1, y1, x2, y2
center: Tuple[float, float] = (0.0, 0.0)
stable_frames: int = 0
total_frames: int = 0
is_stable: bool = False
processed_pipeline: bool = False
last_position_history: List[Tuple[float, float]] = field(default_factory=list)
avg_confidence: float = 0.0
def update_position(self, bbox: Tuple[int, int, int, int], confidence: float):
"""Update vehicle position and confidence."""
self.bbox = bbox
self.center = ((bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2)
self.last_seen = time.time()
self.confidence = confidence
self.total_frames += 1
# Update confidence average
self.avg_confidence = ((self.avg_confidence * (self.total_frames - 1)) + confidence) / self.total_frames
# Maintain position history (last 10 positions)
self.last_position_history.append(self.center)
if len(self.last_position_history) > 10:
self.last_position_history.pop(0)
def calculate_stability(self) -> float:
"""Calculate stability score based on position history."""
if len(self.last_position_history) < 2:
return 0.0
# Calculate movement variance
positions = np.array(self.last_position_history)
if len(positions) < 2:
return 0.0
# Calculate standard deviation of positions
std_x = np.std(positions[:, 0])
std_y = np.std(positions[:, 1])
# Lower variance means more stable (inverse relationship)
# Normalize to 0-1 range (assuming max reasonable std is 50 pixels)
stability = max(0, 1 - (std_x + std_y) / 100)
return stability
def is_expired(self, timeout_seconds: float = 2.0) -> bool:
"""Check if vehicle tracking has expired."""
return (time.time() - self.last_seen) > timeout_seconds
class VehicleTracker:
"""
Main vehicle tracking implementation using YOLO tracking capabilities.
Manages continuous tracking, vehicle identification, and state persistence.
"""
def __init__(self, tracking_config: Optional[Dict] = None):
"""
Initialize the vehicle tracker.
Args:
tracking_config: Configuration from pipeline.json tracking section
"""
self.config = tracking_config or {}
self.trigger_classes = self.config.get('triggerClasses', ['front_rear'])
self.min_confidence = self.config.get('minConfidence', 0.6)
# Tracking state
self.tracked_vehicles: Dict[int, TrackedVehicle] = {}
self.next_track_id = 1
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}, "
f"min_confidence={self.min_confidence}")
def process_detections(self,
results: Any,
display_id: str,
frame: np.ndarray) -> List[TrackedVehicle]:
"""
Process YOLO detection results and update tracking state.
Args:
results: YOLO detection results with tracking
display_id: Display identifier for this stream
frame: Current frame being processed
Returns:
List of currently tracked vehicles
"""
current_time = time.time()
active_tracks = []
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]
# Process new detections
if hasattr(results, 'boxes') and results.boxes is not None:
boxes = results.boxes
# Check if tracking is available
if hasattr(boxes, 'id') and boxes.id is not None:
# Process tracked objects
for i, box in enumerate(boxes):
# Get tracking ID
track_id = int(boxes.id[i].item()) if boxes.id[i] is not None else None
if track_id is None:
continue
# Get class and confidence
cls_id = int(box.cls.item())
confidence = float(box.conf.item())
# Check if class is in trigger classes
class_name = results.names[cls_id] if hasattr(results, 'names') else str(cls_id)
if class_name not in self.trigger_classes and confidence < self.min_confidence:
continue
# Get bounding box
x1, y1, x2, y2 = box.xyxy[0].cpu().numpy().astype(int)
bbox = (x1, y1, x2, y2)
# Update or create tracked vehicle
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
# 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={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}")
active_tracks.append(self.tracked_vehicles[track_id])
else:
# No tracking available, process as detections only
logger.debug("No tracking IDs available, processing as detections only")
for i, box in enumerate(boxes):
cls_id = int(box.cls.item())
confidence = float(box.conf.item())
# Check confidence threshold
if confidence < self.min_confidence:
continue
# Get bounding box
x1, y1, x2, y2 = box.xyxy[0].cpu().numpy().astype(int)
bbox = (x1, y1, x2, y2)
center = ((x1 + x2) / 2, (y1 + y2) / 2)
# Try to match with existing tracks by position
matched_track = self._find_closest_track(center)
if matched_track:
matched_track.update_position(bbox, confidence)
matched_track.display_id = display_id
active_tracks.append(matched_track)
else:
# Create new track with generated ID
track_id = self.next_track_id
self.next_track_id += 1
vehicle = TrackedVehicle(
track_id=track_id,
first_seen=current_time,
last_seen=current_time,
display_id=display_id,
confidence=confidence,
bbox=bbox,
center=center,
total_frames=1
)
vehicle.last_position_history.append(center)
self.tracked_vehicles[track_id] = vehicle
active_tracks.append(vehicle)
logger.info(f"New vehicle detected (no tracking): ID={track_id}")
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.
Args:
display_id: Optional display ID to filter by
Returns:
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)
]
return stable
def get_vehicle_by_session(self, session_id: str) -> Optional[TrackedVehicle]:
"""
Get a tracked vehicle by its session ID.
Args:
session_id: Session ID to look up
Returns:
Tracked vehicle if found, None otherwise
"""
with self.lock:
for vehicle in self.tracked_vehicles.values():
if vehicle.session_id == session_id:
return vehicle
return None
def mark_processed(self, track_id: int, session_id: str):
"""
Mark a vehicle as processed through the pipeline.
Args:
track_id: Track ID of the vehicle
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}")
def clear_session(self, session_id: str):
"""
Clear session ID from a tracked vehicle (post-fueling).
Args:
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
def reset_tracking(self):
"""Reset all tracking state."""
with self.lock:
self.tracked_vehicles.clear()
self.next_track_id = 1
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)
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
}

408
core/tracking/validator.py Normal file
View file

@ -0,0 +1,408 @@
"""
Vehicle Validation Module - Stable car detection and validation logic.
Differentiates between stable (fueling) cars and passing-by vehicles.
"""
import logging
import time
import numpy as np
from typing import List, Optional, Tuple, Dict, Any
from dataclasses import dataclass
from enum import Enum
from .tracker import TrackedVehicle
logger = logging.getLogger(__name__)
class VehicleState(Enum):
"""Vehicle state classification."""
UNKNOWN = "unknown"
ENTERING = "entering"
STABLE = "stable"
LEAVING = "leaving"
PASSING_BY = "passing_by"
@dataclass
class ValidationResult:
"""Result of vehicle validation."""
is_valid: bool
state: VehicleState
confidence: float
reason: str
should_process: bool = False
track_id: Optional[int] = None
class StableCarValidator:
"""
Validates whether a tracked vehicle is stable (fueling) or just passing by.
Uses multiple criteria including position stability, duration, and movement patterns.
"""
def __init__(self, config: Optional[Dict] = None):
"""
Initialize the validator with configuration.
Args:
config: Optional configuration dictionary
"""
self.config = config or {}
# Validation thresholds
self.min_stable_duration = self.config.get('min_stable_duration', 3.0) # seconds
self.min_stable_frames = self.config.get('min_stable_frames', 10)
self.position_variance_threshold = self.config.get('position_variance_threshold', 25.0) # pixels
self.min_confidence = self.config.get('min_confidence', 0.7)
self.velocity_threshold = self.config.get('velocity_threshold', 5.0) # pixels/frame
self.entering_zone_ratio = self.config.get('entering_zone_ratio', 0.3) # 30% of frame
self.leaving_zone_ratio = self.config.get('leaving_zone_ratio', 0.3)
# Frame dimensions (will be updated on first frame)
self.frame_width = 1920
self.frame_height = 1080
# History for validation
self.validation_history: Dict[int, List[VehicleState]] = {}
self.last_processed_vehicles: Dict[int, float] = {} # track_id -> last_process_time
logger.info(f"StableCarValidator initialized with min_duration={self.min_stable_duration}s, "
f"min_frames={self.min_stable_frames}, position_variance={self.position_variance_threshold}")
def update_frame_dimensions(self, width: int, height: int):
"""Update frame dimensions for zone calculations."""
self.frame_width = width
self.frame_height = height
logger.debug(f"Updated frame dimensions: {width}x{height}")
def validate_vehicle(self, vehicle: TrackedVehicle, frame_shape: Optional[Tuple] = None) -> ValidationResult:
"""
Validate whether a tracked vehicle is stable and should be processed.
Args:
vehicle: The tracked vehicle to validate
frame_shape: Optional frame shape (height, width, channels)
Returns:
ValidationResult with validation status and reasoning
"""
# Update frame dimensions if provided
if frame_shape:
self.update_frame_dimensions(frame_shape[1], frame_shape[0])
# Initialize validation history for new vehicles
if vehicle.track_id not in self.validation_history:
self.validation_history[vehicle.track_id] = []
# Check if already processed
if vehicle.processed_pipeline:
return ValidationResult(
is_valid=False,
state=VehicleState.STABLE,
confidence=1.0,
reason="Already processed through pipeline",
should_process=False,
track_id=vehicle.track_id
)
# Check if recently processed (cooldown period)
if vehicle.track_id in self.last_processed_vehicles:
time_since_process = time.time() - self.last_processed_vehicles[vehicle.track_id]
if time_since_process < 10.0: # 10 second cooldown
return ValidationResult(
is_valid=False,
state=VehicleState.STABLE,
confidence=1.0,
reason=f"Recently processed ({time_since_process:.1f}s ago)",
should_process=False,
track_id=vehicle.track_id
)
# Determine vehicle state
state = self._determine_vehicle_state(vehicle)
# Update history
self.validation_history[vehicle.track_id].append(state)
if len(self.validation_history[vehicle.track_id]) > 20:
self.validation_history[vehicle.track_id].pop(0)
# Validate based on state
if state == VehicleState.STABLE:
return self._validate_stable_vehicle(vehicle)
elif state == VehicleState.PASSING_BY:
return ValidationResult(
is_valid=False,
state=state,
confidence=0.8,
reason="Vehicle is passing by",
should_process=False,
track_id=vehicle.track_id
)
elif state == VehicleState.ENTERING:
return ValidationResult(
is_valid=False,
state=state,
confidence=0.5,
reason="Vehicle is entering, waiting for stability",
should_process=False,
track_id=vehicle.track_id
)
elif state == VehicleState.LEAVING:
return ValidationResult(
is_valid=False,
state=state,
confidence=0.5,
reason="Vehicle is leaving",
should_process=False,
track_id=vehicle.track_id
)
else:
return ValidationResult(
is_valid=False,
state=state,
confidence=0.0,
reason="Unknown vehicle state",
should_process=False,
track_id=vehicle.track_id
)
def _determine_vehicle_state(self, vehicle: TrackedVehicle) -> VehicleState:
"""
Determine the current state of the vehicle based on movement patterns.
Args:
vehicle: The tracked vehicle
Returns:
Current vehicle state
"""
# Not enough data
if len(vehicle.last_position_history) < 3:
return VehicleState.UNKNOWN
# Calculate velocity
velocity = self._calculate_velocity(vehicle)
# Get position zones
x_position = vehicle.center[0] / self.frame_width
y_position = vehicle.center[1] / self.frame_height
# Check if vehicle is stable
stability = vehicle.calculate_stability()
if stability > 0.7 and velocity < self.velocity_threshold:
# Check if it's been stable long enough
duration = time.time() - vehicle.first_seen
if duration > self.min_stable_duration and vehicle.stable_frames >= self.min_stable_frames:
return VehicleState.STABLE
else:
return VehicleState.ENTERING
# Check if vehicle is entering or leaving
if velocity > self.velocity_threshold:
# Determine direction based on position history
positions = np.array(vehicle.last_position_history)
if len(positions) >= 2:
direction = positions[-1] - positions[0]
# Entering: moving towards center
if x_position < self.entering_zone_ratio or x_position > (1 - self.entering_zone_ratio):
if abs(direction[0]) > abs(direction[1]): # Horizontal movement
if (x_position < 0.5 and direction[0] > 0) or (x_position > 0.5 and direction[0] < 0):
return VehicleState.ENTERING
# Leaving: moving away from center
if 0.3 < x_position < 0.7: # In center zone
if abs(direction[0]) > abs(direction[1]): # Horizontal movement
if abs(direction[0]) > 10: # Significant movement
return VehicleState.LEAVING
return VehicleState.PASSING_BY
return VehicleState.UNKNOWN
def _validate_stable_vehicle(self, vehicle: TrackedVehicle) -> ValidationResult:
"""
Perform detailed validation of a stable vehicle.
Args:
vehicle: The stable vehicle to validate
Returns:
Detailed validation result
"""
# Check duration
duration = time.time() - vehicle.first_seen
if duration < self.min_stable_duration:
return ValidationResult(
is_valid=False,
state=VehicleState.STABLE,
confidence=0.6,
reason=f"Not stable long enough ({duration:.1f}s < {self.min_stable_duration}s)",
should_process=False,
track_id=vehicle.track_id
)
# Check frame count
if vehicle.stable_frames < self.min_stable_frames:
return ValidationResult(
is_valid=False,
state=VehicleState.STABLE,
confidence=0.6,
reason=f"Not enough stable frames ({vehicle.stable_frames} < {self.min_stable_frames})",
should_process=False,
track_id=vehicle.track_id
)
# Check confidence
if vehicle.avg_confidence < self.min_confidence:
return ValidationResult(
is_valid=False,
state=VehicleState.STABLE,
confidence=vehicle.avg_confidence,
reason=f"Confidence too low ({vehicle.avg_confidence:.2f} < {self.min_confidence})",
should_process=False,
track_id=vehicle.track_id
)
# Check position variance
variance = self._calculate_position_variance(vehicle)
if variance > self.position_variance_threshold:
return ValidationResult(
is_valid=False,
state=VehicleState.STABLE,
confidence=0.7,
reason=f"Position variance too high ({variance:.1f} > {self.position_variance_threshold})",
should_process=False,
track_id=vehicle.track_id
)
# Check state history consistency
if vehicle.track_id in self.validation_history:
history = self.validation_history[vehicle.track_id][-5:] # Last 5 states
stable_count = sum(1 for s in history if s == VehicleState.STABLE)
if stable_count < 3:
return ValidationResult(
is_valid=False,
state=VehicleState.STABLE,
confidence=0.7,
reason="Inconsistent state history",
should_process=False,
track_id=vehicle.track_id
)
# All checks passed - vehicle is valid for processing
self.last_processed_vehicles[vehicle.track_id] = time.time()
return ValidationResult(
is_valid=True,
state=VehicleState.STABLE,
confidence=vehicle.avg_confidence,
reason="Vehicle is stable and ready for processing",
should_process=True,
track_id=vehicle.track_id
)
def _calculate_velocity(self, vehicle: TrackedVehicle) -> float:
"""
Calculate the velocity of the vehicle based on position history.
Args:
vehicle: The tracked vehicle
Returns:
Velocity in pixels per frame
"""
if len(vehicle.last_position_history) < 2:
return 0.0
positions = np.array(vehicle.last_position_history)
if len(positions) < 2:
return 0.0
# Calculate velocity over last 3 frames
recent_positions = positions[-min(3, len(positions)):]
velocities = []
for i in range(1, len(recent_positions)):
dx = recent_positions[i][0] - recent_positions[i-1][0]
dy = recent_positions[i][1] - recent_positions[i-1][1]
velocity = np.sqrt(dx**2 + dy**2)
velocities.append(velocity)
return np.mean(velocities) if velocities else 0.0
def _calculate_position_variance(self, vehicle: TrackedVehicle) -> float:
"""
Calculate the position variance of the vehicle.
Args:
vehicle: The tracked vehicle
Returns:
Position variance in pixels
"""
if len(vehicle.last_position_history) < 2:
return 0.0
positions = np.array(vehicle.last_position_history)
variance_x = np.var(positions[:, 0])
variance_y = np.var(positions[:, 1])
return np.sqrt(variance_x + variance_y)
def should_skip_same_car(self,
vehicle: TrackedVehicle,
session_cleared: bool = False) -> 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
Returns:
True if we should skip this vehicle
"""
# 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:
# Check if enough time has passed since processing
if vehicle.track_id in self.last_processed_vehicles:
time_since = time.time() - self.last_processed_vehicles[vehicle.track_id]
if time_since < 30.0: # 30 second cooldown after session clear
logger.debug(f"Skipping same car {vehicle.track_id} after session clear "
f"({time_since:.1f}s since processing)")
return True
return False
def reset_vehicle(self, track_id: int):
"""
Reset validation state for a specific vehicle.
Args:
track_id: Track ID of the vehicle to reset
"""
if track_id in self.validation_history:
del self.validation_history[track_id]
if track_id in self.last_processed_vehicles:
del self.last_processed_vehicles[track_id]
logger.debug(f"Reset validation state for vehicle {track_id}")
def get_statistics(self) -> Dict:
"""Get validation statistics."""
return {
'vehicles_in_history': len(self.validation_history),
'recently_processed': len(self.last_processed_vehicles),
'state_distribution': self._get_state_distribution()
}
def _get_state_distribution(self) -> Dict[str, int]:
"""Get distribution of current vehicle states."""
distribution = {state.value: 0 for state in VehicleState}
for history in self.validation_history.values():
if history:
current_state = history[-1]
distribution[current_state.value] += 1
return distribution