Spaces:
Sleeping
Sleeping
deploy: batch update 5 file(s)
Browse files- alert_routes.py +6 -2
- app.py +27 -3
- behavior_analysis/action_recognition.py +1 -1
- live_stream_processor.py +183 -16
- real_time_alerts.py +2 -0
alert_routes.py
CHANGED
|
@@ -45,8 +45,12 @@ def alert_stream():
|
|
| 45 |
event: heartbeat
|
| 46 |
data: {"time": 1234567890}
|
| 47 |
"""
|
| 48 |
-
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 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 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =
|
| 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
|
| 895 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 896 |
if camera_id not in _live_processors:
|
| 897 |
-
_live_processors[camera_id] = LiveStreamProcessor(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)
|