Refactor: PHASE 2: Core Module Extraction
This commit is contained in:
parent
96bedae80a
commit
4e9ae6bcc4
7 changed files with 3684 additions and 0 deletions
694
detector_worker/pipeline/pipeline_executor.py
Normal file
694
detector_worker/pipeline/pipeline_executor.py
Normal file
|
@ -0,0 +1,694 @@
|
|||
"""
|
||||
Pipeline execution engine for computer vision detection workflows.
|
||||
|
||||
This module provides the main pipeline execution functionality including:
|
||||
- Multi-class detection coordination
|
||||
- Branch processing (parallel and sequential)
|
||||
- Action execution and database operations
|
||||
- Session state management
|
||||
"""
|
||||
|
||||
import logging
|
||||
import concurrent.futures
|
||||
from typing import Dict, List, Any, Optional, Tuple, Union
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
import numpy as np
|
||||
|
||||
from ..core.constants import (
|
||||
DEFAULT_THREAD_POOL_SIZE,
|
||||
CLASSIFICATION_TIMEOUT_SECONDS,
|
||||
PIPELINE_EXECUTION_TIMEOUT
|
||||
)
|
||||
from ..core.exceptions import PipelineError, create_pipeline_error
|
||||
from ..detection.detection_result import DetectionResult, DetectionSession
|
||||
from ..detection.yolo_detector import run_detection_with_tracking
|
||||
from ..detection.stability_validator import validate_pipeline_execution
|
||||
from ..detection.tracking_manager import is_camera_active, occupancy_detector
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PipelineContext:
|
||||
"""Context information passed through pipeline execution."""
|
||||
camera_id: str = "unknown"
|
||||
backend_session_id: Optional[str] = None
|
||||
display_id: Optional[str] = None
|
||||
current_mode: str = "unknown"
|
||||
regions_dict: Optional[Dict[str, Any]] = None
|
||||
session_id: Optional[str] = None
|
||||
timestamp: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary format."""
|
||||
return {
|
||||
"camera_id": self.camera_id,
|
||||
"backend_session_id": self.backend_session_id,
|
||||
"display_id": self.display_id,
|
||||
"current_mode": self.current_mode,
|
||||
"regions_dict": self.regions_dict,
|
||||
"session_id": self.session_id,
|
||||
"timestamp": self.timestamp
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class BranchResult:
|
||||
"""Result from branch execution."""
|
||||
model_id: str
|
||||
success: bool
|
||||
result: Optional[Dict[str, Any]] = None
|
||||
error: Optional[str] = None
|
||||
nested_results: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary format."""
|
||||
data = {
|
||||
"model_id": self.model_id,
|
||||
"success": self.success
|
||||
}
|
||||
if self.result:
|
||||
data["result"] = self.result
|
||||
if self.error:
|
||||
data["error"] = self.error
|
||||
if self.nested_results:
|
||||
data["nested_results"] = self.nested_results
|
||||
return data
|
||||
|
||||
|
||||
@dataclass
|
||||
class PipelineResult:
|
||||
"""Result from pipeline execution."""
|
||||
success: bool
|
||||
primary_detection: Optional[Dict[str, Any]] = None
|
||||
primary_bbox: Optional[List[int]] = None
|
||||
branch_results: Dict[str, Any] = field(default_factory=dict)
|
||||
session_id: Optional[str] = None
|
||||
awaiting_session_id: bool = False
|
||||
awaiting_stable_tracks: bool = False
|
||||
|
||||
def to_tuple(self, return_bbox: bool = False) -> Union[Dict[str, Any], Tuple[Dict[str, Any], List[int]], Tuple[None, None]]:
|
||||
"""Convert to return format expected by original function."""
|
||||
if not self.success or not self.primary_detection:
|
||||
return (None, None) if return_bbox else None
|
||||
|
||||
if return_bbox:
|
||||
return (self.primary_detection, self.primary_bbox or [0, 0, 0, 0])
|
||||
else:
|
||||
return self.primary_detection
|
||||
|
||||
|
||||
class PipelineExecutor:
|
||||
"""
|
||||
Main pipeline execution engine for computer vision detection workflows.
|
||||
|
||||
This class handles the complete pipeline including detection, tracking,
|
||||
branch processing, action execution, and database operations.
|
||||
"""
|
||||
|
||||
def __init__(self, thread_pool_size: int = DEFAULT_THREAD_POOL_SIZE):
|
||||
"""
|
||||
Initialize pipeline executor.
|
||||
|
||||
Args:
|
||||
thread_pool_size: Maximum number of threads for parallel processing
|
||||
"""
|
||||
self.thread_pool_size = thread_pool_size
|
||||
|
||||
def _extract_context(self, context: Optional[Dict[str, Any]]) -> PipelineContext:
|
||||
"""Extract pipeline context from input dictionary."""
|
||||
if not context:
|
||||
return PipelineContext()
|
||||
|
||||
return PipelineContext(
|
||||
camera_id=context.get("camera_id", "unknown"),
|
||||
backend_session_id=context.get("backend_session_id"),
|
||||
display_id=context.get("display_id"),
|
||||
current_mode=context.get("current_mode", "unknown"),
|
||||
regions_dict=context.get("regions_dict"),
|
||||
session_id=context.get("session_id"),
|
||||
timestamp=context.get("timestamp")
|
||||
)
|
||||
|
||||
def _handle_classification_task(self,
|
||||
frame: np.ndarray,
|
||||
node: Dict[str, Any],
|
||||
pipeline_context: PipelineContext,
|
||||
return_bbox: bool) -> Union[Dict[str, Any], Tuple[Dict[str, Any], List[int]], Tuple[None, None]]:
|
||||
"""Handle classification-only pipeline nodes."""
|
||||
try:
|
||||
results = node["model"].predict(frame, stream=False)
|
||||
if not results:
|
||||
return (None, None) if return_bbox else None
|
||||
|
||||
r = results[0]
|
||||
probs = r.probs
|
||||
if probs is None:
|
||||
return (None, None) if return_bbox else None
|
||||
|
||||
top1_idx = int(probs.top1)
|
||||
top1_conf = float(probs.top1conf)
|
||||
class_name = node["model"].names[top1_idx]
|
||||
|
||||
det = {
|
||||
"class": class_name,
|
||||
"confidence": top1_conf,
|
||||
"id": None,
|
||||
class_name: class_name # Add class name as key for backward compatibility
|
||||
}
|
||||
|
||||
# Add specific field mappings for database operations based on model type
|
||||
model_id = node.get("modelId", "").lower()
|
||||
if "brand" in model_id or "brand_cls" in model_id:
|
||||
det["brand"] = class_name
|
||||
elif "bodytype" in model_id or "body" in model_id:
|
||||
det["body_type"] = class_name
|
||||
elif "color" in model_id:
|
||||
det["color"] = class_name
|
||||
|
||||
# Execute actions for classification nodes
|
||||
self._execute_node_actions(node, frame, det, pipeline_context.regions_dict)
|
||||
|
||||
return (det, None) if return_bbox else det
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in classification task for {node.get('modelId')}: {e}")
|
||||
return (None, None) if return_bbox else None
|
||||
|
||||
def _check_camera_active(self, camera_id: str, model_id: str, return_bbox: bool) -> Optional[Union[Dict[str, Any], Tuple[Dict[str, Any], List[int]]]]:
|
||||
"""Check if camera is active for processing."""
|
||||
if not is_camera_active(camera_id, model_id):
|
||||
logger.debug(f"⏰ Camera {camera_id}: Waiting for backend sessionId, sending 'none' detection")
|
||||
none_detection = {
|
||||
"class": "none",
|
||||
"confidence": 1.0,
|
||||
"bbox": [0, 0, 0, 0],
|
||||
"branch_results": {}
|
||||
}
|
||||
return (none_detection, [0, 0, 0, 0]) if return_bbox else none_detection
|
||||
return None
|
||||
|
||||
def _run_detection_stage(self,
|
||||
frame: np.ndarray,
|
||||
node: Dict[str, Any],
|
||||
pipeline_context: PipelineContext,
|
||||
validated_detection: Optional[Dict[str, Any]] = None) -> Tuple[List[Dict[str, Any]], Dict[str, Any], Dict[str, Any]]:
|
||||
"""Run the detection stage of the pipeline."""
|
||||
if validated_detection:
|
||||
track_id = validated_detection.get('track_id')
|
||||
logger.info(f"🔄 PIPELINE: Using validated detection from validation phase - track_id={track_id}")
|
||||
|
||||
# Convert validated detection back to all_detections format for branch processing
|
||||
all_detections = [validated_detection]
|
||||
|
||||
# Create regions_dict based on validated detection class with proper structure
|
||||
class_name = validated_detection.get("class", "car")
|
||||
regions_dict = {
|
||||
class_name: {
|
||||
"confidence": validated_detection.get("confidence"),
|
||||
"bbox": validated_detection.get("bbox", [0, 0, 0, 0]),
|
||||
"detection": validated_detection
|
||||
}
|
||||
}
|
||||
|
||||
# Bypass track validation completely - force pipeline execution
|
||||
track_validation_result = {
|
||||
"validation_complete": True,
|
||||
"stable_tracks": ["cached"], # Use dummy stable track to force pipeline execution
|
||||
"current_tracks": ["cached"],
|
||||
"bypass_validation": True
|
||||
}
|
||||
else:
|
||||
# Normal detection stage - Using structured detection function
|
||||
all_detections, regions_dict, track_validation_result = run_detection_with_tracking(
|
||||
frame, node, pipeline_context.to_dict()
|
||||
)
|
||||
|
||||
return all_detections, regions_dict, track_validation_result
|
||||
|
||||
def _validate_tracking_requirements(self,
|
||||
node: Dict[str, Any],
|
||||
track_validation_result: Dict[str, Any],
|
||||
pipeline_context: PipelineContext,
|
||||
return_bbox: bool) -> Optional[Union[Dict[str, Any], Tuple[Dict[str, Any], List[int]]]]:
|
||||
"""Validate tracking requirements for pipeline execution."""
|
||||
tracking_config = node.get("tracking", {})
|
||||
stability_threshold = tracking_config.get("stabilityThreshold", node.get("stabilityThreshold", 1))
|
||||
|
||||
if stability_threshold <= 1 or not tracking_config.get("enabled", True):
|
||||
return None # No tracking requirements
|
||||
|
||||
# Check if this is a branch node - branches should execute regardless of main validation state
|
||||
is_branch_node = node.get("cropClass") is not None or node.get("parallel") is True
|
||||
|
||||
if is_branch_node:
|
||||
logger.debug(f"🔍 Camera {pipeline_context.camera_id}: Branch node {node.get('modelId')} executing during track validation phase")
|
||||
return None
|
||||
|
||||
# Main pipeline node during track validation - check for stable tracks
|
||||
stable_tracks = track_validation_result.get("stable_tracks", [])
|
||||
|
||||
if not stable_tracks:
|
||||
logger.debug(f"🔒 Camera {pipeline_context.camera_id}: Main pipeline requires stable tracks - none found, skipping pipeline execution")
|
||||
none_detection = {
|
||||
"class": "none",
|
||||
"confidence": 1.0,
|
||||
"bbox": [0, 0, 0, 0],
|
||||
"awaiting_stable_tracks": True
|
||||
}
|
||||
return (none_detection, [0, 0, 0, 0]) if return_bbox else none_detection
|
||||
|
||||
logger.info(f"🎯 Camera {pipeline_context.camera_id}: STABLE TRACKS DETECTED - proceeding with full pipeline (tracks: {stable_tracks})")
|
||||
return None
|
||||
|
||||
def _handle_database_operations(self,
|
||||
node: Dict[str, Any],
|
||||
detection_result: Dict[str, Any],
|
||||
regions_dict: Dict[str, Any],
|
||||
pipeline_context: PipelineContext) -> None:
|
||||
"""Handle database operations if database manager is available."""
|
||||
if not (node.get("db_manager") and regions_dict):
|
||||
return
|
||||
|
||||
detected_classes = list(regions_dict.keys())
|
||||
logger.debug(f"Valid detections found: {detected_classes}")
|
||||
|
||||
if pipeline_context.backend_session_id:
|
||||
# Backend sessionId is available, proceed with database operations
|
||||
display_id = pipeline_context.display_id or "unknown"
|
||||
timestamp = datetime.now().strftime("%Y-%m-%dT%H-%M-%S")
|
||||
|
||||
inserted_session_id = node["db_manager"].insert_initial_detection(
|
||||
display_id=display_id,
|
||||
captured_timestamp=timestamp,
|
||||
session_id=pipeline_context.backend_session_id
|
||||
)
|
||||
|
||||
if inserted_session_id:
|
||||
detection_result["session_id"] = inserted_session_id
|
||||
detection_result["timestamp"] = timestamp
|
||||
logger.info(f"💾 DATABASE RECORD CREATED with backend session_id: {inserted_session_id}")
|
||||
logger.debug(f"Database record: display_id={display_id}, timestamp={timestamp}")
|
||||
else:
|
||||
logger.error(f"Failed to create database record with backend session_id: {pipeline_context.backend_session_id}")
|
||||
else:
|
||||
logger.info(f"📡 Camera {pipeline_context.camera_id}: Full pipeline completed, detection data will be sent to backend. Database operations will occur when sessionId is received.")
|
||||
# Store detection info for later database operations when sessionId arrives
|
||||
detection_result["awaiting_session_id"] = True
|
||||
detection_result["timestamp"] = datetime.now().strftime("%Y-%m-%dT%H-%M-%S")
|
||||
|
||||
def _execute_node_actions(self,
|
||||
node: Dict[str, Any],
|
||||
frame: np.ndarray,
|
||||
detection_result: Dict[str, Any],
|
||||
regions_dict: Optional[Dict[str, Any]]) -> None:
|
||||
"""Execute actions for a node."""
|
||||
# This is a placeholder for action execution
|
||||
# In the actual implementation, this would import and call execute_actions
|
||||
pass
|
||||
|
||||
def _execute_parallel_actions(self,
|
||||
node: Dict[str, Any],
|
||||
frame: np.ndarray,
|
||||
detection_result: Dict[str, Any],
|
||||
regions_dict: Dict[str, Any]) -> None:
|
||||
"""Execute parallel actions after branch completion."""
|
||||
# This is a placeholder for parallel action execution
|
||||
# In the actual implementation, this would import and call execute_parallel_actions
|
||||
pass
|
||||
|
||||
def _crop_region_by_class(self,
|
||||
frame: np.ndarray,
|
||||
regions_dict: Dict[str, Any],
|
||||
class_name: str) -> Optional[np.ndarray]:
|
||||
"""Crop a specific region from frame based on detected class."""
|
||||
if class_name not in regions_dict:
|
||||
logger.warning(f"Class '{class_name}' not found in detected regions")
|
||||
return None
|
||||
|
||||
bbox = regions_dict[class_name]["bbox"]
|
||||
x1, y1, x2, y2 = bbox
|
||||
|
||||
# Validate bbox coordinates
|
||||
if x2 <= x1 or y2 <= y1:
|
||||
logger.warning(f"Invalid bbox for class {class_name}: {bbox}")
|
||||
return None
|
||||
|
||||
try:
|
||||
cropped = frame[y1:y2, x1:x2]
|
||||
if cropped.size == 0:
|
||||
logger.warning(f"Empty crop for class {class_name}")
|
||||
return None
|
||||
return cropped
|
||||
except Exception as e:
|
||||
logger.error(f"Error cropping region for class {class_name}: {e}")
|
||||
return None
|
||||
|
||||
def _prepare_branch_context(self,
|
||||
base_context: PipelineContext,
|
||||
regions_dict: Dict[str, Any],
|
||||
detection_result: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Prepare context for branch execution."""
|
||||
branch_context = base_context.to_dict()
|
||||
branch_context["regions_dict"] = regions_dict
|
||||
|
||||
# Pass session_id from detection_result to branch context for Redis actions
|
||||
if "session_id" in detection_result:
|
||||
branch_context["session_id"] = detection_result["session_id"]
|
||||
logger.debug(f"Added session_id to branch context: {detection_result['session_id']}")
|
||||
elif base_context.backend_session_id:
|
||||
branch_context["session_id"] = base_context.backend_session_id
|
||||
logger.debug(f"Added backend_session_id to branch context: {base_context.backend_session_id}")
|
||||
|
||||
return branch_context
|
||||
|
||||
def _execute_single_branch(self,
|
||||
frame: np.ndarray,
|
||||
branch: Dict[str, Any],
|
||||
branch_context: Dict[str, Any],
|
||||
regions_dict: Dict[str, Any]) -> BranchResult:
|
||||
"""Execute a single branch."""
|
||||
model_id = branch["modelId"]
|
||||
|
||||
try:
|
||||
sub_frame = frame
|
||||
crop_class = branch.get("cropClass")
|
||||
|
||||
logger.info(f"Starting branch: {model_id}, cropClass: {crop_class}")
|
||||
|
||||
# Handle cropping if required
|
||||
if branch.get("crop", False) and crop_class:
|
||||
if crop_class in regions_dict:
|
||||
cropped = self._crop_region_by_class(frame, regions_dict, crop_class)
|
||||
if cropped is not None:
|
||||
sub_frame = cropped # Use cropped image without manual resizing
|
||||
logger.debug(f"Successfully cropped {crop_class} region for {model_id} - model will handle resizing")
|
||||
else:
|
||||
return BranchResult(
|
||||
model_id=model_id,
|
||||
success=False,
|
||||
error=f"Failed to crop {crop_class} region"
|
||||
)
|
||||
else:
|
||||
return BranchResult(
|
||||
model_id=model_id,
|
||||
success=False,
|
||||
error=f"Crop class {crop_class} not found in detected regions"
|
||||
)
|
||||
|
||||
# Execute branch pipeline
|
||||
result, _ = self.run_pipeline(sub_frame, branch, True, branch_context)
|
||||
|
||||
if result:
|
||||
branch_result = BranchResult(
|
||||
model_id=model_id,
|
||||
success=True,
|
||||
result=result
|
||||
)
|
||||
|
||||
# Collect nested branch results if they exist
|
||||
if "branch_results" in result:
|
||||
branch_result.nested_results = result["branch_results"]
|
||||
|
||||
logger.info(f"Branch {model_id} completed: {result}")
|
||||
return branch_result
|
||||
else:
|
||||
return BranchResult(
|
||||
model_id=model_id,
|
||||
success=False,
|
||||
error="Branch returned no result"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in branch {model_id}: {e}")
|
||||
return BranchResult(
|
||||
model_id=model_id,
|
||||
success=False,
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
def _process_branches_parallel(self,
|
||||
frame: np.ndarray,
|
||||
active_branches: List[Dict[str, Any]],
|
||||
branch_context: Dict[str, Any],
|
||||
regions_dict: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Process branches in parallel."""
|
||||
branch_results = {}
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=len(active_branches)) as executor:
|
||||
futures = {}
|
||||
|
||||
for branch in active_branches:
|
||||
future = executor.submit(
|
||||
self._execute_single_branch,
|
||||
frame, branch, branch_context, regions_dict
|
||||
)
|
||||
futures[future] = branch
|
||||
|
||||
# Collect results
|
||||
for future in concurrent.futures.as_completed(futures):
|
||||
branch = futures[future]
|
||||
try:
|
||||
result = future.result()
|
||||
if result.success and result.result:
|
||||
branch_results[result.model_id] = result.result
|
||||
|
||||
# Collect nested branch results
|
||||
for nested_id, nested_result in result.nested_results.items():
|
||||
branch_results[nested_id] = nested_result
|
||||
logger.info(f"Collected nested branch result: {nested_id} = {nested_result}")
|
||||
else:
|
||||
logger.error(f"Branch {result.model_id} failed: {result.error}")
|
||||
except Exception as e:
|
||||
logger.error(f"Branch {branch['modelId']} failed: {e}")
|
||||
|
||||
return branch_results
|
||||
|
||||
def _process_branches_sequential(self,
|
||||
frame: np.ndarray,
|
||||
active_branches: List[Dict[str, Any]],
|
||||
branch_context: Dict[str, Any],
|
||||
regions_dict: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Process branches sequentially."""
|
||||
branch_results = {}
|
||||
|
||||
for branch in active_branches:
|
||||
result = self._execute_single_branch(frame, branch, branch_context, regions_dict)
|
||||
|
||||
if result.success and result.result:
|
||||
branch_results[result.model_id] = result.result
|
||||
|
||||
# Collect nested branch results
|
||||
for nested_id, nested_result in result.nested_results.items():
|
||||
branch_results[nested_id] = nested_result
|
||||
logger.info(f"Collected nested branch result: {nested_id} = {nested_result}")
|
||||
else:
|
||||
logger.error(f"Branch {result.model_id} failed: {result.error}")
|
||||
|
||||
return branch_results
|
||||
|
||||
def _filter_active_branches(self,
|
||||
node: Dict[str, Any],
|
||||
regions_dict: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
"""Filter branches that should be triggered based on detected regions."""
|
||||
active_branches = []
|
||||
|
||||
for br in node["branches"]:
|
||||
trigger_classes = br.get("triggerClasses", [])
|
||||
min_conf = br.get("minConfidence", 0)
|
||||
|
||||
logger.debug(f"Evaluating branch {br['modelId']}: trigger_classes={trigger_classes}, min_conf={min_conf}")
|
||||
|
||||
# Check if any detected class matches branch trigger
|
||||
branch_triggered = False
|
||||
for det_class in regions_dict:
|
||||
det_confidence = regions_dict[det_class]["confidence"]
|
||||
logger.debug(f" Checking detected class '{det_class}' (confidence={det_confidence:.3f}) against triggers {trigger_classes}")
|
||||
|
||||
if (det_class in trigger_classes and det_confidence >= min_conf):
|
||||
active_branches.append(br)
|
||||
branch_triggered = True
|
||||
logger.info(f"Branch {br['modelId']} activated by class '{det_class}' (conf={det_confidence:.3f} >= {min_conf})")
|
||||
break
|
||||
|
||||
if not branch_triggered:
|
||||
logger.debug(f"Branch {br['modelId']} not triggered - no matching classes or insufficient confidence")
|
||||
|
||||
return active_branches
|
||||
|
||||
def _process_branches(self,
|
||||
frame: np.ndarray,
|
||||
node: Dict[str, Any],
|
||||
detection_result: Dict[str, Any],
|
||||
regions_dict: Dict[str, Any],
|
||||
pipeline_context: PipelineContext) -> Dict[str, Any]:
|
||||
"""Process all branches for a node."""
|
||||
if not node.get("branches"):
|
||||
return {}
|
||||
|
||||
# Filter branches that should be triggered
|
||||
active_branches = self._filter_active_branches(node, regions_dict)
|
||||
|
||||
if not active_branches:
|
||||
return {}
|
||||
|
||||
# Prepare branch context
|
||||
branch_context = self._prepare_branch_context(pipeline_context, regions_dict, detection_result)
|
||||
|
||||
# Execute branches
|
||||
if node.get("parallel", False) or any(br.get("parallel", False) for br in active_branches):
|
||||
# Run branches in parallel
|
||||
branch_results = self._process_branches_parallel(frame, active_branches, branch_context, regions_dict)
|
||||
else:
|
||||
# Run branches sequentially
|
||||
branch_results = self._process_branches_sequential(frame, active_branches, branch_context, regions_dict)
|
||||
|
||||
return branch_results
|
||||
|
||||
def run_pipeline(self,
|
||||
frame: np.ndarray,
|
||||
node: Dict[str, Any],
|
||||
return_bbox: bool = False,
|
||||
context: Optional[Dict[str, Any]] = None,
|
||||
validated_detection: Optional[Dict[str, Any]] = None) -> Union[Dict[str, Any], Tuple[Dict[str, Any], List[int]], Tuple[None, None]]:
|
||||
"""
|
||||
Run enhanced pipeline that supports:
|
||||
- Multi-class detection (detecting multiple classes simultaneously)
|
||||
- Parallel branch processing
|
||||
- Region-based actions and cropping
|
||||
- Context passing for session/camera information
|
||||
|
||||
Args:
|
||||
frame: Input frame for processing
|
||||
node: Pipeline node configuration
|
||||
return_bbox: Whether to return bounding box with result
|
||||
context: Optional context information
|
||||
validated_detection: Optional pre-validated detection to use
|
||||
|
||||
Returns:
|
||||
Detection result, optionally with bounding box
|
||||
"""
|
||||
try:
|
||||
# Extract context information
|
||||
pipeline_context = self._extract_context(context)
|
||||
model_id = node.get("modelId", "unknown")
|
||||
|
||||
if pipeline_context.backend_session_id:
|
||||
logger.info(f"🔑 PIPELINE USING BACKEND SESSION_ID: {pipeline_context.backend_session_id} for camera {pipeline_context.camera_id}")
|
||||
|
||||
task = getattr(node["model"], "task", None)
|
||||
|
||||
# ─── Classification stage ───────────────────────────────────
|
||||
if task == "classify":
|
||||
return self._handle_classification_task(frame, node, pipeline_context, return_bbox)
|
||||
|
||||
# ─── Session management check ───────────────────────────────────────
|
||||
camera_inactive_result = self._check_camera_active(pipeline_context.camera_id, model_id, return_bbox)
|
||||
if camera_inactive_result is not None:
|
||||
return camera_inactive_result
|
||||
|
||||
# ─── Detection stage ───
|
||||
all_detections, regions_dict, track_validation_result = self._run_detection_stage(
|
||||
frame, node, pipeline_context, validated_detection
|
||||
)
|
||||
|
||||
if not all_detections:
|
||||
logger.debug("No detections from structured detection function - sending 'none' detection")
|
||||
none_detection = {
|
||||
"class": "none",
|
||||
"confidence": 1.0,
|
||||
"bbox": [0, 0, 0, 0],
|
||||
"branch_results": {}
|
||||
}
|
||||
return (none_detection, [0, 0, 0, 0]) if return_bbox else none_detection
|
||||
|
||||
# ─── Track-Based Validation System ────────────────────────
|
||||
tracking_validation_result = self._validate_tracking_requirements(
|
||||
node, track_validation_result, pipeline_context, return_bbox
|
||||
)
|
||||
if tracking_validation_result is not None:
|
||||
return tracking_validation_result
|
||||
|
||||
# ─── Pre-validate pipeline execution ────────────────────────
|
||||
pipeline_valid, missing_branches = validate_pipeline_execution(node, regions_dict)
|
||||
|
||||
if not pipeline_valid:
|
||||
logger.error(f"Pipeline execution validation FAILED - required branches {missing_branches} cannot execute")
|
||||
logger.error("Aborting pipeline: no Redis actions or database records will be created")
|
||||
return (None, None) if return_bbox else None
|
||||
|
||||
# ─── Execute actions with region information ────────────────
|
||||
detection_result = {
|
||||
"detections": all_detections,
|
||||
"regions": regions_dict,
|
||||
**pipeline_context.to_dict()
|
||||
}
|
||||
|
||||
# ─── Database operations ────
|
||||
self._handle_database_operations(node, detection_result, regions_dict, pipeline_context)
|
||||
|
||||
# Execute actions for root node only if it doesn't have branches
|
||||
# Branch nodes with actions will execute them after branch processing
|
||||
if not node.get("branches") or node.get("modelId") == "yolo11n":
|
||||
self._execute_node_actions(node, frame, detection_result, regions_dict)
|
||||
|
||||
# ─── Branch processing ─────────────────────────────
|
||||
branch_results = self._process_branches(frame, node, detection_result, regions_dict, pipeline_context)
|
||||
detection_result["branch_results"] = branch_results
|
||||
|
||||
# ─── Execute Parallel Actions ───────────────────────────────
|
||||
if node.get("parallelActions") and "branch_results" in detection_result:
|
||||
self._execute_parallel_actions(node, frame, detection_result, regions_dict)
|
||||
|
||||
# ─── Auto-enable occupancy mode after successful pipeline completion ─────────────────
|
||||
occupancy_detector(pipeline_context.camera_id, model_id, enable=True)
|
||||
|
||||
logger.info(f"✅ Camera {pipeline_context.camera_id}: Pipeline completed, detection data will be sent to backend")
|
||||
logger.info(f"🛑 Camera {pipeline_context.camera_id}: Model will stop inference for future frames")
|
||||
logger.info(f"📡 Backend sessionId will be handled when received via WebSocket")
|
||||
|
||||
# ─── Execute actions after successful detection AND branch processing ──────────
|
||||
# This ensures detection nodes (like frontal_detection_v1) execute their actions
|
||||
# after completing both detection and branch processing
|
||||
if node.get("actions") and regions_dict and node.get("modelId") != "yolo11n":
|
||||
# Execute actions for branch detection nodes, skip root to avoid duplication
|
||||
logger.debug(f"Executing post-detection actions for branch node {node.get('modelId')}")
|
||||
self._execute_node_actions(node, frame, detection_result, regions_dict)
|
||||
|
||||
# ─── Return detection result ────────────────────────────────
|
||||
primary_detection = max(all_detections, key=lambda x: x["confidence"])
|
||||
primary_bbox = primary_detection["bbox"]
|
||||
|
||||
# Add branch results and session_id to primary detection for compatibility
|
||||
if "branch_results" in detection_result:
|
||||
primary_detection["branch_results"] = detection_result["branch_results"]
|
||||
if "session_id" in detection_result:
|
||||
primary_detection["session_id"] = detection_result["session_id"]
|
||||
|
||||
return (primary_detection, primary_bbox) if return_bbox else primary_detection
|
||||
|
||||
except Exception as e:
|
||||
pipeline_id = node.get("modelId", "unknown")
|
||||
raise create_pipeline_error(pipeline_id, "pipeline_execution", e)
|
||||
|
||||
|
||||
# Global pipeline executor instance
|
||||
pipeline_executor = PipelineExecutor()
|
||||
|
||||
|
||||
# ===== CONVENIENCE FUNCTIONS =====
|
||||
# These provide the same interface as the original functions in pympta.py
|
||||
|
||||
def run_pipeline(frame: np.ndarray,
|
||||
node: Dict[str, Any],
|
||||
return_bbox: bool = False,
|
||||
context: Optional[Dict[str, Any]] = None,
|
||||
validated_detection: Optional[Dict[str, Any]] = None) -> Union[Dict[str, Any], Tuple[Dict[str, Any], List[int]], Tuple[None, None]]:
|
||||
"""Run enhanced pipeline using global executor instance."""
|
||||
return pipeline_executor.run_pipeline(frame, node, return_bbox, context, validated_detection)
|
||||
|
||||
|
||||
def crop_region_by_class(frame: np.ndarray, regions_dict: Dict[str, Any], class_name: str) -> Optional[np.ndarray]:
|
||||
"""Crop a specific region from frame based on detected class."""
|
||||
return pipeline_executor._crop_region_by_class(frame, regions_dict, class_name)
|
Loading…
Add table
Add a link
Reference in a new issue