update tracker
This commit is contained in:
		
							parent
							
								
									a54da904f7
								
							
						
					
					
						commit
						3a4a27ca68
					
				
					 7 changed files with 1405 additions and 84 deletions
				
			
		| 
						 | 
				
			
			@ -17,6 +17,13 @@ from .database import DatabaseManager
 | 
			
		|||
# Create a logger specifically for this module
 | 
			
		||||
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:
 | 
			
		||||
    """Validate Redis configuration parameters."""
 | 
			
		||||
    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}")
 | 
			
		||||
    model = YOLO(model_path)
 | 
			
		||||
    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")
 | 
			
		||||
    else:
 | 
			
		||||
        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]
 | 
			
		||||
        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 = {
 | 
			
		||||
        "modelId": node_config["modelId"],
 | 
			
		||||
        "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),
 | 
			
		||||
        "actions": node_config.get("actions", []),
 | 
			
		||||
        "parallelActions": node_config.get("parallelActions", []),
 | 
			
		||||
        "tracking": tracking_config,
 | 
			
		||||
        "stabilityThreshold": stability_threshold,
 | 
			
		||||
        "model": model,
 | 
			
		||||
        "branches": [],
 | 
			
		||||
        "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}")
 | 
			
		||||
        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):
 | 
			
		||||
    """
 | 
			
		||||
    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)
 | 
			
		||||
            return (det, None) if return_bbox else det
 | 
			
		||||
 | 
			
		||||
        # ─── Detection stage - Multi-class support ──────────────────
 | 
			
		||||
        tk = node["triggerClassIndices"]
 | 
			
		||||
        logger.debug(f"Running detection for node {node['modelId']} with trigger classes: {node.get('triggerClasses', [])} (indices: {tk})")
 | 
			
		||||
        logger.debug(f"Node configuration: minConfidence={node['minConfidence']}, multiClass={node.get('multiClass', False)}")
 | 
			
		||||
        # ─── Session management check ───────────────────────────────────────
 | 
			
		||||
        camera_id = context.get("camera_id", "unknown") if context else "unknown"
 | 
			
		||||
        model_id = node.get("modelId", "unknown")
 | 
			
		||||
        
 | 
			
		||||
        res = node["model"].track(
 | 
			
		||||
            frame,
 | 
			
		||||
            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")
 | 
			
		||||
        if not is_camera_active(camera_id, model_id):
 | 
			
		||||
            logger.info(f"⏰ Camera {camera_id}: Tracker stopped - in cooldown period, skipping all detection")
 | 
			
		||||
            return (None, None) if return_bbox else None
 | 
			
		||||
 | 
			
		||||
        # ─── Multi-class validation ─────────────────────────────────
 | 
			
		||||
        if node.get("multiClass", False) and node.get("expectedClasses"):
 | 
			
		||||
            expected_classes = node["expectedClasses"]
 | 
			
		||||
            detected_classes = list(regions_dict.keys())
 | 
			
		||||
        # ─── Detection stage - Using structured detection function ──────────────────
 | 
			
		||||
        all_detections, regions_dict = run_detection_with_tracking(frame, node, context)
 | 
			
		||||
        
 | 
			
		||||
        if not all_detections:
 | 
			
		||||
            logger.warning("No detections from structured detection function - returning null")
 | 
			
		||||
            return (None, None) if return_bbox else None
 | 
			
		||||
 | 
			
		||||
        # Extract bounding boxes for compatibility
 | 
			
		||||
        all_boxes = [det["bbox"] for det in all_detections]
 | 
			
		||||
 | 
			
		||||
        # ─── 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")
 | 
			
		||||
            
 | 
			
		||||
            logger.info(f"Multi-class validation: expected={expected_classes}, detected={detected_classes}")
 | 
			
		||||
            # Check if we have stable tracks for this specific camera
 | 
			
		||||
            has_stable_tracks, stable_detections = check_stable_tracks(camera_id, model_id, regions_dict)
 | 
			
		||||
            
 | 
			
		||||
            # Check if at least one expected class is detected (flexible mode)
 | 
			
		||||
            matching_classes = [cls for cls in expected_classes if cls in detected_classes]
 | 
			
		||||
            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}")
 | 
			
		||||
            
 | 
			
		||||
            if not matching_classes:
 | 
			
		||||
                # No expected classes found at all
 | 
			
		||||
                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:
 | 
			
		||||
                logger.info(f"Partial multi-class detection: {matching_classes} found, {missing_classes} missing")
 | 
			
		||||
            if not has_stable_tracks:
 | 
			
		||||
                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:
 | 
			
		||||
                logger.info(f"Complete multi-class detection success: {detected_classes}")
 | 
			
		||||
        else:
 | 
			
		||||
            logger.debug("No multi-class validation - proceeding with all detections")
 | 
			
		||||
                logger.info(f"Camera {camera_id}: Stable tracks {[det[1] for det in stable_detections]} detected - proceeding with full pipeline")
 | 
			
		||||
 | 
			
		||||
        # ─── Pre-validate pipeline execution ────────────────────────
 | 
			
		||||
        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)
 | 
			
		||||
 | 
			
		||||
        # ─── Parallel branch processing ─────────────────────────────
 | 
			
		||||
        # ─── Branch processing (no stability check here) ─────────────────────────────
 | 
			
		||||
        if node["branches"]:
 | 
			
		||||
            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
 | 
			
		||||
            active_branches = []
 | 
			
		||||
            for br in node["branches"]:
 | 
			
		||||
| 
						 | 
				
			
			@ -848,6 +1151,10 @@ def run_pipeline(frame, node: dict, return_bbox: bool=False, context=None):
 | 
			
		|||
        # ─── Execute Parallel Actions ───────────────────────────────
 | 
			
		||||
        if node.get("parallelActions") and "branch_results" in detection_result:
 | 
			
		||||
            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 ────────────────────────────────
 | 
			
		||||
        primary_detection = max(all_detections, key=lambda x: x["confidence"])
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue