From 279bffbcdef904a029794fd33ded5f088db3bbe7 Mon Sep 17 00:00:00 2001 From: wuzhuorong <973204353@qq.com> Date: Thu, 11 Jun 2026 17:27:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E9=9B=86=E6=88=90=E4=BA=8B=E4=BB=B6?= =?UTF-8?q?=E5=86=B3=E7=AD=96/=E8=A7=84=E5=88=99/=E8=81=9A=E5=90=88?= =?UTF-8?q?=E7=AE=A1=E9=81=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/server/services/detection_service.py | 84 +++++++++++++++++++++-- 1 file changed, 79 insertions(+), 5 deletions(-) diff --git a/apps/server/services/detection_service.py b/apps/server/services/detection_service.py index f815f9e..1e197fc 100644 --- a/apps/server/services/detection_service.py +++ b/apps/server/services/detection_service.py @@ -10,6 +10,10 @@ from PIL import Image, ImageDraw, ImageFont import torch from .loitering_service import get_loitering_service +from .adapters import DetectionAdapter +from .event import AlertRuleEngine, EventAggregator, EventDecisionEngine +from core.settings import get_settings +from models.event_schemas import DetectionSource logger = logging.getLogger(__name__) @@ -19,12 +23,24 @@ class DetectionService: self.base_dir = os.path.dirname(os.path.dirname(__file__)) self.results_dir = os.path.join(self.base_dir, "static", "results") self.temp_dir = os.path.join(self.base_dir, "static", "temp") - + os.makedirs(self.results_dir, exist_ok=True) os.makedirs(self.temp_dir, exist_ok=True) - + # 初始化徘徊检测服务(懒加载,实际初始化在第一次使用时) self.loitering_service = get_loitering_service() + + # 事件管道 (MVP-1 / P1-P2-P5) + settings = get_settings() + self.decision_engine = EventDecisionEngine( + min_confidence=settings.detection.min_confidence, + ) + rules_dir = os.path.join(self.base_dir, settings.event_engine.rules_dir) + self.rule_engine = AlertRuleEngine.from_directory(rules_dir) + self.event_aggregator = EventAggregator( + dedup_window_seconds=settings.event_engine.dedup_window_seconds, + max_active_events=settings.event_engine.max_active_events, + ) async def detect_image( self, @@ -133,6 +149,9 @@ class DetectionService: result_data, algorithm_config ) + # 事件管道 (MVP-1): 决策 → 规则 → 聚合 + result_data = self._apply_event_pipeline(result_data, model_id) + return result_data except Exception as e: logger.error(f"图片检测失败: {e}") @@ -170,7 +189,8 @@ class DetectionService: if draw: frame = self.draw_detections(frame, detections, fps) - + + # detect_fire_composite 已自带事件管道,此处无需再次调用 return frame, result_data # 普通单模型检测 @@ -294,6 +314,9 @@ class DetectionService: if draw: frame = self.draw_detections(frame, detections, fps) + # 事件管道 (MVP-1): 决策 → 规则 → 聚合 + result_data = self._apply_event_pipeline(result_data, model_id=model_id) + return frame, result_data except Exception as e: logger.error(f"帧检测失败: {e}") @@ -385,7 +408,7 @@ class DetectionService: processing_time = time.time() - start_time avg_confidence = sum(d['confidence'] for d in all_detections) / len(all_detections) if all_detections else 0 - return { + result_data = { 'success': True, 'message': '复合火灾检测完成', 'detections': all_detections, @@ -400,7 +423,15 @@ class DetectionService: 'model_used': 'fire_composite' } } - + + # 事件管道 (MVP-1): 复合火灾检测产出 candidate/alert 事件 + result_data = self._apply_event_pipeline( + result_data, + model_id='fire_composite', + source=DetectionSource.COMPOSITE, + ) + return result_data + except Exception as e: logger.error(f"复合火灾检测失败: {e}") import traceback @@ -470,6 +501,49 @@ class DetectionService: return result_data + def _apply_event_pipeline( + self, + result_data: Dict, + model_id: Optional[str] = None, + source_id: Optional[str] = None, + source: DetectionSource = DetectionSource.YOLO, + ) -> Dict: + """对检测结果执行 决策 → 规则 → 聚合 三段管道。 + + 在 ``result_data`` 中追加两个字段: + + - ``candidate_events``: List[dict] 决策引擎产出的候选事件 + - ``alert_events``: List[dict] 规则命中后经聚合的预警事件 + """ + + if not result_data.get('success') or not result_data.get('detections'): + result_data['candidate_events'] = [] + result_data['alert_events'] = [] + return result_data + + try: + unified_result = DetectionAdapter.from_yolo( + result_data, model_id=model_id, source=source + ) + candidates = self.decision_engine.decide(unified_result, source_id=source_id) + alerts = self.rule_engine.evaluate(candidates) + emitted = self.event_aggregator.aggregate(alerts) + + result_data['candidate_events'] = [ + event.model_dump(mode='json') for event in candidates + ] + result_data['alert_events'] = [ + event.model_dump(mode='json') for event in emitted + ] + except Exception as e: # noqa: BLE001 + logger.error(f"事件管道执行失败: {e}") + result_data['candidate_events'] = [] + result_data['alert_events'] = [] + result_data['event_pipeline_error'] = str(e) + + return result_data + + def draw_detections( self, frame: np.ndarray,