blacksinisterx commited on
Commit
7838616
Β·
verified Β·
1 Parent(s): db1ea2f

deploy: batch update 5 file(s)

Browse files
alert_routes.py CHANGED
@@ -45,8 +45,12 @@ def alert_stream():
45
  event: heartbeat
46
  data: {"time": 1234567890}
47
  """
48
- engine = get_alert_engine()
49
- subscriber_queue = engine.subscribe()
 
 
 
 
50
 
51
  def event_stream():
52
  try:
 
45
  event: heartbeat
46
  data: {"time": 1234567890}
47
  """
48
+ try:
49
+ engine = get_alert_engine()
50
+ subscriber_queue = engine.subscribe()
51
+ except Exception as e:
52
+ logger.error(f"Failed to initialise alert engine / subscribe: {e}")
53
+ return jsonify({'error': 'Alert engine unavailable', 'detail': str(e)}), 503
54
 
55
  def event_stream():
56
  try:
app.py CHANGED
@@ -3691,10 +3691,27 @@ def process_live_frame():
3691
  if 'frame' not in request.files:
3692
  return jsonify({'error': 'No frame provided'}), 400
3693
 
 
 
 
 
 
3694
  camera_id = request.form.get('camera_id', 'webcam_01')
3695
 
3696
  from live_stream_processor import get_live_processor
3697
- processor = get_live_processor(camera_id)
 
 
 
 
 
 
 
 
 
 
 
 
3698
 
3699
  # Decode uploaded JPEG into OpenCV frame
3700
  import cv2
@@ -3735,9 +3752,16 @@ def live_video_feed(camera_id):
3735
  """Video feed endpoint for live stream - streams frames directly"""
3736
  logger.info(f"🎬 ===== VIDEO FEED REQUESTED ===== camera_id: {camera_id}")
3737
  try:
 
 
 
3738
  from live_stream_processor import get_live_processor
3739
-
3740
- processor = get_live_processor(camera_id)
 
 
 
 
3741
  source_type = getattr(processor, 'source_type', '')
3742
  camera_index = getattr(processor, 'camera_index', 0)
3743
  stream_url = getattr(processor, 'stream_url', None)
 
3691
  if 'frame' not in request.files:
3692
  return jsonify({'error': 'No frame provided'}), 400
3693
 
3694
+ # Wait for heavy models to finish loading (max 120s)
3695
+ if not _init_ready.is_set():
3696
+ logger.info("⏳ Waiting for background init before processing live frame...")
3697
+ _init_ready.wait(timeout=120)
3698
+
3699
  camera_id = request.form.get('camera_id', 'webcam_01')
3700
 
3701
  from live_stream_processor import get_live_processor
3702
+
3703
+ # Share pre-loaded models from the main pipeline to avoid loading them twice
3704
+ shared_obj_det = None
3705
+ shared_beh_ana = None
3706
+ if db_video_service:
3707
+ shared_obj_det = getattr(db_video_service, 'object_detector', None)
3708
+ shared_beh_ana = getattr(db_video_service, 'behavior_analyzer', None)
3709
+
3710
+ processor = get_live_processor(
3711
+ camera_id,
3712
+ object_detector=shared_obj_det,
3713
+ behavior_analyzer=shared_beh_ana,
3714
+ )
3715
 
3716
  # Decode uploaded JPEG into OpenCV frame
3717
  import cv2
 
3752
  """Video feed endpoint for live stream - streams frames directly"""
3753
  logger.info(f"🎬 ===== VIDEO FEED REQUESTED ===== camera_id: {camera_id}")
3754
  try:
3755
+ if not _init_ready.is_set():
3756
+ _init_ready.wait(timeout=120)
3757
+
3758
  from live_stream_processor import get_live_processor
3759
+
3760
+ shared_obj_det = getattr(db_video_service, 'object_detector', None) if db_video_service else None
3761
+ shared_beh_ana = getattr(db_video_service, 'behavior_analyzer', None) if db_video_service else None
3762
+ processor = get_live_processor(camera_id,
3763
+ object_detector=shared_obj_det,
3764
+ behavior_analyzer=shared_beh_ana)
3765
  source_type = getattr(processor, 'source_type', '')
3766
  camera_index = getattr(processor, 'camera_index', 0)
