update tracker
This commit is contained in:
		
							parent
							
								
									a54da904f7
								
							
						
					
					
						commit
						3a4a27ca68
					
				
					 7 changed files with 1405 additions and 84 deletions
				
			
		
							
								
								
									
										2
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							| 
						 | 
					@ -13,3 +13,5 @@ no_frame_debug.log
 | 
				
			||||||
 | 
					
 | 
				
			||||||
feeder/
 | 
					feeder/
 | 
				
			||||||
.venv/
 | 
					.venv/
 | 
				
			||||||
 | 
					.vscode/
 | 
				
			||||||
 | 
					dist/
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										7
									
								
								app.py
									
										
									
									
									
								
							
							
						
						
									
										7
									
								
								app.py
									
										
									
									
									
								
							| 
						 | 
					@ -27,7 +27,7 @@ from websockets.exceptions import ConnectionClosedError
 | 
				
			||||||
from ultralytics import YOLO
 | 
					from ultralytics import YOLO
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Import shared pipeline functions
 | 
					# Import shared pipeline functions
 | 
				
			||||||
from siwatsystem.pympta import load_pipeline_from_zip, run_pipeline
 | 
					from siwatsystem.pympta import load_pipeline_from_zip, run_pipeline, cleanup_camera_stability
 | 
				
			||||||
 | 
					
 | 
				
			||||||