3767
  stream_url = getattr(processor, 'stream_url', None)
behavior_analysis/action_recognition.py CHANGED
@@ -139,7 +139,7 @@ ACTION_LABELS = {
139
  # Per-action confidence thresholds
140
  ACTION_CONFIDENCE_THRESHOLDS = {
141
  "fighting": 0.5,
142
- "accident": 0.80,
143
  "climbing": 0.8
144
  }
145
 
 
139
  # Per-action confidence thresholds
140
  ACTION_CONFIDENCE_THRESHOLDS = {
141
  "fighting": 0.5,
142
+ "accident": 0.65,
143
  "climbing": 0.8
144
  }
145
 
live_stream_processor.py CHANGED
@@ -42,13 +42,16 @@ logger = logging.getLogger(__name__)
42
  class LiveStreamProcessor:
43
  """Process live video streams with DetectifAI pipeline"""
44
 
45
- def __init__(self, config: VideoProcessingConfig = None, camera_id: str = "webcam_01"):
 
46
  """
47
  Initialize live stream processor
48
 
49
  Args:
50
  config: VideoProcessingConfig object
51
  camera_id: Unique identifier for the camera/stream
 
 
52
  """
53
  self.config = config or get_security_focused_config()
54
  self.camera_id = camera_id
@@ -60,29 +63,49 @@ class LiveStreamProcessor:
60
  self.keyframe_interval = 1.0 # Extract keyframe every 1 second
61
 
62
  # Initialize database connections
63
- self.db_manager = DatabaseManager()
64
- self.video_repo = VideoRepository(self.db_manager)
65
- self.event_repo = EventRepository(self.db_manager)
66
- self.keyframe_repo = KeyframeRepository(self.db_manager)
67
-
68
- # Initialize processing components
69
- self.object_detector = None
70
- if self.config.enable_object_detection:
 
 
 
 
 
 
 
71
  try:
72
  self.object_detector = ObjectDetector(self.config)
73
- logger.info("βœ… Object detection enabled for live stream")
74
  except Exception as e:
75
  logger.warning(f"⚠️ Object detection initialization failed: {e}")
76
  self.config.enable_object_detection = False
 
 
77
 
78
- self.behavior_analyzer = None
79
- if getattr(self.config, 'enable_behavior_analysis', False):
80
  try:
81
  self.behavior_analyzer = BehaviorAnalysisIntegrator(self.config)
82
- logger.info("βœ… Behavior analysis enabled for live stream")
83
  except Exception as e:
84
  logger.warning(f"⚠️ Behavior analysis initialization failed: {e}")
85
  self.config.enable_behavior_analysis = False
 
 
 
 
 
 
 
 
 
 
 
86
 
87
  # Initialize facial recognition if enabled
88
  self.face_recognizer = None
@@ -325,8 +348,28 @@ class LiveStreamProcessor:
325
  logger.info(f"🎭 REAL-TIME BEHAVIOR: {len(results['behaviors_detected'])} behavior(s) detected: {', '.join(behavior_types)} (frame {self.frame_count})")
326
 
327
  # Generate real-time alerts for each behavior
 
328
  if self.alert_engine:
329
  for beh in results['behaviors_detected']:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330
  alert = self.alert_engine.process_detection(
331
  camera_id=self.camera_id,
332
  detection_class=beh['behavior_type'],
@@ -550,6 +593,112 @@ class LiveStreamProcessor:
550
  logger.error(traceback.format_exc())
551
  return None
552
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
553
  def process_single_frame(self, frame: np.ndarray) -> Dict[str, Any]:
554
  """
555
  Process a single frame sent from the browser webcam.
@@ -891,10 +1040,28 @@ class LiveStreamProcessor:
891
  _live_processors = {}
892
 
893
 
894
- def get_live_processor(camera_id: str = "webcam_01", config: VideoProcessingConfig = None) -> LiveStreamProcessor:
895
- """Get or create a live stream processor for a camera"""
 
 
 
 
 
896
  if camera_id not in _live_processors:
897
- _live_processors[camera_id] = LiveStreamProcessor(config, camera_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
898
  return _live_processors[camera_id]
899
 
900
 
 
42
  class LiveStreamProcessor:
43
  """Process live video streams with DetectifAI pipeline"""
44
 
45
+ def __init__(self, config: VideoProcessingConfig = None, camera_id: str = "webcam_01",
46
+ object_detector=None, behavior_analyzer=None):
47
  """
48
  Initialize live stream processor
49
 
50
  Args:
51
  config: VideoProcessingConfig object
52
  camera_id: Unique identifier for the camera/stream
53
+ object_detector: Pre-loaded ObjectDetector (shared from main pipeline)
54
+ behavior_analyzer: Pre-loaded BehaviorAnalysisIntegrator (shared)
55
  """
56
  self.config = config or get_security_focused_config()
57
  self.camera_id = camera_id
 
63
  self.keyframe_interval = 1.0 # Extract keyframe every 1 second
64
 
65
  # Initialize database connections
66
+ try:
67
+ self.db_manager = DatabaseManager()
68
+ self.video_repo = VideoRepository(self.db_manager)
69
+ self.event_repo = EventRepository(self.db_manager)
70
+ self.keyframe_repo = KeyframeRepository(self.db_manager)
71
+ except Exception as e:
72
+ logger.warning(f"⚠️ Database connection failed (live stream continues without DB): {e}")
73
+ self.db_manager = None
74
+ self.video_repo = None
75
+ self.event_repo = None
76
+ self.keyframe_repo = None
77
+
78
+ # Initialize processing components β€” prefer shared models from main pipeline
79
+ self.object_detector = object_detector
80
+ if self.object_detector is None and self.config.enable_object_detection:
81
  try:
82
  self.object_detector = ObjectDetector(self.config)
83
+ logger.info("βœ… Object detection enabled for live stream (new instance)")
84
  except Exception as e:
85
  logger.warning(f"⚠️ Object detection initialization failed: {e}")
86
  self.config.enable_object_detection = False
87
+ elif self.object_detector is not None:
88
+ logger.info("βœ… Object detection enabled for live stream (shared instance)")
89
 
90
+ self.behavior_analyzer = behavior_analyzer
91
+ if self.behavior_analyzer is None and getattr(self.config, 'enable_behavior_analysis', False):
92
  try:
93
  self.behavior_analyzer = BehaviorAnalysisIntegrator(self.config)
94
+ logger.info("βœ… Behavior analysis enabled for live stream (new instance)")
95
  except Exception as e:
96
  logger.warning(f"⚠️ Behavior analysis initialization failed: {e}")
97
  self.config.enable_behavior_analysis = False
98
+ elif self.behavior_analyzer is not None:
99
+ logger.info("βœ… Behavior analysis enabled for live stream (shared instance)")
100
+
101
+ # General-purpose YOLO detector for person/car validation (Option B)
102
+ self.general_detector = None
103
+ try:
104
+ from ultralytics import YOLO as _YOLO
105
+ self.general_detector = _YOLO("yolov8n.pt")
106
+ logger.info("βœ… General YOLO detector (yolov8n) loaded for person/car validation")
107
+ except Exception as e:
108
+ logger.warning(f"⚠️ General YOLO detector not available: {e}")
109
 
110
  # Initialize facial recognition if enabled
111
  self.face_recognizer = None
 
348
  logger.info(f"🎭 REAL-TIME BEHAVIOR: {len(results['behaviors_detected'])} behavior(s) detected: {', '.join(behavior_types)} (frame {self.frame_count})")
349
 
350
  # Generate real-time alerts for each behavior
351
+ # Option B: validate fight/accident with YOLO person/car detection
352
  if self.alert_engine:
353
  for beh in results['behaviors_detected']:
354
+ btype = beh['behavior_type'].lower()
355
+
356
+ # Option B: fight requires 2+ persons close together
357
+ if btype in ('fighting', 'fight'):
358
+ valid, person_dets = self._validate_fight_with_yolo(processed_frame)
359
+ if not valid:
360
+ logger.info(f"🚫 Skipping fight alert β€” YOLO person check failed")
361
+ continue
362
+ # Include person bounding boxes in alert
363
+ beh['yolo_context'] = person_dets
364
+
365
+ # Option B: accident requires 2+ vehicles close together
366
+ elif btype in ('accident', 'road_accident'):
367
+ valid, vehicle_dets = self._validate_accident_with_yolo(processed_frame)
368
+ if not valid:
369
+ logger.info(f"🚫 Skipping accident alert β€” YOLO vehicle check failed")
370
+ continue
371
+ beh['yolo_context'] = vehicle_dets
372
+
373
  alert = self.alert_engine.process_detection(
374
  camera_id=self.camera_id,
375
  detection_class=beh['behavior_type'],
 
593
  logger.error(traceback.format_exc())
594
  return None
595
 
596
+ def _validate_fight_with_yolo(self, frame: np.ndarray) -> Tuple[bool, List[Dict]]:
597
+ """
598
+ Option B: Validate fighting detection by checking for 2+ persons close together.
599
+ Uses general YOLO (yolov8n) to detect persons (COCO class 0).
600
+
601
+ Returns:
602
+ (is_valid, person_detections) β€” True if 2+ persons are near each other
603
+ """
604
+ if self.general_detector is None:
605
+ # No general detector β€” allow the detection anyway
606
+ return True, []
607
+ try:
608
+ results = self.general_detector(frame, conf=0.35, verbose=False)
609
+ persons = []
610
+ for r in results:
611
+ for box in r.boxes:
612
+ cls_id = int(box.cls)
613
+ if cls_id == 0: # person
614
+ x1, y1, x2, y2 = box.xyxy[0].tolist()
615
+ persons.append({
616
+ 'class': 'person', 'confidence': float(box.conf),
617
+ 'bbox': [x1, y1, x2, y2],
618
+ 'center': ((x1 + x2) / 2, (y1 + y2) / 2)
619
+ })
620
+ if len(persons) < 2:
621
+ logger.info(f"🚫 Fight validation failed: only {len(persons)} person(s) detected, need 2+")
622
+ return False, persons
623
+ # Check proximity: any pair of persons with overlapping/close bounding boxes
624
+ for i in range(len(persons)):
625
+ for j in range(i + 1, len(persons)):
626
+ if self._boxes_are_close(persons[i]['bbox'], persons[j]['bbox'], frame.shape):
627
+ logger.info(f"βœ… Fight validation passed: {len(persons)} persons, pair ({i},{j}) close together")
628
+ return True, persons
629
+ logger.info(f"🚫 Fight validation failed: {len(persons)} persons but none close together")
630
+ return False, persons
631
+ except Exception as e:
632
+ logger.warning(f"⚠️ Fight YOLO validation error: {e}")
633
+ return True, [] # On error, allow detection
634
+
635
+ def _validate_accident_with_yolo(self, frame: np.ndarray) -> Tuple[bool, List[Dict]]:
636
+ """
637
+ Option B: Validate accident detection by checking for 2+ vehicles close together.
638
+ Uses general YOLO (yolov8n) to detect cars/trucks/buses (COCO classes 2,5,7).
639
+
640
+ Returns:
641
+ (is_valid, vehicle_detections) β€” True if 2+ vehicles are near each other
642
+ """
643
+ if self.general_detector is None:
644
+ return True, []
645
+ try:
646
+ results = self.general_detector(frame, conf=0.35, verbose=False)
647
+ vehicles = []
648
+ vehicle_classes = {2: 'car', 3: 'motorcycle', 5: 'bus', 7: 'truck'}
649
+ for r in results:
650
+ for box in r.boxes:
651
+ cls_id = int(box.cls)
652
+ if cls_id in vehicle_classes:
653
+ x1, y1, x2, y2 = box.xyxy[0].tolist()
654
+ vehicles.append({
655
+ 'class': vehicle_classes[cls_id], 'confidence': float(box.conf),
656
+ 'bbox': [x1, y1, x2, y2],
657
+ 'center': ((x1 + x2) / 2, (y1 + y2) / 2)
658
+ })
659
+ if len(vehicles) < 2:
660
+ logger.info(f"🚫 Accident validation failed: only {len(vehicles)} vehicle(s) detected, need 2+")
661
+ return False, vehicles
662
+ for i in range(len(vehicles)):
663
+ for j in range(i + 1, len(vehicles)):
664
+ if self._boxes_are_close(vehicles[i]['bbox'], vehicles[j]['bbox'], frame.shape):
665
+ logger.info(f"βœ… Accident validation passed: {len(vehicles)} vehicles, pair ({i},{j}) close together")
666
+ return True, vehicles
667
+ logger.info(f"🚫 Accident validation failed: {len(vehicles)} vehicles but none close together")
668
+ return False, vehicles
669
+ except Exception as e:
670
+ logger.warning(f"⚠️ Accident YOLO validation error: {e}")
671
+ return True, []
672
+
673
+ @staticmethod
674
+ def _boxes_are_close(bbox1: List[float], bbox2: List[float], frame_shape: tuple,
675
+ overlap_threshold: float = 0.0, distance_ratio: float = 0.15) -> bool:
676
+ """
677
+ Check if two bounding boxes are close/touching/overlapping.
678
+
679
+ Args:
680
+ bbox1, bbox2: [x1, y1, x2, y2]
681
+ frame_shape: (height, width, channels)
682
+ overlap_threshold: IoU threshold (0 = any overlap counts)
683
+ distance_ratio: max gap as fraction of frame diagonal for 'close'
684
+
685
+ Returns:
686
+ True if boxes overlap or are very close
687
+ """
688
+ x1a, y1a, x2a, y2a = bbox1
689
+ x1b, y1b, x2b, y2b = bbox2
690
+ # Check overlap (IoU > 0)
691
+ ix1 = max(x1a, x1b); iy1 = max(y1a, y1b)
692
+ ix2 = min(x2a, x2b); iy2 = min(y2a, y2b)
693
+ if ix2 > ix1 and iy2 > iy1:
694
+ return True # Boxes overlap
695
+ # Check distance between closest edges
696
+ dx = max(0, max(x1a, x1b) - min(x2a, x2b))
697
+ dy = max(0, max(y1a, y1b) - min(y2a, y2b))
698
+ distance = (dx ** 2 + dy ** 2) ** 0.5
699
+ diag = (frame_shape[0] ** 2 + frame_shape[1] ** 2) ** 0.5
700
+ return distance < (diag * distance_ratio)
701
+
702
  def process_single_frame(self, frame: np.ndarray) -> Dict[str, Any]:
703
  """
704
  Process a single frame sent from the browser webcam.
 
1040
  _live_processors = {}
1041
 
1042
 
1043
+ def get_live_processor(camera_id: str = "webcam_01", config: VideoProcessingConfig = None,
1044
+ object_detector=None, behavior_analyzer=None) -> LiveStreamProcessor:
1045
+ """Get or create a live stream processor for a camera.
1046
+
1047
+ If object_detector / behavior_analyzer are provided they will be
1048
+ shared with the processor (avoids loading models twice).
1049
+ """
1050
  if camera_id not in _live_processors:
1051
+ _live_processors[camera_id] = LiveStreamProcessor(
1052
+ config, camera_id,
1053
+ object_detector=object_detector,
1054
+ behavior_analyzer=behavior_analyzer
1055
+ )
1056
+ else:
1057
+ # If processor already exists but doesn't have models yet, inject them
1058
+ proc = _live_processors[camera_id]
1059
+ if proc.object_detector is None and object_detector is not None:
1060
+ proc.object_detector = object_detector
1061
+ logger.info("βœ… Injected shared object detector into existing live processor")
1062
+ if proc.behavior_analyzer is None and behavior_analyzer is not None:
1063
+ proc.behavior_analyzer = behavior_analyzer
1064
+ logger.info("βœ… Injected shared behavior analyzer into existing live processor")
1065
  return _live_processors[camera_id]
1066
 
1067
 
real_time_alerts.py CHANGED
@@ -735,7 +735,9 @@ class RealTimeAlertEngine:
735
  "knife": 0.60,
736
  "fighting": 0.55,
737
  "road_accident": 0.50,
 
738
  "wallclimb": 0.50,
 
739
  "suspicious_reappearance": 0.55,
740
  }
741
  return thresholds.get(detection_class, 0.50)
 
735
  "knife": 0.60,
736
  "fighting": 0.55,
737
  "road_accident": 0.50,
738
+ "accident": 0.50,
739
  "wallclimb": 0.50,
740
+ "climbing": 0.50,
741
  "suspicious_reappearance": 0.55,
742
  }
743
  return thresholds.get(detection_class, 0.50)