app = FastAPI()
 | 
					app = FastAPI()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -325,6 +325,7 @@ async def detect(websocket: WebSocket):
 | 
				
			||||||
                "type": "imageDetection",
 | 
					                "type": "imageDetection",
 | 
				
			||||||
                "subscriptionIdentifier": stream["subscriptionIdentifier"],
 | 
					                "subscriptionIdentifier": stream["subscriptionIdentifier"],
 | 
				
			||||||
                "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S.%fZ", time.gmtime()),
 | 
					                "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S.%fZ", time.gmtime()),
 | 
				
			||||||
 | 
					                # "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime()) + f".{int(time.time() * 1000) % 1000:03d}Z",
 | 
				
			||||||
                "data": {
 | 
					                "data": {
 | 
				
			||||||
                    "detection": detection_dict,
 | 
					                    "detection": detection_dict,
 | 
				
			||||||
                    "modelId": stream["modelId"],
 | 
					                    "modelId": stream["modelId"],
 | 
				
			||||||
| 
						 | 
					@ -704,6 +705,7 @@ async def detect(websocket: WebSocket):
 | 
				
			||||||
                    del camera_streams[camera_url]
 | 
					                    del camera_streams[camera_url]
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            latest_frames.pop(subscription_id, None)
 | 
					            latest_frames.pop(subscription_id, None)
 | 
				
			||||||
 | 
					            cleanup_camera_stability(subscription_id)
 | 
				
			||||||
            logger.info(f"Unsubscribed from camera {subscription_id}")
 | 
					            logger.info(f"Unsubscribed from camera {subscription_id}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async def process_streams():
 | 
					    async def process_streams():
 | 
				
			||||||
| 
						 | 
					@ -1018,8 +1020,9 @@ async def detect(websocket: WebSocket):
 | 
				
			||||||
                                else:
 | 
					                                else:
 | 
				
			||||||
                                    logger.info(f"Shared stream for {camera_url} still has {shared_stream['ref_count']} references")
 | 
					                                    logger.info(f"Shared stream for {camera_url} still has {shared_stream['ref_count']} references")
 | 
				
			||||||
                            
 | 
					                            
 | 
				
			||||||
                            # Clean up cached frame
 | 
					                            # Clean up cached frame and stability tracking
 | 
				
			||||||
                            latest_frames.pop(camera_id, None)
 | 
					                            latest_frames.pop(camera_id, None)
 | 
				
			||||||
 | 
					                            cleanup_camera_stability(camera_id)
 | 
				
			||||||
                            logger.info(f"Unsubscribed from camera {camera_id}")
 | 
					                            logger.info(f"Unsubscribed from camera {camera_id}")
 | 
				
			||||||
                            # Note: Keep models in memory for potential reuse
 | 
					                            # Note: Keep models in memory for potential reuse
 | 
				
			||||||
                elif msg_type == "requestState":
 | 
					                elif msg_type == "requestState":
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										142
									
								
								debug/test_camera_indices.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								debug/test_camera_indices.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,142 @@
 | 
				
			||||||
 | 
					#!/usr/bin/env python3
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Test script to check available camera indices
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import cv2
 | 
				
			||||||
 | 
					import logging
 | 
				
			||||||
 | 
					import sys
 | 
				
			||||||
 | 
					import subprocess
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Configure logging
 | 
				
			||||||
 | 
					logging.basicConfig(
 | 
				
			||||||
 | 
					    level=logging.INFO,
 | 
				
			||||||
 | 
					    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					logger = logging.getLogger("camera_index_test")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_camera_index(index):
 | 
				
			||||||
 | 
					    """Test if a camera index is available"""
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        cap = cv2.VideoCapture(index)
 | 
				
			||||||
 | 
					        if cap.isOpened():
 | 
				
			||||||
 | 
					            ret, frame = cap.read()
 | 
				
			||||||
 | 
					            if ret and frame is not None:
 | 
				
			||||||
 | 
					                width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
 | 
				
			||||||
 | 
					                height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
 | 
				
			||||||
 | 
					                fps = cap.get(cv2.CAP_PROP_FPS)
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                cap.release()
 | 
				
			||||||
 | 
					                return True, f"{width}x{height} @ {fps}fps"
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                cap.release()
 | 
				
			||||||
 | 
					                return False, "Can open but cannot read frames"
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            cap.release()
 | 
				
			||||||
 | 
					            return False, "Cannot open camera"
 | 
				
			||||||
 | 
					    except Exception as e:
 | 
				
			||||||
 | 
					        return False, f"Error: {str(e)}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_windows_cameras_ffmpeg():
 | 
				
			||||||
 | 
					    """Get available cameras on Windows using FFmpeg"""
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        result = subprocess.run(['ffmpeg', '-f', 'dshow', '-list_devices', 'true', '-i', 'dummy'], 
 | 
				
			||||||
 | 
					                              capture_output=True, text=True, timeout=10, encoding='utf-8', errors='ignore')
 | 
				
			||||||
 | 
					        output = result.stderr
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        lines = output.split('\n')
 | 
				
			||||||
 | 
					        video_devices = []
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Parse the output - look for lines with (video) that contain device names in quotes
 | 
				
			||||||
 | 
					        for line in lines:
 | 
				
			||||||
 | 
					            if '[dshow @' in line and '(video)' in line and '"' in line:
 | 
				
			||||||
 | 
					                # Extract device name between first pair of quotes
 | 
				
			||||||
 | 
					                start = line.find('"') + 1
 | 
				
			||||||
 | 
					                end = line.find('"', start)
 | 
				
			||||||
 | 
					                if start > 0 and end > start:
 | 
				
			||||||
 | 
					                    device_name = line[start:end]
 | 
				
			||||||
 | 
					                    video_devices.append(device_name)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        logger.info(f"FFmpeg detected video devices: {video_devices}")
 | 
				
			||||||
 | 
					        return video_devices
 | 
				
			||||||
 | 
					    except Exception as e:
 | 
				
			||||||
 | 
					        logger.error(f"Failed to get Windows camera names: {e}")
 | 
				
			||||||
 | 
					        return []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def main():
 | 
				
			||||||
 | 
					    logger.info("=== Camera Index Test ===")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Check FFmpeg availability for Windows device detection
 | 
				
			||||||
 | 
					    ffmpeg_available = False
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        result = subprocess.run(['ffmpeg', '-version'], capture_output=True, text=True, timeout=5)
 | 
				
			||||||
 | 
					        if result.returncode == 0:
 | 
				
			||||||
 | 
					            ffmpeg_available = True
 | 
				
			||||||
 | 
					            logger.info("FFmpeg is available")
 | 
				
			||||||
 | 
					    except:
 | 
				
			||||||
 | 
					        logger.info("FFmpeg not available")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Get Windows camera names if possible
 | 
				
			||||||
 | 
					    if sys.platform.startswith('win') and ffmpeg_available:
 | 
				
			||||||
 | 
					        logger.info("\n=== Windows Camera Devices (FFmpeg) ===")
 | 
				
			||||||
 | 
					        cameras = get_windows_cameras_ffmpeg()
 | 
				
			||||||
 | 
					        if cameras:
 | 
				
			||||||
 | 
					            for i, camera in enumerate(cameras):
 | 
				
			||||||
 | 
					                logger.info(f"Device {i}: {camera}")
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            logger.info("No cameras detected via FFmpeg")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Test camera indices 0-9
 | 
				
			||||||
 | 
					    logger.info("\n=== Testing Camera Indices ===")
 | 
				
			||||||
 | 
					    available_cameras = []
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    for index in range(10):
 | 
				
			||||||
 | 
					        logger.info(f"Testing camera index {index}...")
 | 
				
			||||||
 | 
					        is_available, info = test_camera_index(index)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if is_available:
 | 
				
			||||||
 | 
					            logger.info(f"✓ Camera {index}: AVAILABLE - {info}")
 | 
				
			||||||
 | 
					            available_cameras.append(index)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            logger.info(f"✗ Camera {index}: NOT AVAILABLE - {info}")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Summary
 | 
				
			||||||
 | 
					    logger.info("\n=== Summary ===")
 | 
				
			||||||
 | 
					    if available_cameras:
 | 
				
			||||||
 | 
					        logger.info(f"Available camera indices: {available_cameras}")
 | 
				
			||||||
 | 
					        logger.info(f"Default camera index to use: {available_cameras[0]}")
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Test the first available camera more thoroughly
 | 
				
			||||||
 | 
					        logger.info(f"\n=== Detailed Test for Camera {available_cameras[0]} ===")
 | 
				
			||||||
 | 
					        cap = cv2.VideoCapture(available_cameras[0])
 | 
				
			||||||
 | 
					        if cap.isOpened():
 | 
				
			||||||
 | 
					            # Get properties
 | 
				
			||||||
 | 
					            width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
 | 
				
			||||||
 | 
					            height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
 | 
				
			||||||
 | 
					            fps = cap.get(cv2.CAP_PROP_FPS)
 | 
				
			||||||
 | 
					            backend = cap.getBackendName()
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            logger.info(f"Resolution: {width}x{height}")
 | 
				
			||||||
 | 
					            logger.info(f"FPS: {fps}")
 | 
				
			||||||
 | 
					            logger.info(f"Backend: {backend}")
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            # Test frame capture
 | 
				
			||||||
 | 
					            ret, frame = cap.read()
 | 
				
			||||||
 | 
					            if ret and frame is not None:
 | 
				
			||||||
 | 
					                logger.info(f"Frame capture: SUCCESS")
 | 
				
			||||||
 | 
					                logger.info(f"Frame shape: {frame.shape}")
 | 
				
			||||||
 | 
					                logger.info(f"Frame dtype: {frame.dtype}")
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                logger.info(f"Frame capture: FAILED")
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            cap.release()
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        logger.error("No cameras available!")
 | 
				
			||||||
 | 
					        logger.info("Possible solutions:")
 | 
				
			||||||
 | 
					        logger.info("1. Check if camera is connected and not used by another application")
 | 
				
			||||||
 | 
					        logger.info("2. Check camera permissions")
 | 
				
			||||||
 | 
					        logger.info("3. Try different camera indices")
 | 
				
			||||||
 | 
					        logger.info("4. Install camera drivers")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if __name__ == "__main__":
 | 
				
			||||||
 | 
					    main()
 | 
				
			||||||
| 
						 | 
					@ -17,6 +17,13 @@ from .database import DatabaseManager
 | 
				
			||||||
# Create a logger specifically for this module
 | 
					# Create a logger specifically for this module
 | 
				
			||||||
logger = logging.getLogger("detector_worker.pympta")
 | 
					logger = logging.getLogger("detector_worker.pympta")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Global camera-aware stability tracking
 | 
				
			||||||
 | 
					# Structure: {camera_id: {model_id: {"track_stability_counters": {track_id: count}, "stable_tracks": set(), "session_state": {...}}}}
 | 
				
			||||||
 | 
					_camera_stability_tracking = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Timer-based cooldown configuration (for testing)
 | 
				
			||||||
 | 
					_cooldown_duration_seconds = 30
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def validate_redis_config(redis_config: dict) -> bool:
 | 
					def validate_redis_config(redis_config: dict) -> bool:
 | 
				
			||||||
    """Validate Redis configuration parameters."""
 | 
					    """Validate Redis configuration parameters."""
 | 
				
			||||||
    required_fields = ["host", "port"]
 | 
					    required_fields = ["host", "port"]
 | 
				
			||||||
| 
						 | 
					@ -78,7 +85,7 @@ def load_pipeline_node(node_config: dict, mpta_dir: str, redis_client, db_manage
 | 
				
			||||||
    logger.info(f"Loading model for node {node_config['modelId']} from {model_path}")
 | 
					    logger.info(f"Loading model for node {node_config['modelId']} from {model_path}")
 | 
				
			||||||
    model = YOLO(model_path)
 | 
					    model = YOLO(model_path)
 | 
				
			||||||
    if torch.cuda.is_available():
 | 
					    if torch.cuda.is_available():
 | 
				
			||||||
        logger.info(f"CUDA available. Moving model {node_config['modelId']} to GPU")
 | 
					        logger.info(f"CUDA available. Moving model {node_config['modelId']} to GPU VRAM")
 | 
				
			||||||
        model.to("cuda")
 | 
					        model.to("cuda")
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        logger.info(f"CUDA not available. Using CPU for model {node_config['modelId']}")
 | 
					        logger.info(f"CUDA not available. Using CPU for model {node_config['modelId']}")
 | 
				
			||||||
| 
						 | 
					@ -92,6 +99,10 @@ def load_pipeline_node(node_config: dict, mpta_dir: str, redis_client, db_manage
 | 
				
			||||||
                                if name in trigger_classes]
 | 
					                                if name in trigger_classes]
 | 
				
			||||||
        logger.debug(f"Converted trigger classes to indices: {trigger_class_indices}")
 | 
					        logger.debug(f"Converted trigger classes to indices: {trigger_class_indices}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Extract stability threshold from tracking config
 | 
				
			||||||
 | 
					    tracking_config = node_config.get("tracking", {"enabled": True, "reidConfigPath": "botsort.yaml"})
 | 
				
			||||||
 | 
					    stability_threshold = tracking_config.get("stabilityThreshold", 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    node = {
 | 
					    node = {
 | 
				
			||||||
        "modelId": node_config["modelId"],
 | 
					        "modelId": node_config["modelId"],
 | 
				
			||||||
        "modelFile": node_config["modelFile"],
 | 
					        "modelFile": node_config["modelFile"],
 | 
				
			||||||
| 
						 | 
					@ -105,6 +116,8 @@ def load_pipeline_node(node_config: dict, mpta_dir: str, redis_client, db_manage
 | 
				
			||||||
        "parallel": node_config.get("parallel", False),
 | 
					        "parallel": node_config.get("parallel", False),
 | 
				
			||||||
        "actions": node_config.get("actions", []),
 | 
					        "actions": node_config.get("actions", []),
 | 
				
			||||||
        "parallelActions": node_config.get("parallelActions", []),
 | 
					        "parallelActions": node_config.get("parallelActions", []),
 | 
				
			||||||
 | 
					        "tracking": tracking_config,
 | 
				
			||||||
 | 
					        "stabilityThreshold": stability_threshold,
 | 
				
			||||||
        "model": model,
 | 
					        "model": model,
 | 
				
			||||||
        "branches": [],
 | 
					        "branches": [],
 | 
				
			||||||
        "redis_client": redis_client,
 | 
					        "redis_client": redis_client,
 | 
				
			||||||
| 
						 | 
					@ -514,6 +527,342 @@ def resolve_field_mapping(value_template, branch_results, action_context):
 | 
				
			||||||
        logger.error(f"Error resolving field mapping '{value_template}': {e}")
 | 
					        logger.error(f"Error resolving field mapping '{value_template}': {e}")
 | 
				
			||||||
        return None
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def run_detection_with_tracking(frame, node, context=None):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Structured function for running YOLO detection with BoT-SORT tracking.
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    Args:
 | 
				
			||||||
 | 
					        frame: Input frame/image
 | 
				
			||||||
 | 
					        node: Pipeline node configuration with model and settings
 | 
				
			||||||
 | 
					        context: Optional context information (camera info, session data, etc.)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					    Returns:
 | 
				
			||||||
 | 
					        tuple: (all_detections, regions_dict) where:
 | 
				
			||||||
 | 
					            - all_detections: List of all detection objects
 | 
				
			||||||
 | 
					            - regions_dict: Dict mapping class names to highest confidence detections
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					    Configuration options in node:
 | 
				
			||||||
 | 
					        - model: YOLO model instance
 | 
				
			||||||
 | 
					        - triggerClassIndices: List of class indices to detect (None for all classes)
 | 
				
			||||||
 | 
					        - minConfidence: Minimum confidence threshold
 | 
				
			||||||
 | 
					        - multiClass: Whether to enable multi-class detection mode
 | 
				
			||||||
 | 
					        - expectedClasses: List of expected class names for multi-class validation
 | 
				
			||||||
 | 
					        - tracking: Dict with tracking configuration
 | 
				
			||||||
 | 
					            - enabled: Boolean to enable/disable tracking
 | 
				
			||||||
 | 
					            - reidConfigPath: Path to ReID config file (default: "botsort.yaml")
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        # Extract tracking configuration
 | 
				
			||||||
 | 
					        tracking_config = node.get("tracking", {})
 | 
				
			||||||
 | 
					        tracking_enabled = tracking_config.get("enabled", True)
 | 
				
			||||||
 | 
					        reid_config_path = tracking_config.get("reidConfigPath", "botsort.yaml")
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Check if we need to reset tracker after cooldown
 | 
				
			||||||
 | 
					        camera_id = context.get("camera_id", "unknown") if context else "unknown"
 | 
				
			||||||
 | 
					        model_id = node.get("modelId", "unknown")
 | 
				
			||||||
 | 
					        stability_data = get_camera_stability_data(camera_id, model_id)
 | 
				
			||||||
 | 
					        session_state = stability_data["session_state"]
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if session_state.get("reset_tracker_on_resume", False):
 | 
				
			||||||
 | 
					            # Reset YOLO tracker to get fresh track IDs
 | 
				
			||||||
 | 
					            if hasattr(node["model"], 'trackers') and node["model"].trackers:
 | 
				
			||||||
 | 
					                node["model"].trackers.clear()  # Clear tracker state
 | 
				
			||||||
 | 
					                logger.info(f"Camera {camera_id}: 🔄 Reset YOLO tracker - new cars will get fresh track IDs")
 | 
				
			||||||
 | 
					            session_state["reset_tracker_on_resume"] = False  # Clear the flag
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Get tracking zone from runtime context (camera-specific)
 | 
				
			||||||
 | 
					        tracking_zone = context.get("trackingZone", []) if context else []
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Prepare class filtering
 | 
				
			||||||
 | 
					        trigger_class_indices = node.get("triggerClassIndices")
 | 
				
			||||||
 | 
					        class_filter = {"classes": trigger_class_indices} if trigger_class_indices else {}
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        logger.debug(f"Running detection for {node['modelId']} - tracking: {tracking_enabled}, classes: {node.get('triggerClasses', 'all')}")
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if tracking_enabled and tracking_zone:
 | 
				
			||||||
 | 
					            # Use tracking with zone validation
 | 
				
			||||||
 | 
					            logger.debug(f"Using tracking with ReID config: {reid_config_path}")
 | 
				
			||||||
 | 
					            res = node["model"].track(
 | 
				
			||||||
 | 
					                frame,
 | 
				
			||||||
 | 
					                stream=False,
 | 
				
			||||||
 | 
					                persist=True,
 | 
				
			||||||
 | 
					                tracker=reid_config_path,
 | 
				
			||||||
 | 
					                **class_filter
 | 
				
			||||||
 | 
					            )[0]
 | 
				
			||||||
 | 
					        elif tracking_enabled:
 | 
				
			||||||
 | 
					            # Use tracking without zone restriction
 | 
				
			||||||
 | 
					            logger.debug("Using tracking without zone restriction")
 | 
				
			||||||
 | 
					            res = node["model"].track(
 | 
				
			||||||
 | 
					                frame,
 | 
				
			||||||
 | 
					                stream=False,
 | 
				
			||||||
 | 
					                persist=True,
 | 
				
			||||||
 | 
					                **class_filter
 | 
				
			||||||
 | 
					            )[0]
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            # Use detection only (no tracking)
 | 
				
			||||||
 | 
					            logger.debug("Using detection only (tracking disabled)")
 | 
				
			||||||
 | 
					            res = node["model"].predict(
 | 
				
			||||||
 | 
					                frame,
 | 
				
			||||||
 | 
					                stream=False,
 | 
				
			||||||
 | 
					                **class_filter
 | 
				
			||||||
 | 
					            )[0]
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Process detection results
 | 
				
			||||||
 | 
					        all_detections = []
 | 
				
			||||||
 | 
					        regions_dict = {}
 | 
				
			||||||
 | 
					        min_confidence = node.get("minConfidence", 0.0)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if res.boxes is None or len(res.boxes) == 0:
 | 
				
			||||||
 | 
					            logger.debug("No detections found")
 | 
				
			||||||
 | 
					            return [], {}
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        logger.debug(f"Processing {len(res.boxes)} raw detections")
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        for i, box in enumerate(res.boxes):
 | 
				
			||||||
 | 
					            # Extract detection data
 | 
				
			||||||
 | 
					            conf = float(box.cpu().conf[0])
 | 
				
			||||||
 | 
					            cls_id = int(box.cpu().cls[0])
 | 
				
			||||||
 | 
					            class_name = node["model"].names[cls_id]
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            # Apply confidence filtering
 | 
				
			||||||
 | 
					            if conf < min_confidence:
 | 
				
			||||||
 | 
					                logger.debug(f"Detection {i} '{class_name}' rejected: {conf:.3f} < {min_confidence}")
 | 
				
			||||||
 | 
					                continue
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            # Extract bounding box
 | 
				
			||||||
 | 
					            xy = box.cpu().xyxy[0]
 | 
				
			||||||
 | 
					            x1, y1, x2, y2 = map(int, xy)
 | 
				
			||||||
 | 
					            bbox = (x1, y1, x2, y2)
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            # Extract tracking ID if available
 | 
				
			||||||
 | 
					            track_id = None
 | 
				
			||||||
 | 
					            if hasattr(box, "id") and box.id is not None:
 | 
				
			||||||
 | 
					                track_id = int(box.id.item())
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            # Apply tracking zone validation if enabled
 | 
				
			||||||
 | 
					            if tracking_enabled and tracking_zone:
 | 
				
			||||||
 | 
					                bbox_center_x = (x1 + x2) // 2
 | 
				
			||||||
 | 
					                bbox_center_y = (y1 + y2) // 2
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                # Check if detection center is within tracking zone
 | 
				
			||||||
 | 
					                if not _point_in_polygon((bbox_center_x, bbox_center_y), tracking_zone):
 | 
				
			||||||
 | 
					                    logger.debug(f"Detection {i} '{class_name}' outside tracking zone")
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            # Create detection object
 | 
				
			||||||
 | 
					            detection = {
 | 
				
			||||||
 | 
					                "class": class_name,
 | 
				
			||||||
 | 
					                "confidence": conf,
 | 
				
			||||||
 | 
					                "id": track_id,
 | 
				
			||||||
 | 
					                "bbox": bbox,
 | 
				
			||||||
 | 
					                "class_id": cls_id
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            all_detections.append(detection)
 | 
				
			||||||
 | 
					            logger.debug(f"Detection {i} accepted: {class_name} (conf={conf:.3f}, id={track_id}, bbox={bbox})")
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            # Update regions_dict with highest confidence detection per class
 | 
				
			||||||
 | 
					            if class_name not in regions_dict or conf > regions_dict[class_name]["confidence"]:
 | 
				
			||||||
 | 
					                regions_dict[class_name] = {
 | 
				
			||||||
 | 
					                    "bbox": bbox,
 | 
				
			||||||
 | 
					                    "confidence": conf,
 | 
				
			||||||
 | 
					                    "detection": detection,
 | 
				
			||||||
 | 
					                    "track_id": track_id
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Multi-class validation
 | 
				
			||||||
 | 
					        if node.get("multiClass", False) and node.get("expectedClasses"):
 | 
				
			||||||
 | 
					            expected_classes = node["expectedClasses"]
 | 
				
			||||||
 | 
					            detected_classes = list(regions_dict.keys())
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            logger.debug(f"Multi-class validation: expected={expected_classes}, detected={detected_classes}")
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            # Check for required classes (flexible - at least one must match)
 | 
				
			||||||
 | 
					            matching_classes = [cls for cls in expected_classes if cls in detected_classes]
 | 
				
			||||||
 | 
					            if not matching_classes:
 | 
				
			||||||
 | 
					                logger.warning(f"Multi-class validation failed: no expected classes detected")
 | 
				
			||||||
 | 
					                return [], {}
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            logger.info(f"Multi-class validation passed: {matching_classes} detected")
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        logger.info(f"Detection completed: {len(all_detections)} detections, {len(regions_dict)} unique classes")
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Update stability tracking for detections with track IDs (requires camera_id from context)
 | 
				
			||||||
 | 
					        camera_id = context.get("camera_id", "unknown") if context else "unknown"
 | 
				
			||||||
 | 
					        update_track_stability(node, all_detections, camera_id)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return all_detections, regions_dict
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					    except Exception as e:
 | 
				
			||||||
 | 
					        logger.error(f"Error in detection_with_tracking for {node.get('modelId', 'unknown')}: {e}")
 | 
				
			||||||
 | 
					        logger.debug(f"Detection error traceback: {traceback.format_exc()}")
 | 
				
			||||||
 | 
					        return [], {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def _point_in_polygon(point, polygon):
 | 
				
			||||||
 | 
					    """Check if a point is inside a polygon using ray casting algorithm."""
 | 
				
			||||||
 | 
					    if not polygon or len(polygon) < 3:
 | 
				
			||||||
 | 
					        return True  # No zone restriction if invalid polygon
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    x, y = point
 | 
				
			||||||
 | 
					    n = len(polygon)
 | 
				
			||||||
 | 
					    inside = False
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    p1x, p1y = polygon[0]
 | 
				
			||||||
 | 
					    for i in range(1, n + 1):
 | 
				
			||||||
 | 
					        p2x, p2y = polygon[i % n]
 | 
				
			||||||
 | 
					        if y > min(p1y, p2y):
 | 
				
			||||||
 | 
					            if y <= max(p1y, p2y):
 | 
				
			||||||
 | 
					                if x <= max(p1x, p2x):
 | 
				
			||||||
 | 
					                    if p1y != p2y:
 | 
				
			||||||
 | 
					                        xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
 | 
				
			||||||
 | 
					                    if p1x == p2x or x <= xinters:
 | 
				
			||||||
 | 
					                        inside = not inside
 | 
				
			||||||
 | 
					        p1x, p1y = p2x, p2y
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return inside
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_camera_stability_data(camera_id, model_id):
 | 
				
			||||||
 | 
					    """Get or create stability tracking data for a specific camera and model."""
 | 
				
			||||||
 | 
					    global _camera_stability_tracking
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if camera_id not in _camera_stability_tracking:
 | 
				
			||||||
 | 
					        _camera_stability_tracking[camera_id] = {}
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if model_id not in _camera_stability_tracking[camera_id]:
 | 
				
			||||||
 | 
					        logger.warning(f"🔄 Camera {camera_id}: Creating NEW stability data for {model_id} - this will reset any cooldown!")
 | 
				
			||||||
 | 
					        _camera_stability_tracking[camera_id][model_id] = {
 | 
				
			||||||
 | 
					            "track_stability_counters": {},
 | 
				
			||||||
 | 
					            "stable_tracks": set(),
 | 
				
			||||||
 | 
					            "session_state": {
 | 
				
			||||||
 | 
					                "active": True,
 | 
				
			||||||
 | 
					                "cooldown_until": 0.0,
 | 
				
			||||||
 | 
					                "reset_tracker_on_resume": False
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return _camera_stability_tracking[camera_id][model_id]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def update_track_stability(node, detections, camera_id):
 | 
				
			||||||
 | 
					    """Update stability counters for tracked objects per camera."""
 | 
				
			||||||
 | 
					    stability_threshold = node.get("stabilityThreshold", 1)
 | 
				
			||||||
 | 
					    model_id = node.get("modelId", "unknown")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Get camera-specific stability data
 | 
				
			||||||
 | 
					    stability_data = get_camera_stability_data(camera_id, model_id)
 | 
				
			||||||
 | 
					    track_counters = stability_data["track_stability_counters"]
 | 
				
			||||||
 | 
					    stable_tracks = stability_data["stable_tracks"]
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Get current track IDs from detections
 | 
				
			||||||
 | 
					    current_track_ids = set()
 | 
				
			||||||
 | 
					    for detection in detections:
 | 
				
			||||||
 | 
					        track_id = detection.get("id")
 | 
				
			||||||
 | 
					        if track_id is not None:
 | 
				
			||||||
 | 
					            current_track_ids.add(track_id)
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            # Increment counter for this track
 | 
				
			||||||
 | 
					            track_counters[track_id] = track_counters.get(track_id, 0) + 1
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            # Check if track becomes stable
 | 
				
			||||||
 | 
					            if track_counters[track_id] >= stability_threshold and track_id not in stable_tracks:
 | 
				
			||||||
 | 
					                stable_tracks.add(track_id)
 | 
				
			||||||
 | 
					                logger.info(f"Camera {camera_id}: Track ID {track_id} became stable after {track_counters[track_id]} detections (threshold: {stability_threshold})")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Clean up counters for tracks that disappeared
 | 
				
			||||||
 | 
					    disappeared_tracks = set(track_counters.keys()) - current_track_ids
 | 
				
			||||||
 | 
					    for track_id in disappeared_tracks:
 | 
				
			||||||
 | 
					        logger.debug(f"Camera {camera_id}: Track ID {track_id} disappeared, removing from counters")
 | 
				
			||||||
 | 
					        track_counters.pop(track_id, None)
 | 
				
			||||||
 | 
					        stable_tracks.discard(track_id)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    logger.debug(f"Camera {camera_id}: Track stability: active={list(current_track_ids)}, stable={list(stable_tracks)}, counters={track_counters}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def check_stable_tracks(camera_id, model_id, regions_dict):
 | 
				
			||||||
 | 
					    """Check if any stable tracks match the detected classes for a specific camera."""
 | 
				
			||||||
 | 
					    # Get camera-specific stability data
 | 
				
			||||||
 | 
					    stability_data = get_camera_stability_data(camera_id, model_id)
 | 
				
			||||||
 | 
					    stable_tracks = stability_data["stable_tracks"]
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if not stable_tracks:
 | 
				
			||||||
 | 
					        return False, []
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Check if any detection in regions_dict has a stable track ID
 | 
				
			||||||
 | 
					    stable_detections = []
 | 
				
			||||||
 | 
					    for class_name, region_data in regions_dict.items():
 | 
				
			||||||
 | 
					        detection = region_data.get("detection", {})
 | 
				
			||||||
 | 
					        track_id = detection.get("id")
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if track_id is not None and track_id in stable_tracks:
 | 
				
			||||||
 | 
					            stable_detections.append((class_name, track_id))
 | 
				
			||||||
 | 
					            logger.debug(f"Camera {camera_id}: Found stable detection: {class_name} with stable track ID {track_id}")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    has_stable_tracks = len(stable_detections) > 0
 | 
				
			||||||
 | 
					    return has_stable_tracks, stable_detections
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def start_cooldown_timer(camera_id, model_id):
 | 
				
			||||||
 | 
					    """Start 30-second cooldown timer after successful pipeline completion."""
 | 
				
			||||||
 | 
					    stability_data = get_camera_stability_data(camera_id, model_id)
 | 
				
			||||||
 | 
					    session_state = stability_data["session_state"]
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Start timer-based cooldown
 | 
				
			||||||
 | 
					    cooldown_until = time.time() + _cooldown_duration_seconds
 | 
				
			||||||
 | 
					    session_state["cooldown_until"] = cooldown_until
 | 
				
			||||||
 | 
					    session_state["active"] = False
 | 
				
			||||||
 | 
					    session_state["reset_tracker_on_resume"] = True  # Flag to reset YOLO tracker
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    logger.info(f"Camera {camera_id}: 🛑 Starting {_cooldown_duration_seconds}s cooldown timer (until: {cooldown_until:.2f})")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # DO NOT clear tracking state here - preserve it during cooldown
 | 
				
			||||||
 | 
					    # Tracking state will be cleared when cooldown expires and new session starts
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def is_camera_active(camera_id, model_id):
 | 
				
			||||||
 | 
					    """Check if camera should be processing detections (timer-based cooldown)."""
 | 
				
			||||||
 | 
					    stability_data = get_camera_stability_data(camera_id, model_id)
 | 
				
			||||||
 | 
					    session_state = stability_data["session_state"]
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Check if cooldown timer has expired
 | 
				
			||||||
 | 
					    if not session_state["active"]:
 | 
				
			||||||
 | 
					        current_time = time.time()
 | 
				
			||||||
 | 
					        cooldown_until = session_state["cooldown_until"]
 | 
				
			||||||
 | 
					        remaining_time = cooldown_until - current_time
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if current_time >= cooldown_until:
 | 
				
			||||||
 | 
					            session_state["active"] = True
 | 
				
			||||||
 | 
					            session_state["reset_tracker_on_resume"] = True  # Ensure tracker reset flag is set
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            # Clear tracking state NOW - before new detection session starts
 | 
				
			||||||
 | 
					            stability_data["track_stability_counters"].clear()
 | 
				
			||||||
 | 
					            stability_data["stable_tracks"].clear()
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            logger.info(f"Camera {camera_id}: 📢 Cooldown timer ended, resuming detection with fresh track IDs")
 | 
				
			||||||
 | 
					            logger.info(f"Camera {camera_id}: 🧹 Cleared stability counters and stable tracks for fresh session")
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            logger.debug(f"Camera {camera_id}: Still in cooldown - {remaining_time:.1f}s remaining")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return session_state["active"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def cleanup_camera_stability(camera_id):
 | 
				
			||||||
 | 
					    """Clean up stability tracking data when a camera is disconnected, preserving cooldown timers."""
 | 
				
			||||||
 | 
					    global _camera_stability_tracking
 | 
				
			||||||
 | 
					    if camera_id in _camera_stability_tracking:
 | 
				
			||||||
 | 
					        # Check if any models are still in cooldown before cleanup
 | 
				
			||||||
 | 
					        models_in_cooldown = []
 | 
				
			||||||
 | 
					        for model_id, model_data in _camera_stability_tracking[camera_id].items():
 | 
				
			||||||
 | 
					            session_state = model_data.get("session_state", {})
 | 
				
			||||||
 | 
					            if not session_state.get("active", True) and time.time() < session_state.get("cooldown_until", 0):
 | 
				
			||||||
 | 
					                cooldown_remaining = session_state["cooldown_until"] - time.time()
 | 
				
			||||||
 | 
					                models_in_cooldown.append((model_id, cooldown_remaining))
 | 
				
			||||||
 | 
					                logger.warning(f"⚠️ Camera {camera_id}: Model {model_id} is in cooldown ({cooldown_remaining:.1f}s remaining) - preserving timer!")
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if models_in_cooldown:
 | 
				
			||||||
 | 
					            # DO NOT clear any tracking data during cooldown - preserve everything
 | 
				
			||||||
 | 
					            logger.warning(f"⚠️ Camera {camera_id}: PRESERVING ALL data during cooldown - no cleanup performed!")
 | 
				
			||||||
 | 
					            logger.warning(f"   - Track IDs will reset only AFTER cooldown expires")
 | 
				
			||||||
 | 
					            logger.warning(f"   - Stability counters preserved until cooldown ends")
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            # Safe to delete everything - no active cooldowns
 | 
				
			||||||
 | 
					            del _camera_stability_tracking[camera_id]
 | 
				
			||||||
 | 
					            logger.info(f"Cleaned up stability tracking data for camera {camera_id} (no active cooldowns)")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def validate_pipeline_execution(node, regions_dict):
 | 
					def validate_pipeline_execution(node, regions_dict):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Pre-validate that all required branches will execute successfully before 
 | 
					    Pre-validate that all required branches will execute successfully before 
 | 
				
			||||||
| 
						 | 
					@ -618,92 +967,42 @@ def run_pipeline(frame, node: dict, return_bbox: bool=False, context=None):
 | 
				
			||||||
            execute_actions(node, frame, det)
 | 
					            execute_actions(node, frame, det)
 | 
				
			||||||
            return (det, None) if return_bbox else det
 | 
					            return (det, None) if return_bbox else det
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # ─── Detection stage - Multi-class support ──────────────────
 | 
					        # ─── Session management check ───────────────────────────────────────
 | 
				
			||||||
        tk = node["triggerClassIndices"]
 | 
					        camera_id = context.get("camera_id", "unknown") if context else "unknown"
 | 
				
			||||||
        logger.debug(f"Running detection for node {node['modelId']} with trigger classes: {node.get('triggerClasses', [])} (indices: {tk})")
 | 
					        model_id = node.get("modelId", "unknown")
 | 
				
			||||||
        logger.debug(f"Node configuration: minConfidence={node['minConfidence']}, multiClass={node.get('multiClass', False)}")
 | 
					 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        res = node["model"].track(
 | 
					        if not is_camera_active(camera_id, model_id):
 | 
				
			||||||
            frame,
 | 
					            logger.info(f"⏰ Camera {camera_id}: Tracker stopped - in cooldown period, skipping all detection")
 | 
				
			||||||
            stream=False,
 | 
					 | 
				
			||||||
            persist=True,
 | 
					 | 
				
			||||||
            **({"classes": tk} if tk else {})
 | 
					 | 
				
			||||||
        )[0]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Collect all detections above confidence threshold
 | 
					 | 
				
			||||||
        all_detections = []
 | 
					 | 
				
			||||||
        all_boxes = []
 | 
					 | 
				
			||||||
        regions_dict = {}
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        logger.debug(f"Raw detection results from model: {len(res.boxes) if res.boxes is not None else 0} detections")
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        for i, box in enumerate(res.boxes):
 | 
					 | 
				
			||||||
            conf = float(box.cpu().conf[0])
 | 
					 | 
				
			||||||
            cid = int(box.cpu().cls[0])
 | 
					 | 
				
			||||||
            name = node["model"].names[cid]
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            logger.debug(f"Detection {i}: class='{name}' (id={cid}), confidence={conf:.3f}, threshold={node['minConfidence']}")
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            if conf < node["minConfidence"]:
 | 
					 | 
				
			||||||
                logger.debug(f"  -> REJECTED: confidence {conf:.3f} < threshold {node['minConfidence']}")
 | 
					 | 
				
			||||||
                continue
 | 
					 | 
				
			||||||
                
 | 
					 | 
				
			||||||
            xy = box.cpu().xyxy[0]
 | 
					 | 
				
			||||||
            x1, y1, x2, y2 = map(int, xy)
 | 
					 | 
				
			||||||
            bbox = (x1, y1, x2, y2)
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            detection = {
 | 
					 | 
				
			||||||
                "class": name,
 | 
					 | 
				
			||||||
                "confidence": conf,
 | 
					 | 
				
			||||||
                "id": box.id.item() if hasattr(box, "id") else None,
 | 
					 | 
				
			||||||
                "bbox": bbox
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            all_detections.append(detection)
 | 
					 | 
				
			||||||
            all_boxes.append(bbox)
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            logger.debug(f"  -> ACCEPTED: {name} with confidence {conf:.3f}, bbox={bbox}")
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            # Store highest confidence detection for each class
 | 
					 | 
				
			||||||
            if name not in regions_dict or conf > regions_dict[name]["confidence"]:
 | 
					 | 
				
			||||||
                regions_dict[name] = {
 | 
					 | 
				
			||||||
                    "bbox": bbox,
 | 
					 | 
				
			||||||
                    "confidence": conf,
 | 
					 | 
				
			||||||
                    "detection": detection
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                logger.debug(f"  -> Updated regions_dict['{name}'] with confidence {conf:.3f}")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        logger.info(f"Detection summary: {len(all_detections)} accepted detections from {len(res.boxes) if res.boxes is not None else 0} total")
 | 
					 | 
				
			||||||
        logger.info(f"Detected classes: {list(regions_dict.keys())}")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if not all_detections:
 | 
					 | 
				
			||||||
            logger.warning("No detections above confidence threshold - returning null")
 | 
					 | 
				
			||||||
            return (None, None) if return_bbox else None
 | 
					            return (None, None) if return_bbox else None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # ─── Multi-class validation ─────────────────────────────────
 | 
					        # ─── Detection stage - Using structured detection function ──────────────────
 | 
				
			||||||
        if node.get("multiClass", False) and node.get("expectedClasses"):
 | 
					        all_detections, regions_dict = run_detection_with_tracking(frame, node, context)
 | 
				
			||||||
            expected_classes = node["expectedClasses"]
 | 
					 | 
				
			||||||
            detected_classes = list(regions_dict.keys())
 | 
					 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
            logger.info(f"Multi-class validation: expected={expected_classes}, detected={detected_classes}")
 | 
					        if not all_detections:
 | 
				
			||||||
 | 
					            logger.warning("No detections from structured detection function - returning null")
 | 
				
			||||||
 | 
					            return (None, None) if return_bbox else None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # Check if at least one expected class is detected (flexible mode)
 | 
					        # Extract bounding boxes for compatibility
 | 
				
			||||||
            matching_classes = [cls for cls in expected_classes if cls in detected_classes]
 | 
					        all_boxes = [det["bbox"] for det in all_detections]
 | 
				
			||||||
            missing_classes = [cls for cls in expected_classes if cls not in detected_classes]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            logger.debug(f"Matching classes: {matching_classes}, Missing classes: {missing_classes}")
 | 
					        # ─── Stability validation (only for root pipeline node) ────────────────────────
 | 
				
			||||||
 | 
					        stability_threshold = node.get("stabilityThreshold", 1)
 | 
				
			||||||
 | 
					        if stability_threshold > 1:
 | 
				
			||||||
 | 
					            # Extract camera_id for stability check
 | 
				
			||||||
 | 
					            camera_id = context.get("camera_id", "unknown") if context else "unknown"
 | 
				
			||||||
 | 
					            model_id = node.get("modelId", "unknown")
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            if not matching_classes:
 | 
					            # Check if we have stable tracks for this specific camera
 | 
				
			||||||
                # No expected classes found at all
 | 
					            has_stable_tracks, stable_detections = check_stable_tracks(camera_id, model_id, regions_dict)
 | 
				
			||||||
                logger.warning(f"PIPELINE REJECTED: No expected classes detected. Expected: {expected_classes}, Detected: {detected_classes}")
 | 
					 | 
				
			||||||
                return (None, None) if return_bbox else None
 | 
					 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            if missing_classes:
 | 
					            if not has_stable_tracks:
 | 
				
			||||||
                logger.info(f"Partial multi-class detection: {matching_classes} found, {missing_classes} missing")
 | 
					                logger.info(f"Camera {camera_id}: Track not stable yet (threshold: {stability_threshold}) - validation only, skipping branches")
 | 
				
			||||||
 | 
					                # Return early with just the detection result, no branch processing
 | 
				
			||||||
 | 
					                primary_detection = max(all_detections, key=lambda x: x["confidence"]) if all_detections else {"class": "none", "confidence": 0.0, "bbox": [0, 0, 0, 0]}
 | 
				
			||||||
 | 
					                primary_bbox = primary_detection.get("bbox", [0, 0, 0, 0])
 | 
				
			||||||
 | 
					                return (primary_detection, primary_bbox) if return_bbox else primary_detection
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                logger.info(f"Complete multi-class detection success: {detected_classes}")
 | 
					                logger.info(f"Camera {camera_id}: Stable tracks {[det[1] for det in stable_detections]} detected - proceeding with full pipeline")
 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            logger.debug("No multi-class validation - proceeding with all detections")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # ─── Pre-validate pipeline execution ────────────────────────
 | 
					        # ─── Pre-validate pipeline execution ────────────────────────
 | 
				
			||||||
        pipeline_valid, missing_branches = validate_pipeline_execution(node, regions_dict)
 | 
					        pipeline_valid, missing_branches = validate_pipeline_execution(node, regions_dict)
 | 
				
			||||||
| 
						 | 
					@ -752,10 +1051,14 @@ def run_pipeline(frame, node: dict, return_bbox: bool=False, context=None):
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        execute_actions(node, frame, detection_result, regions_dict)
 | 
					        execute_actions(node, frame, detection_result, regions_dict)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # ─── Parallel branch processing ─────────────────────────────
 | 
					        # ─── Branch processing (no stability check here) ─────────────────────────────
 | 
				
			||||||
        if node["branches"]:
 | 
					        if node["branches"]:
 | 
				
			||||||
            branch_results = {}
 | 
					            branch_results = {}
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
 | 
					            # Extract camera_id for logging
 | 
				
			||||||
 | 
					            camera_id = detection_result.get("camera_id", context.get("camera_id", "unknown") if context else "unknown")
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
            # Filter branches that should be triggered
 | 
					            # Filter branches that should be triggered
 | 
				
			||||||
            active_branches = []
 | 
					            active_branches = []
 | 
				
			||||||
            for br in node["branches"]:
 | 
					            for br in node["branches"]:
 | 
				
			||||||
| 
						 | 
					@ -849,6 +1152,10 @@ def run_pipeline(frame, node: dict, return_bbox: bool=False, context=None):
 | 
				
			||||||
        if node.get("parallelActions") and "branch_results" in detection_result:
 | 
					        if node.get("parallelActions") and "branch_results" in detection_result:
 | 
				
			||||||
            execute_parallel_actions(node, frame, detection_result, regions_dict)
 | 
					            execute_parallel_actions(node, frame, detection_result, regions_dict)
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
 | 
					            # ─── Start 30s cooldown timer after successful pipeline completion ─────────────────
 | 
				
			||||||
 | 
					            start_cooldown_timer(camera_id, model_id)
 | 
				
			||||||
 | 
					            logger.info(f"Camera {camera_id}: Pipeline completed successfully, starting 30s cooldown")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # ─── Return detection result ────────────────────────────────
 | 
					        # ─── Return detection result ────────────────────────────────
 | 
				
			||||||
        primary_detection = max(all_detections, key=lambda x: x["confidence"])
 | 
					        primary_detection = max(all_detections, key=lambda x: x["confidence"])
 | 
				
			||||||
        primary_bbox = primary_detection["bbox"]
 | 
					        primary_bbox = primary_detection["bbox"]
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										352
									
								
								test_botsort_zone_track.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										352
									
								
								test_botsort_zone_track.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,352 @@
 | 
				
			||||||
 | 
					import cv2
 | 
				
			||||||
 | 
					import torch
 | 
				
			||||||
 | 
					import numpy as np
 | 
				
			||||||
 | 
					import time
 | 
				
			||||||
 | 
					from collections import defaultdict
 | 
				
			||||||
 | 
					from ultralytics import YOLO
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def point_in_polygon(point, polygon):
 | 
				
			||||||
 | 
					    """Check if a point is inside a polygon using ray casting algorithm"""
 | 
				
			||||||
 | 
					    x, y = point
 | 
				
			||||||
 | 
					    n = len(polygon)
 | 
				
			||||||
 | 
					    inside = False
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    p1x, p1y = polygon[0]
 | 
				
			||||||
 | 
					    for i in range(1, n + 1):
 | 
				
			||||||
 | 
					        p2x, p2y = polygon[i % n]
 | 
				
			||||||
 | 
					        if y > min(p1y, p2y):
 | 
				
			||||||
 | 
					            if y <= max(p1y, p2y):
 | 
				
			||||||
 | 
					                if x <= max(p1x, p2x):
 | 
				
			||||||
 | 
					                    if p1y != p2y:
 | 
				
			||||||
 | 
					                        xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
 | 
				
			||||||
 | 
					                    if p1x == p2x or x <= xinters:
 | 
				
			||||||
 | 
					                        inside = not inside
 | 
				
			||||||
 | 
					        p1x, p1y = p2x, p2y
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return inside
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def draw_zone(frame, zone_polygon, color=(255, 0, 0), thickness=3):
 | 
				
			||||||
 | 
					    """Draw tracking zone on frame"""
 | 
				
			||||||
 | 
					    pts = np.array(zone_polygon, np.int32)
 | 
				
			||||||
 | 
					    pts = pts.reshape((-1, 1, 2))
 | 
				
			||||||
 | 
					    cv2.polylines(frame, [pts], True, color, thickness)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Add semi-transparent fill
 | 
				
			||||||
 | 
					    overlay = frame.copy()
 | 
				
			||||||
 | 
					    cv2.fillPoly(overlay, [pts], color)
 | 
				
			||||||
 | 
					    cv2.addWeighted(overlay, 0.2, frame, 0.8, 0, frame)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def setup_video_writer(output_path, fps, width, height):
 | 
				
			||||||
 | 
					    """Setup video writer for output"""
 | 
				
			||||||
 | 
					    fourcc = cv2.VideoWriter_fourcc(*'mp4v')
 | 
				
			||||||
 | 
					    return cv2.VideoWriter(output_path, fourcc, fps, (width, height))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def write_frame_to_video(video_writer, frame, repeat_count):
 | 
				
			||||||
 | 
					    """Write frame to video with specified repeat count"""
 | 
				
			||||||
 | 
					    for _ in range(repeat_count):
 | 
				
			||||||
 | 
					        video_writer.write(frame)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def finalize_video(video_writer):
 | 
				
			||||||
 | 
					    """Release video writer"""
 | 
				
			||||||
 | 
					    video_writer.release()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def main():
 | 
				
			||||||
 | 
					    video_path = "sample2.mp4"
 | 
				
			||||||
 | 
					    yolo_model = "bangchakv2/yolov8n.pt"
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
 | 
				
			||||||
 | 
					    print(f"Using device: {device}")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    print("Loading YOLO model...")
 | 
				
			||||||
 | 
					    model = YOLO(yolo_model)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    print("Opening video...")
 | 
				
			||||||
 | 
					    cap = cv2.VideoCapture(video_path)
 | 
				
			||||||
 | 
					    fps = int(cap.get(cv2.CAP_PROP_FPS))
 | 
				
			||||||
 | 
					    width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
 | 
				
			||||||
 | 
					    height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
 | 
				
			||||||
 | 
					    total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    print(f"Video info: {width}x{height}, {fps} FPS, {total_frames} frames")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Define tracking zone - Gas station floor area (trapezoidal shape)
 | 
				
			||||||
 | 
					    # Based on the perspective of the gas station floor from your image
 | 
				
			||||||
 | 
					    # width 2560, height 1440
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    tracking_zone = [
 | 
				
			||||||
 | 
					        (423, 974),   # Point 1
 | 
				
			||||||
 | 
					        (1540, 1407), # Point 2
 | 
				
			||||||
 | 
					        (1976, 806),  # Point 3
 | 
				
			||||||
 | 
					        (1364, 749)   # Point 4
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    print(f"🎯 Tracking zone defined: {tracking_zone}")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # CONTINUOUS TRACKING: Process every 118 frames (~2.0s intervals)
 | 
				
			||||||
 | 
					    frame_skip = 118
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    print(f"🎯 CONTINUOUS MODE: Processing every {frame_skip} frames ({frame_skip/fps:.2f}s intervals)")
 | 
				
			||||||
 | 
					    print(f"🎬 Output video will have same duration as input (each processed frame shown for 2 seconds)")
 | 
				
			||||||
 | 
					    print("🔥 ZONE-FIRST TRACKING: Only cars entering the zone will be tracked!")
 | 
				
			||||||
 | 
					    print("Requires 5 consecutive detections IN ZONE for verification")
 | 
				
			||||||
 | 
					    print("🕐 24/7 MODE: Memory reset every hour to prevent overflow")
 | 
				
			||||||
 | 
					    print("Press 'q' to quit")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Setup video writer for output (same fps as input for normal playback speed)
 | 
				
			||||||
 | 
					    output_path = "tracking_output_botsort_zone_track.mp4"
 | 
				
			||||||
 | 
					    output_fps = fps  # Use same fps as input video
 | 
				
			||||||
 | 
					    out = setup_video_writer(output_path, output_fps, width, height)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Track car IDs and their consecutive detections
 | 
				
			||||||
 | 
					    car_id_counts = defaultdict(int)
 | 
				
			||||||
 | 
					    successful_cars = set()
 | 
				
			||||||
 | 
					    last_positions = {}
 | 
				
			||||||
 | 
					    processed_count = 0
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # ID remapping for clean sequential zone IDs
 | 
				
			||||||
 | 
					    tracker_to_zone_id = {}  # Maps tracker IDs to clean zone IDs
 | 
				
			||||||
 | 
					    next_zone_id = 1  # Next clean zone ID to assign
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Store previous frame detections to filter tracking inputs
 | 
				
			||||||
 | 
					    previous_zone_cars = set()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # 24/7 operation: Reset every hour (1800 snapshots at 2-sec intervals = 1 hour)
 | 
				
			||||||
 | 
					    RESET_INTERVAL = 1800  # Reset every 1800 processed frames (1 hour)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    frame_idx = 0
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    while True:
 | 
				
			||||||
 | 
					        # Skip frames to maintain interval
 | 
				
			||||||
 | 
					        for _ in range(frame_skip):
 | 
				
			||||||
 | 
					            ret, frame = cap.read()
 | 
				
			||||||
 | 
					            if not ret:
 | 
				
			||||||
 | 
					                print("\nNo more frames to read")
 | 
				
			||||||
 | 
					                cap.release()
 | 
				
			||||||
 | 
					                cv2.destroyAllWindows()
 | 
				
			||||||
 | 
					                return
 | 
				
			||||||
 | 
					            frame_idx += 1
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        processed_count += 1
 | 
				
			||||||
 | 
					        current_time = frame_idx / fps
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        print(f"\n🎬 Frame {frame_idx} at {current_time:.2f}s (processed #{processed_count})")
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # 24/7 Memory Management: Reset every hour
 | 
				
			||||||
 | 
					        if processed_count % RESET_INTERVAL == 0:
 | 
				
			||||||
 | 
					            print(f"🕐 HOURLY RESET: Clearing all tracking data (processed {processed_count} frames)")
 | 
				
			||||||
 | 
					            print(f"   📊 Before reset: {len(tracker_to_zone_id)} tracked cars, next Zone ID was {next_zone_id}")
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            # Clear all tracking data
 | 
				
			||||||
 | 
					            tracker_to_zone_id.clear()
 | 
				
			||||||
 | 
					            car_id_counts.clear()
 | 
				
			||||||
 | 
					            successful_cars.clear()
 | 
				
			||||||
 | 
					            last_positions.clear()
 | 
				
			||||||
 | 
					            next_zone_id = 1  # Reset to 1
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            # Reset BoT-SORT tracker state
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                model.reset()
 | 
				
			||||||
 | 
					                print(f"   ✅ BoT-SORT tracker reset successfully")
 | 
				
			||||||
 | 
					            except:
 | 
				
			||||||
 | 
					                print(f"   ⚠️ BoT-SORT reset not available (continuing without reset)")
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            print(f"   🆕 Zone IDs will start from 1 again")
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Draw tracking zone on frame
 | 
				
			||||||
 | 
					        draw_zone(frame, tracking_zone, color=(0, 255, 255), thickness=3)  # Yellow zone
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # First run YOLO detection (without tracking) to find cars in zone
 | 
				
			||||||
 | 
					        detection_results = model(frame, verbose=False, conf=0.7, classes=[2])
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Find cars currently in the tracking zone
 | 
				
			||||||
 | 
					        current_zone_cars = []
 | 
				
			||||||
 | 
					        total_detections = 0
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if detection_results[0].boxes is not None:
 | 
				
			||||||
 | 
					            boxes = detection_results[0].boxes.xyxy.cpu()
 | 
				
			||||||
 | 
					            scores = detection_results[0].boxes.conf.cpu()
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            total_detections = len(boxes)
 | 
				
			||||||
 | 
					            print(f"  🔍 Total car detections: {total_detections}")
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            for i in range(len(boxes)):
 | 
				
			||||||
 | 
					                x1, y1, x2, y2 = boxes[i]
 | 
				
			||||||
 | 
					                conf = float(scores[i])
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                # Check if detection is in zone (using bottom center)
 | 
				
			||||||
 | 
					                box_bottom = ((x1 + x2) / 2, y2)
 | 
				
			||||||
 | 
					                if point_in_polygon(box_bottom, tracking_zone):
 | 
				
			||||||
 | 
					                    current_zone_cars.append({
 | 
				
			||||||
 | 
					                        'bbox': [float(x1), float(y1), float(x2), float(y2)],
 | 
				
			||||||
 | 
					                        'conf': conf,
 | 
				
			||||||
 | 
					                        'center': ((x1 + x2) / 2, (y1 + y2) / 2),
 | 
				
			||||||
 | 
					                        'bottom': box_bottom
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        print(f"  🎯 Cars in zone: {len(current_zone_cars)}")
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Only run tracking if there are cars in the zone
 | 
				
			||||||
 | 
					        detected_car_ids = set()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if current_zone_cars:
 | 
				
			||||||
 | 
					            # Run tracking on the full frame (let tracker handle associations)
 | 
				
			||||||
 | 
					            # But we'll filter results to only zone cars afterward
 | 
				
			||||||
 | 
					            results = model.track(
 | 
				
			||||||
 | 
					                frame, 
 | 
				
			||||||
 | 
					                persist=True,
 | 
				
			||||||
 | 
					                verbose=False,
 | 
				
			||||||
 | 
					                conf=0.7,
 | 
				
			||||||
 | 
					                classes=[2],
 | 
				
			||||||
 | 
					                tracker="botsort_reid.yaml"
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if results[0].boxes is not None and results[0].boxes.id is not None:
 | 
				
			||||||
 | 
					                boxes = results[0].boxes.xyxy.cpu()
 | 
				
			||||||
 | 
					                scores = results[0].boxes.conf.cpu()
 | 
				
			||||||
 | 
					                track_ids = results[0].boxes.id.cpu().int()
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                print(f"  📊 Total tracked objects: {len(track_ids)}")
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                # Filter tracked objects to only those in zone
 | 
				
			||||||
 | 
					                zone_tracks = []
 | 
				
			||||||
 | 
					                for i, track_id in enumerate(track_ids):
 | 
				
			||||||
 | 
					                    x1, y1, x2, y2 = boxes[i]
 | 
				
			||||||
 | 
					                    conf = float(scores[i])
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    # Check if this tracked object is in our zone
 | 
				
			||||||
 | 
					                    box_bottom = ((x1 + x2) / 2, y2)
 | 
				
			||||||
 | 
					                    if point_in_polygon(box_bottom, tracking_zone):
 | 
				
			||||||
 | 
					                        zone_tracks.append({
 | 
				
			||||||
 | 
					                            'id': int(track_id),
 | 
				
			||||||
 | 
					                            'bbox': [int(x1), int(y1), int(x2), int(y2)],
 | 
				
			||||||
 | 
					                            'conf': conf,
 | 
				
			||||||
 | 
					                            'center': ((x1 + x2) / 2, (y1 + y2) / 2),
 | 
				
			||||||
 | 
					                            'bottom': box_bottom
 | 
				
			||||||
 | 
					                        })
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                print(f"  ✅ Zone tracks: {len(zone_tracks)}")
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                # Process each zone track
 | 
				
			||||||
 | 
					                for track in zone_tracks:
 | 
				
			||||||
 | 
					                    tracker_id = track['id']  # Original tracker ID
 | 
				
			||||||
 | 
					                    x1, y1, x2, y2 = track['bbox']
 | 
				
			||||||
 | 
					                    conf = track['conf']
 | 
				
			||||||
 | 
					                    box_center = track['center']
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    # Map tracker ID to clean zone ID
 | 
				
			||||||
 | 
					                    if tracker_id not in tracker_to_zone_id:
 | 
				
			||||||
 | 
					                        tracker_to_zone_id[tracker_id] = next_zone_id
 | 
				
			||||||
 | 
					                        print(f"  🆕 New car: Tracker ID {tracker_id} → Zone ID {next_zone_id}")
 | 
				
			||||||
 | 
					                        next_zone_id += 1
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    zone_id = tracker_to_zone_id[tracker_id]  # Clean sequential ID
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    # Validate track continuity (use tracker_id for internal logic)
 | 
				
			||||||
 | 
					                    is_valid = True
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    # Check for suspicious jumps
 | 
				
			||||||
 | 
					                    if tracker_id in last_positions:
 | 
				
			||||||
 | 
					                        last_center = last_positions[tracker_id]
 | 
				
			||||||
 | 
					                        distance = np.sqrt((box_center[0] - last_center[0])**2 + 
 | 
				
			||||||
 | 
					                                         (box_center[1] - last_center[1])**2)
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        if distance > 400:  # pixels in ~2.0s
 | 
				
			||||||
 | 
					                            is_valid = False
 | 
				
			||||||
 | 
					                            print(f"  ⚠️ Zone ID {zone_id} (Tracker {tracker_id}): suspicious jump {distance:.0f}px")
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    # Skip already successful cars (use zone_id for user logic)
 | 
				
			||||||
 | 
					                    if zone_id in successful_cars:
 | 
				
			||||||
 | 
					                        is_valid = False
 | 
				
			||||||
 | 
					                        print(f"  ✅ Zone ID {zone_id}: already successful, skipping")
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    # Only process valid, high-confidence zone tracks
 | 
				
			||||||
 | 
					                    if is_valid and conf > 0.7:
 | 
				
			||||||
 | 
					                        detected_car_ids.add(zone_id)  # Use zone_id for display
 | 
				
			||||||
 | 
					                        car_id_counts[zone_id] += 1
 | 
				
			||||||
 | 
					                        last_positions[tracker_id] = box_center  # Track by tracker_id internally
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        # Draw tracking results with clean zone ID
 | 
				
			||||||
 | 
					                        zone_color = (0, 255, 0)  # Green for zone cars
 | 
				
			||||||
 | 
					                        cv2.rectangle(frame, (x1, y1), (x2, y2), zone_color, 2)
 | 
				
			||||||
 | 
					                        cv2.putText(frame, f'ZONE ID:{zone_id}', 
 | 
				
			||||||
 | 
					                                  (x1, y1-30), cv2.FONT_HERSHEY_SIMPLEX, 0.6, zone_color, 2)
 | 
				
			||||||
 | 
					                        cv2.putText(frame, f'#{car_id_counts[zone_id]} {conf:.2f}', 
 | 
				
			||||||
 | 
					                                  (x1, y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, zone_color, 2)
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        # Draw center point
 | 
				
			||||||
 | 
					                        cv2.circle(frame, (int(track['bottom'][0]), int(track['bottom'][1])), 5, zone_color, -1)
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        print(f"  ✅ Zone ID {zone_id} (Tracker {tracker_id}): ZONE detection #{car_id_counts[zone_id]} (conf: {conf:.2f})")
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        # Check for success (5 consecutive detections IN ZONE)
 | 
				
			||||||
 | 
					                        if car_id_counts[zone_id] == 5:
 | 
				
			||||||
 | 
					                            print(f"🏆 SUCCESS: Zone ID {zone_id} achieved 5 continuous ZONE detections - TRIGGER NEXT MODEL!")
 | 
				
			||||||
 | 
					                            successful_cars.add(zone_id)
 | 
				
			||||||
 | 
					                            
 | 
				
			||||||
 | 
					                            # Add success indicator to frame
 | 
				
			||||||
 | 
					                            cv2.putText(frame, f"SUCCESS: Zone Car {zone_id}!", 
 | 
				
			||||||
 | 
					                                      (50, height-50), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 0), 3)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            print("  📋 No cars in zone - no tracking performed")
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            # Draw any cars outside the zone in red (for reference)
 | 
				
			||||||
 | 
					            if detection_results[0].boxes is not None:
 | 
				
			||||||
 | 
					                boxes = detection_results[0].boxes.xyxy.cpu()
 | 
				
			||||||
 | 
					                scores = detection_results[0].boxes.conf.cpu()
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                for i in range(len(boxes)):
 | 
				
			||||||
 | 
					                    x1, y1, x2, y2 = boxes[i]
 | 
				
			||||||
 | 
					                    conf = float(scores[i])
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    box_bottom = ((x1 + x2) / 2, y2)
 | 
				
			||||||
 | 
					                    if not point_in_polygon(box_bottom, tracking_zone):
 | 
				
			||||||
 | 
					                        # Draw cars outside zone in red (not tracked)
 | 
				
			||||||
 | 
					                        x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2)
 | 
				
			||||||
 | 
					                        cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 0, 255), 1)
 | 
				
			||||||
 | 
					                        cv2.putText(frame, f'OUT {conf:.2f}', 
 | 
				
			||||||
 | 
					                                  (x1, y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Display results
 | 
				
			||||||
 | 
					        if detected_car_ids:
 | 
				
			||||||
 | 
					            print(f"  📋 Active Zone IDs: {sorted(detected_car_ids)} (Clean sequential IDs)")
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Show ID mapping for debugging
 | 
				
			||||||
 | 
					        if tracker_to_zone_id:
 | 
				
			||||||
 | 
					            mapping_str = ", ".join([f"Tracker{k}→Zone{v}" for k, v in tracker_to_zone_id.items()])
 | 
				
			||||||
 | 
					            print(f"  🔄 ID Mapping: {mapping_str}")
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Add annotations to frame
 | 
				
			||||||
 | 
					        cv2.putText(frame, f"BoT-SORT Zone-First Tracking | Frame: {frame_idx} | {current_time:.2f}s", 
 | 
				
			||||||
 | 
					                   (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 255, 255), 2)
 | 
				
			||||||
 | 
					        cv2.putText(frame, f"Zone Cars: {len(current_zone_cars)} | Active Tracks: {len(detected_car_ids)}", 
 | 
				
			||||||
 | 
					                   (10, 65), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2)
 | 
				
			||||||
 | 
					        cv2.putText(frame, f"Successful Cars: {len(successful_cars)}", 
 | 
				
			||||||
 | 
					                   (10, 100), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)
 | 
				
			||||||
 | 
					        cv2.putText(frame, "TRACKING ZONE", 
 | 
				
			||||||
 | 
					                   (tracking_zone[0][0], tracking_zone[0][1]-10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 255), 2)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Write annotated frame to output video (repeat for 2 seconds duration)
 | 
				
			||||||
 | 
					        write_frame_to_video(out, frame, frame_skip)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Show video with zone tracking info
 | 
				
			||||||
 | 
					        display_frame = cv2.resize(frame, (960, 540))
 | 
				
			||||||
 | 
					        cv2.imshow('BoT-SORT Zone-First Tracking', display_frame)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Quick check for quit
 | 
				
			||||||
 | 
					        key = cv2.waitKey(1) & 0xFF
 | 
				
			||||||
 | 
					        if key == ord('q'):
 | 
				
			||||||
 | 
					            break
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Small delay to see results
 | 
				
			||||||
 | 
					        time.sleep(0.1)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    cap.release()
 | 
				
			||||||
 | 
					    finalize_video(out)
 | 
				
			||||||
 | 
					    cv2.destroyAllWindows()
 | 
				
			||||||
 | 
					    print(f"\n🎯 BoT-SORT zone-first tracking completed!")
 | 
				
			||||||
 | 
					    print(f"📊 Processed {processed_count} frames with {frame_skip/fps:.2f}s intervals")
 | 
				
			||||||
 | 
					    print(f"🏆 Successfully tracked {len(successful_cars)} unique cars IN ZONE")
 | 
				
			||||||
 | 
					    print(f"💾 Annotated video saved to: {output_path}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if __name__ == "__main__":
 | 
				
			||||||
 | 
					    main()
 | 
				
			||||||
							
								
								
									
										190
									
								
								test_detection_tracking.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										190
									
								
								test_detection_tracking.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,190 @@
 | 
				
			||||||
 | 
					#!/usr/bin/env python3
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Test script for the refactored detection and tracking functionality.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					import sys
 | 
				
			||||||
 | 
					import cv2
 | 
				
			||||||
 | 
					import numpy as np
 | 
				
			||||||
 | 
					import logging
 | 
				
			||||||
 | 
					from pathlib import Path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Add the project root to Python path
 | 
				
			||||||
 | 
					sys.path.insert(0, str(Path(__file__).parent))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from siwatsystem.pympta import run_detection_with_tracking, load_pipeline_from_zip
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Set up logging
 | 
				
			||||||
 | 
					logging.basicConfig(
 | 
				
			||||||
 | 
					    level=logging.DEBUG,
 | 
				
			||||||
 | 
					    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					logger = logging.getLogger(__name__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def create_test_frame():
 | 
				
			||||||
 | 
					    """Create a simple test frame for detection testing."""
 | 
				
			||||||
 | 
					    frame = np.zeros((480, 640, 3), dtype=np.uint8)
 | 
				
			||||||
 | 
					    # Add some simple shapes to simulate objects
 | 
				
			||||||
 | 
					    cv2.rectangle(frame, (50, 50), (200, 150), (255, 0, 0), -1)  # Blue rectangle
 | 
				
			||||||
 | 
					    cv2.rectangle(frame, (300, 200), (450, 350), (0, 255, 0), -1)  # Green rectangle
 | 
				
			||||||
 | 
					    cv2.putText(frame, "Test Frame", (250, 400), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
 | 
				
			||||||
 | 
					    return frame
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_detection_function():
 | 
				
			||||||
 | 
					    """Test the structured detection function with mock data."""
 | 
				
			||||||
 | 
					    logger.info("Testing run_detection_with_tracking function...")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Create test frame
 | 
				
			||||||
 | 
					    test_frame = create_test_frame()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Mock node configuration (simulating what would come from an MPTA file)
 | 
				
			||||||
 | 
					    mock_node = {
 | 
				
			||||||
 | 
					        "modelId": "test_detection_v1",
 | 
				
			||||||
 | 
					        "triggerClasses": ["car", "person"],
 | 
				
			||||||
 | 
					        "triggerClassIndices": [0, 1],
 | 
				
			||||||
 | 
					        "minConfidence": 0.5,
 | 
				
			||||||
 | 
					        "multiClass": False,
 | 
				
			||||||
 | 
					        "expectedClasses": [],
 | 
				
			||||||
 | 
					        "tracking": {
 | 
				
			||||||
 | 
					            "enabled": True,
 | 
				
			||||||
 | 
					            "reidConfigPath": "botsort.yaml"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Mock context
 | 
				
			||||||
 | 
					    test_context = {
 | 
				
			||||||
 | 
					        "display_id": "test-display-001",
 | 
				
			||||||
 | 
					        "camera_id": "test-cam-001"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    logger.info("Mock node configuration:")
 | 
				
			||||||
 | 
					    for key, value in mock_node.items():
 | 
				
			||||||
 | 
					        logger.info(f"  {key}: {value}")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Note: This test will fail without a real YOLO model, but demonstrates the structure
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        detections, regions = run_detection_with_tracking(test_frame, mock_node, test_context)
 | 
				
			||||||
 | 
					        logger.info(f"Function executed successfully!")
 | 
				
			||||||
 | 
					        logger.info(f"Returned detections: {len(detections)}")
 | 
				
			||||||
 | 
					        logger.info(f"Returned regions: {list(regions.keys())}")
 | 
				
			||||||
 | 
					        return True
 | 
				
			||||||
 | 
					    except Exception as e:
 | 
				
			||||||
 | 
					        logger.error(f"Function failed (expected without real model): {e}")
 | 
				
			||||||
 | 
					        return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_mpta_loading():
 | 
				
			||||||
 | 
					    """Test loading an MPTA file with tracking configuration."""
 | 
				
			||||||
 | 
					    logger.info("Testing MPTA loading with tracking configuration...")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Check if models directory exists
 | 
				
			||||||
 | 
					    models_dir = Path("models")
 | 
				
			||||||
 | 
					    if not models_dir.exists():
 | 
				
			||||||
 | 
					        logger.warning("No models directory found - skipping MPTA test")
 | 
				
			||||||
 | 
					        return False
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Look for any .mpta files
 | 
				
			||||||
 | 
					    mpta_files = list(models_dir.glob("**/*.mpta"))
 | 
				
			||||||
 | 
					    if not mpta_files:
 | 
				
			||||||
 | 
					        logger.warning("No .mpta files found in models directory - skipping MPTA test")
 | 
				
			||||||
 | 
					        return False
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    mpta_file = mpta_files[0]
 | 
				
			||||||
 | 
					    logger.info(f"Testing with MPTA file: {mpta_file}")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        # Attempt to load pipeline
 | 
				
			||||||
 | 
					        target_dir = f"temp_test_{os.getpid()}"
 | 
				
			||||||
 | 
					        pipeline = load_pipeline_from_zip(str(mpta_file), target_dir)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if pipeline:
 | 
				
			||||||
 | 
					            logger.info("MPTA loaded successfully!")
 | 
				
			||||||
 | 
					            logger.info(f"Pipeline model ID: {pipeline.get('modelId')}")
 | 
				
			||||||
 | 
					            logger.info(f"Tracking config: {pipeline.get('tracking')}")
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            # Clean up
 | 
				
			||||||
 | 
					            import shutil
 | 
				
			||||||
 | 
					            if os.path.exists(target_dir):
 | 
				
			||||||
 | 
					                shutil.rmtree(target_dir)
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            return True
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            logger.error("Failed to load MPTA pipeline")
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					    except Exception as e:
 | 
				
			||||||
 | 
					        logger.error(f"MPTA loading failed: {e}")
 | 
				
			||||||
 | 
					        return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def print_usage_example():
 | 
				
			||||||
 | 
					    """Print example usage of the new structured functions."""
 | 
				
			||||||
 | 
					    logger.info("\n" + "="*60)
 | 
				
			||||||
 | 
					    logger.info("USAGE EXAMPLE - Structured Detection & Tracking")
 | 
				
			||||||
 | 
					    logger.info("="*60)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    example_config = '''
 | 
				
			||||||
 | 
					    Example pipeline.json configuration:
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "pipeline": {
 | 
				
			||||||
 | 
					        "modelId": "car_frontal_detection_v1",
 | 
				
			||||||
 | 
					        "modelFile": "yolo11n.pt",
 | 
				
			||||||
 | 
					        "triggerClasses": ["Car", "Frontal"],
 | 
				
			||||||
 | 
					        "minConfidence": 0.7,
 | 
				
			||||||
 | 
					        "multiClass": true,
 | 
				
			||||||
 | 
					        "expectedClasses": ["Car", "Frontal"],
 | 
				
			||||||
 | 
					        "tracking": {
 | 
				
			||||||
 | 
					          "enabled": true,
 | 
				
			||||||
 | 
					          "reidConfigPath": "botsort_reid.yaml"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "actions": [...],
 | 
				
			||||||
 | 
					        "branches": [...]
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    '''
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    logger.info(example_config)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    code_example = '''
 | 
				
			||||||
 | 
					    Usage in code:
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Load pipeline from MPTA file
 | 
				
			||||||
 | 
					    pipeline = load_pipeline_from_zip("model.mpta", "temp_dir")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Run detection with tracking
 | 
				
			||||||
 | 
					    detections, regions = run_detection_with_tracking(frame, pipeline, context)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Process results
 | 
				
			||||||
 | 
					    for detection in detections:
 | 
				
			||||||
 | 
					        class_name = detection["class"]
 | 
				
			||||||
 | 
					        confidence = detection["confidence"]
 | 
				
			||||||
 | 
					        track_id = detection["id"]  # Available when tracking enabled
 | 
				
			||||||
 | 
					        bbox = detection["bbox"]
 | 
				
			||||||
 | 
					        print(f"Detected: {class_name} (ID: {track_id}, conf: {confidence:.2f})")
 | 
				
			||||||
 | 
					    '''
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    logger.info(code_example)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def main():
 | 
				
			||||||
 | 
					    """Main test function."""
 | 
				
			||||||
 | 
					    logger.info("Starting detection & tracking refactoring tests...")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Test 1: Function structure
 | 
				
			||||||
 | 
					    test1_passed = test_detection_function()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Test 2: MPTA loading
 | 
				
			||||||
 | 
					    test2_passed = test_mpta_loading()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Print usage examples
 | 
				
			||||||
 | 
					    print_usage_example()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Summary
 | 
				
			||||||
 | 
					    logger.info("\n" + "="*60)
 | 
				
			||||||
 | 
					    logger.info("TEST SUMMARY")
 | 
				
			||||||
 | 
					    logger.info("="*60)
 | 
				
			||||||
 | 
					    logger.info(f"Function structure test: {'PASS' if test1_passed else 'EXPECTED FAIL (no model)'}")
 | 
				
			||||||
 | 
					    logger.info(f"MPTA loading test: {'PASS' if test2_passed else 'SKIP (no files)'}")
 | 
				
			||||||
 | 
					    logger.info("\nRefactoring completed successfully!")
 | 
				
			||||||
 | 
					    logger.info("The detection and tracking code is now structured and easy to configure.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if __name__ == "__main__":
 | 
				
			||||||
 | 
					    main()
 | 
				
			||||||
							
								
								
									
										325
									
								
								webcam_rtsp_server.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										325
									
								
								webcam_rtsp_server.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,325 @@
 | 
				
			||||||
 | 
					#!/usr/bin/env python3
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Enhanced webcam server that provides both RTSP streaming and HTTP snapshot endpoints
 | 
				
			||||||
 | 
					Compatible with CMS UI requirements for camera configuration
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import cv2
 | 
				
			||||||
 | 
					import threading
 | 
				
			||||||
 | 
					import time
 | 
				
			||||||
 | 
					import logging
 | 
				
			||||||
 | 
					import socket
 | 
				
			||||||
 | 
					from http.server import BaseHTTPRequestHandler, HTTPServer
 | 
				
			||||||
 | 
					import subprocess
 | 
				
			||||||
 | 
					import sys
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Configure logging
 | 
				
			||||||
 | 
					logging.basicConfig(
 | 
				
			||||||
 | 
					    level=logging.INFO,
 | 
				
			||||||
 | 
					    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					logger = logging.getLogger("webcam_rtsp_server")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Global webcam capture object
 | 
				
			||||||
 | 
					webcam_cap = None
 | 
				
			||||||
 | 
					rtsp_process = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class WebcamHTTPHandler(BaseHTTPRequestHandler):
 | 
				
			||||||
 | 
					    """HTTP handler for snapshot requests"""
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def do_GET(self):
 | 
				
			||||||
 | 
					        if self.path == '/snapshot' or self.path == '/snapshot.jpg':
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                # Capture fresh frame from webcam for each request
 | 
				
			||||||
 | 
					                ret, frame = webcam_cap.read()
 | 
				
			||||||
 | 
					                if ret and frame is not None:
 | 
				
			||||||
 | 
					                    # Encode as JPEG
 | 
				
			||||||
 | 
					                    success, buffer = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, 85])
 | 
				
			||||||
 | 
					                    if success:
 | 
				
			||||||
 | 
					                        self.send_response(200)
 | 
				
			||||||
 | 
					                        self.send_header('Content-Type', 'image/jpeg')
 | 
				
			||||||
 | 
					                        self.send_header('Content-Length', str(len(buffer)))
 | 
				
			||||||
 | 
					                        self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate')
 | 
				
			||||||
 | 
					                        self.send_header('Pragma', 'no-cache')
 | 
				
			||||||
 | 
					                        self.send_header('Expires', '0')
 | 
				
			||||||
 | 
					                        self.end_headers()
 | 
				
			||||||
 | 
					                        self.wfile.write(buffer.tobytes())
 | 
				
			||||||
 | 
					                        logger.debug(f"Served webcam snapshot, size: {len(buffer)} bytes")
 | 
				
			||||||
 | 
					                        return
 | 
				
			||||||
 | 
					                    else:
 | 
				
			||||||
 | 
					                        logger.error("Failed to encode frame as JPEG")
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    logger.error("Failed to capture frame from webcam")
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                # Send error response
 | 
				
			||||||
 | 
					                self.send_response(500)
 | 
				
			||||||
 | 
					                self.send_header('Content-Type', 'text/plain')
 | 
				
			||||||
 | 
					                self.end_headers()
 | 
				
			||||||
 | 
					                self.wfile.write(b'Failed to capture webcam frame')
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					            except Exception as e:
 | 
				
			||||||
 | 
					                logger.error(f"Error serving snapshot: {e}")
 | 
				
			||||||
 | 
					                self.send_response(500)
 | 
				
			||||||
 | 
					                self.send_header('Content-Type', 'text/plain')
 | 
				
			||||||
 | 
					                self.end_headers()
 | 
				
			||||||
 | 
					                self.wfile.write(f'Error: {str(e)}'.encode())
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        elif self.path == '/status':
 | 
				
			||||||
 | 
					            # Status endpoint for health checking
 | 
				
			||||||
 | 
					            self.send_response(200)
 | 
				
			||||||
 | 
					            self.send_header('Content-Type', 'application/json')
 | 
				
			||||||
 | 
					            self.end_headers()
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            width = int(webcam_cap.get(cv2.CAP_PROP_FRAME_WIDTH))
 | 
				
			||||||
 | 
					            height = int(webcam_cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
 | 
				
			||||||
 | 
					            fps = webcam_cap.get(cv2.CAP_PROP_FPS)
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            status = f'{{"status": "online", "width": {width}, "height": {height}, "fps": {fps}}}'
 | 
				
			||||||
 | 
					            self.wfile.write(status.encode())
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            # 404 for other paths
 | 
				
			||||||
 | 
					            self.send_response(404)
 | 
				
			||||||
 | 
					            self.send_header('Content-Type', 'text/plain')
 | 
				
			||||||
 | 
					            self.end_headers()
 | 
				
			||||||
 | 
					            self.wfile.write(b'Not Found - Available endpoints: /snapshot, /snapshot.jpg, /status')
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def log_message(self, format, *args):
 | 
				
			||||||
 | 
					        # Suppress default HTTP server logging to avoid spam
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def check_ffmpeg():
 | 
				
			||||||
 | 
					    """Check if FFmpeg is available for RTSP streaming"""
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        result = subprocess.run(['ffmpeg', '-version'], 
 | 
				
			||||||
 | 
					                              capture_output=True, text=True, timeout=5)
 | 
				
			||||||
 | 
					        if result.returncode == 0:
 | 
				
			||||||
 | 
					            logger.info("FFmpeg found and working")
 | 
				
			||||||
 | 
					            return True
 | 
				
			||||||
 | 
					    except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError):
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    logger.warning("FFmpeg not found. RTSP streaming will not be available.")
 | 
				
			||||||
 | 
					    logger.info("To enable RTSP streaming, install FFmpeg:")
 | 
				
			||||||
 | 
					    logger.info("  Windows: Download from https://ffmpeg.org/download.html")
 | 
				
			||||||
 | 
					    logger.info("  Linux: sudo apt install ffmpeg")
 | 
				
			||||||
 | 
					    logger.info("  macOS: brew install ffmpeg")
 | 
				
			||||||
 | 
					    return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_windows_camera_name():
 | 
				
			||||||
 | 
					    """Get the actual camera device name on Windows"""
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        # List video devices using FFmpeg with proper encoding handling
 | 
				
			||||||
 | 
					        result = subprocess.run(['ffmpeg', '-f', 'dshow', '-list_devices', 'true', '-i', 'dummy'], 
 | 
				
			||||||
 | 
					                              capture_output=True, text=True, timeout=10, encoding='utf-8', errors='ignore')
 | 
				
			||||||
 | 
					        output = result.stderr  # FFmpeg outputs device list to stderr
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Look for video devices in the output
 | 
				
			||||||
 | 
					        lines = output.split('\n')
 | 
				
			||||||
 | 
					        video_devices = []
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Parse the output - look for lines with (video) that contain device names in quotes
 | 
				
			||||||
 | 
					        for line in lines:
 | 
				
			||||||
 | 
					            if '[dshow @' in line and '(video)' in line and '"' in line:
 | 
				
			||||||
 | 
					                # Extract device name between first pair of quotes
 | 
				
			||||||
 | 
					                start = line.find('"') + 1
 | 
				
			||||||
 | 
					                end = line.find('"', start)
 | 
				
			||||||
 | 
					                if start > 0 and end > start:
 | 
				
			||||||
 | 
					                    device_name = line[start:end]
 | 
				
			||||||
 | 
					                    video_devices.append(device_name)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        logger.info(f"Found Windows video devices: {video_devices}")
 | 
				
			||||||
 | 
					        if video_devices:
 | 
				
			||||||
 | 
					            # Force use the first device (index 0) which is the Logitech HD webcam
 | 
				
			||||||
 | 
					            return video_devices[0]  # This will be "罗技高清网络摄像机 C930c"
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            logger.info("No devices found via FFmpeg detection, using fallback")
 | 
				
			||||||
 | 
					            # Fall through to fallback names
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					    except Exception as e:
 | 
				
			||||||
 | 
					        logger.debug(f"Failed to get Windows camera name: {e}")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Try common camera device names as fallback
 | 
				
			||||||
 | 
					    # Prioritize Integrated Camera since that's what's working now
 | 
				
			||||||
 | 
					    common_names = [
 | 
				
			||||||
 | 
					        "Integrated Camera",  # This is working for the current setup
 | 
				
			||||||
 | 
					        "USB Video Device",  # Common name for USB cameras
 | 
				
			||||||
 | 
					        "USB2.0 Camera",
 | 
				
			||||||
 | 
					        "C930c",  # Direct model name
 | 
				
			||||||
 | 
					        "HD Pro Webcam C930c",  # Full Logitech name
 | 
				
			||||||
 | 
					        "Logitech",  # Brand name
 | 
				
			||||||
 | 
					        "USB Camera",
 | 
				
			||||||
 | 
					        "Webcam"
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					    logger.info(f"Using fallback camera names: {common_names}")
 | 
				
			||||||
 | 
					    return common_names[0]  # Return "Integrated Camera" first
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def start_rtsp_stream(webcam_index=0, rtsp_port=8554):
 | 
				
			||||||
 | 
					    """Start RTSP streaming using FFmpeg"""
 | 
				
			||||||
 | 
					    global rtsp_process
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if not check_ffmpeg():
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        # Get the actual camera device name for Windows
 | 
				
			||||||
 | 
					        if sys.platform.startswith('win'):
 | 
				
			||||||
 | 
					            camera_name = get_windows_camera_name()
 | 
				
			||||||
 | 
					            logger.info(f"Using Windows camera device: {camera_name}")
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # FFmpeg command to stream webcam via RTSP
 | 
				
			||||||
 | 
					        if sys.platform.startswith('win'):
 | 
				
			||||||
 | 
					            cmd = [
 | 
				
			||||||
 | 
					                'ffmpeg',
 | 
				
			||||||
 | 
					                '-f', 'dshow',
 | 
				
			||||||
 | 
					                '-i', f'video={camera_name}',  # Use detected camera name
 | 
				
			||||||
 | 
					                '-c:v', 'libx264',
 | 
				
			||||||
 | 
					                '-preset', 'veryfast',
 | 
				
			||||||
 | 
					                '-tune', 'zerolatency',
 | 
				
			||||||
 | 
					                '-r', '30',
 | 
				
			||||||
 | 
					                '-s', '1280x720',
 | 
				
			||||||
 | 
					                '-f', 'rtsp',
 | 
				
			||||||
 | 
					                f'rtsp://localhost:{rtsp_port}/stream'
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					        elif sys.platform.startswith('linux'):
 | 
				
			||||||
 | 
					            cmd = [
 | 
				
			||||||
 | 
					                'ffmpeg',
 | 
				
			||||||
 | 
					                '-f', 'v4l2',
 | 
				
			||||||
 | 
					                '-i', f'/dev/video{webcam_index}',
 | 
				
			||||||
 | 
					                '-c:v', 'libx264',
 | 
				
			||||||
 | 
					                '-preset', 'veryfast',
 | 
				
			||||||
 | 
					                '-tune', 'zerolatency',
 | 
				
			||||||
 | 
					                '-r', '30',
 | 
				
			||||||
 | 
					                '-s', '1280x720',
 | 
				
			||||||
 | 
					                '-f', 'rtsp',
 | 
				
			||||||
 | 
					                f'rtsp://localhost:{rtsp_port}/stream'
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					        else:  # macOS
 | 
				
			||||||
 | 
					            cmd = [
 | 
				
			||||||
 | 
					                'ffmpeg',
 | 
				
			||||||
 | 
					                '-f', 'avfoundation',
 | 
				
			||||||
 | 
					                '-i', f'{webcam_index}:',
 | 
				
			||||||
 | 
					                '-c:v', 'libx264',
 | 
				
			||||||
 | 
					                '-preset', 'veryfast',
 | 
				
			||||||
 | 
					                '-tune', 'zerolatency',
 | 
				
			||||||
 | 
					                '-r', '30',
 | 
				
			||||||
 | 
					                '-s', '1280x720',
 | 
				
			||||||
 | 
					                '-f', 'rtsp',
 | 
				
			||||||
 | 
					                f'rtsp://localhost:{rtsp_port}/stream'
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        logger.info(f"Starting RTSP stream on rtsp://localhost:{rtsp_port}/stream")
 | 
				
			||||||
 | 
					        logger.info(f"FFmpeg command: {' '.join(cmd)}")
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        rtsp_process = subprocess.Popen(
 | 
				
			||||||
 | 
					            cmd,
 | 
				
			||||||
 | 
					            stdout=subprocess.PIPE,
 | 
				
			||||||
 | 
					            stderr=subprocess.PIPE,
 | 
				
			||||||
 | 
					            text=True
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Give FFmpeg a moment to start
 | 
				
			||||||
 | 
					        time.sleep(2)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Check if process is still running
 | 
				
			||||||
 | 
					        if rtsp_process.poll() is None:
 | 
				
			||||||
 | 
					            logger.info("RTSP streaming started successfully")
 | 
				
			||||||
 | 
					            return rtsp_process
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            # Get error output if process failed
 | 
				
			||||||
 | 
					            stdout, stderr = rtsp_process.communicate(timeout=2)
 | 
				
			||||||
 | 
					            logger.error("RTSP streaming failed to start")
 | 
				
			||||||
 | 
					            logger.error(f"FFmpeg stdout: {stdout}")
 | 
				
			||||||
 | 
					            logger.error(f"FFmpeg stderr: {stderr}")
 | 
				
			||||||
 | 
					            return None
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					    except Exception as e:
 | 
				
			||||||
 | 
					        logger.error(f"Failed to start RTSP stream: {e}")
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_local_ip():
 | 
				
			||||||
 | 
					    """Get the Wireguard IP address for external access"""
 | 
				
			||||||
 | 
					    # Use Wireguard IP for external access
 | 
				
			||||||
 | 
					    return "10.101.1.4"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def main():
 | 
				
			||||||
 | 
					    global webcam_cap, rtsp_process
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Configuration - Force use index 0 for Logitech HD webcam
 | 
				
			||||||
 | 
					    webcam_index = 0  # Logitech HD webcam C930c (1920x1080@30fps)
 | 
				
			||||||
 | 
					    http_port = 8080
 | 
				
			||||||
 | 
					    rtsp_port = 8554
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    logger.info("=== Webcam RTSP & HTTP Server ===")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Initialize webcam
 | 
				
			||||||
 | 
					    logger.info("Initializing webcam...")
 | 
				
			||||||
 | 
					    webcam_cap = cv2.VideoCapture(webcam_index)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if not webcam_cap.isOpened():
 | 
				
			||||||
 | 
					        logger.error(f"Failed to open webcam at index {webcam_index}")
 | 
				
			||||||
 | 
					        logger.info("Try different webcam indices (0, 1, 2, etc.)")
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Set webcam properties - Use high resolution for Logitech HD webcam
 | 
				
			||||||
 | 
					    webcam_cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1920)
 | 
				
			||||||
 | 
					    webcam_cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080)
 | 
				
			||||||
 | 
					    webcam_cap.set(cv2.CAP_PROP_FPS, 30)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    width = int(webcam_cap.get(cv2.CAP_PROP_FRAME_WIDTH))
 | 
				
			||||||
 | 
					    height = int(webcam_cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
 | 
				
			||||||
 | 
					    fps = webcam_cap.get(cv2.CAP_PROP_FPS)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    logger.info(f"Webcam initialized: {width}x{height} @ {fps}fps")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Get local IP for CMS configuration
 | 
				
			||||||
 | 
					    local_ip = get_local_ip()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Start RTSP streaming (optional, requires FFmpeg)
 | 
				
			||||||
 | 
					    rtsp_process = start_rtsp_stream(webcam_index, rtsp_port)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Start HTTP server for snapshots
 | 
				
			||||||
 | 
					    server_address = ('0.0.0.0', http_port)  # Bind to all interfaces
 | 
				
			||||||
 | 
					    http_server = HTTPServer(server_address, WebcamHTTPHandler)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    logger.info("\n=== Server URLs for CMS Configuration ===")
 | 
				
			||||||
 | 
					    logger.info(f"HTTP Snapshot URL: http://{local_ip}:{http_port}/snapshot")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if rtsp_process:
 | 
				
			||||||
 | 
					        logger.info(f"RTSP Stream URL: rtsp://{local_ip}:{rtsp_port}/stream")
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        logger.info("RTSP Stream: Not available (FFmpeg not found)")
 | 
				
			||||||
 | 
					        logger.info("HTTP-only mode: Use Snapshot URL for camera input")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    logger.info(f"Status URL: http://{local_ip}:{http_port}/status")
 | 
				
			||||||
 | 
					    logger.info("\n=== CMS Configuration Suggestions ===")
 | 
				
			||||||
 | 
					    logger.info(f"Camera Identifier: webcam-local-01")
 | 
				
			||||||
 | 
					    logger.info(f"RTSP Stream URL: rtsp://{local_ip}:{rtsp_port}/stream")
 | 
				
			||||||
 | 
					    logger.info(f"Snapshot URL: http://{local_ip}:{http_port}/snapshot")
 | 
				
			||||||
 | 
					    logger.info(f"Snapshot Interval: 2000 (ms)")
 | 
				
			||||||
 | 
					    logger.info("\nPress Ctrl+C to stop all servers")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        # Start HTTP server
 | 
				
			||||||
 | 
					        http_server.serve_forever()
 | 
				
			||||||
 | 
					    except KeyboardInterrupt:
 | 
				
			||||||
 | 
					        logger.info("Shutting down servers...")
 | 
				
			||||||
 | 
					    finally:
 | 
				
			||||||
 | 
					        # Clean up
 | 
				
			||||||
 | 
					        if webcam_cap:
 | 
				
			||||||
 | 
					            webcam_cap.release()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if rtsp_process:
 | 
				
			||||||
 | 
					            logger.info("Stopping RTSP stream...")
 | 
				
			||||||
 | 
					            rtsp_process.terminate()
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                rtsp_process.wait(timeout=5)
 | 
				
			||||||
 | 
					            except subprocess.TimeoutExpired:
 | 
				
			||||||
 | 
					                rtsp_process.kill()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        http_server.server_close()
 | 
				
			||||||
 | 
					        logger.info("All servers stopped")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if __name__ == "__main__":
 | 
				
			||||||
 | 
					    main()
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue