From 7396889f32989eccaf3ea9c936259c5d3bc8cd97 Mon Sep 17 00:00:00 2001 From: zhanjiesheng <210466500@qq.com> Date: Tue, 16 Jun 2026 16:58:01 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A2=84=E8=AD=A6=E5=9B=BE=E7=89=87=E6=A3=80?= =?UTF-8?q?=E6=B5=8B=E8=A7=A6=E5=8F=91=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/server/api/detection.py | 3 +- apps/server/services/detection_service.py | 312 +----------------- apps/server/services/event/decision_engine.py | 2 + apps/web/src/components/ImageDetection.vue | 13 +- 4 files changed, 15 insertions(+), 315 deletions(-) diff --git a/apps/server/api/detection.py b/apps/server/api/detection.py index cb4fbb5..414b04d 100644 --- a/apps/server/api/detection.py +++ b/apps/server/api/detection.py @@ -130,7 +130,8 @@ async def detect_image( "detections": result['detections'], "image_base64": img_base64, "stats": result['stats'], - "alerts": result.get('alerts', []), + "alerts": result.get('alerts', []) or result.get('alert_events', []), + "alert_events": result.get('alert_events', []), "behavior_stats": result.get('behavior_stats', {}) } ) diff --git a/apps/server/services/detection_service.py b/apps/server/services/detection_service.py index cdfe91b..1e197fc 100644 --- a/apps/server/services/detection_service.py +++ b/apps/server/services/detection_service.py @@ -12,15 +12,6 @@ import torch from .loitering_service import get_loitering_service from .adapters import DetectionAdapter from .event import AlertRuleEngine, EventAggregator, EventDecisionEngine -from .event.frame_accumulator import MultiFrameAccumulator -from .event.llm_trigger import LLMTrigger -from .llm_analysis_service import ( - LLMAnalysisService, - LLMAnalysisRequest, - create_llm_service_from_settings, -) -from .llm_cost_tracker import LLMCostTracker, get_global_tracker -from .result_fusion import ResultFusion from core.settings import get_settings from models.event_schemas import DetectionSource @@ -50,41 +41,6 @@ class DetectionService: dedup_window_seconds=settings.event_engine.dedup_window_seconds, max_active_events=settings.event_engine.max_active_events, ) - - # LLM 二次判断管道 (MVP-3 / D26-D30) - self._llm_enabled = settings.llm.enabled - if self._llm_enabled: - self._frame_accumulator = MultiFrameAccumulator( - window_seconds=settings.llm_trigger.window_seconds, - ) - self._llm_trigger = LLMTrigger( - accumulator=self._frame_accumulator, - min_consecutive_hits=settings.llm_trigger.min_consecutive_hits, - min_avg_confidence=settings.llm_trigger.min_avg_confidence, - cooldown_seconds=settings.llm_trigger.cooldown_seconds, - severity_bypass=settings.llm_trigger.severity_bypass, - ) - self._llm_service = create_llm_service_from_settings(settings) - self._cost_tracker = get_global_tracker() - self._result_fusion = ResultFusion( - strategy=settings.fusion.strategy, - yolo_weight=settings.fusion.yolo_weight, - llm_weight=settings.fusion.llm_weight, - suppress_on_llm_negative=settings.fusion.suppress_on_llm_negative, - fallback_to_yolo=settings.fusion.fallback_to_yolo, - ) - logger.info( - "LLM 管道已初始化: provider=%s model=%s strategy=%s", - settings.llm.provider, - settings.llm.model, - settings.fusion.strategy, - ) - else: - self._frame_accumulator = None - self._llm_trigger = None - self._llm_service = None - self._cost_tracker = None - self._result_fusion = None async def detect_image( self, @@ -92,8 +48,7 @@ class DetectionService: model_id: str, confidence: float = 0.5, iou: float = 0.45, - algorithm_config: Optional[Dict] = None, - region_polygon: Optional[List[List[int]]] = None + algorithm_config: Optional[Dict] = None ) -> Dict: start_time = time.time() @@ -107,40 +62,6 @@ class DetectionService: } try: - # 违停检测特殊处理:调用专门的违停检测方法 - if model_id == 'illegal_parking_detection' and hasattr(model, 'detect_illegal_parking'): - # 如果提供了禁停区域,单张图片模式下即时判定(时间阈值设为0) - parking_time = 0 if region_polygon else None - parking_result = model.detect_illegal_parking( - image, conf=confidence, illegal_parking_time=parking_time, region_polygon=region_polygon - ) - detections = [] - for vehicle in parking_result.get('illegal_parking', []): - detections.append({ - 'class': 'illegal_parking', - 'label': '违停车辆', - 'confidence': 1.0, - 'bbox': vehicle['bbox'], - 'track_id': vehicle.get('track_id'), - 'parking_duration': vehicle.get('parking_duration', 0) - }) - - processing_time = time.time() - start_time - result_data = { - 'success': parking_result['success'], - 'message': parking_result.get('message', '违停检测完成'), - 'detections': detections, - 'stats': { - **parking_result.get('stats', {}), - 'total_detections': len(detections), - 'processing_time': round(processing_time, 3), - 'model_used': model_id - } - } - result_data = self._apply_event_pipeline(result_data, model_id) - result_data = await self._apply_llm_pipeline(result_data) - return result_data - results = model(image, conf=confidence, iou=iou, verbose=False) detections = [] @@ -230,8 +151,6 @@ class DetectionService: # 事件管道 (MVP-1): 决策 → 规则 → 聚合 result_data = self._apply_event_pipeline(result_data, model_id) - # LLM 二次判断管道 (MVP-3) - result_data = await self._apply_llm_pipeline(result_data, frame=image) return result_data except Exception as e: @@ -283,45 +202,6 @@ class DetectionService: 'stats': None } - # 违停检测特殊处理:调用专门的违停检测方法 - if model_id == 'illegal_parking_detection' and hasattr(model, 'detect_illegal_parking'): - parking_result = model.detect_illegal_parking( - frame, conf=confidence - ) - detections = [] - for vehicle in parking_result.get('illegal_parking', []): - detections.append({ - 'class': 'illegal_parking', - 'label': '违停车辆', - 'confidence': 1.0, - 'bbox': vehicle['bbox'], - 'track_id': vehicle.get('track_id'), - 'parking_duration': vehicle.get('parking_duration', 0) - }) - - processing_time = time.time() - start_time - fps = 1.0 / processing_time if processing_time > 0 else 0 - - result_data = { - 'success': parking_result['success'], - 'message': parking_result.get('message', '违停检测完成'), - 'detections': detections, - 'stats': { - **parking_result.get('stats', {}), - 'total_detections': len(detections), - 'fps': round(fps, 2), - 'processing_time': round(processing_time, 3), - 'model_used': model_id - } - } - result_data = self._apply_event_pipeline(result_data, model_id) - result_data = await self._apply_llm_pipeline(result_data, frame=frame) - - if draw: - frame = self.draw_detections(frame, detections, fps) - - return frame, result_data - results = model(frame, conf=confidence, iou=iou, verbose=False) detections = [] @@ -436,8 +316,6 @@ class DetectionService: # 事件管道 (MVP-1): 决策 → 规则 → 聚合 result_data = self._apply_event_pipeline(result_data, model_id=model_id) - # LLM 二次判断管道 (MVP-3) - result_data = await self._apply_llm_pipeline(result_data, frame=frame) return frame, result_data except Exception as e: @@ -552,8 +430,6 @@ class DetectionService: model_id='fire_composite', source=DetectionSource.COMPOSITE, ) - # LLM 二次判断管道 (MVP-3) - result_data = await self._apply_llm_pipeline(result_data, frame=image) return result_data except Exception as e: @@ -631,15 +507,13 @@ class DetectionService: model_id: Optional[str] = None, source_id: Optional[str] = None, source: DetectionSource = DetectionSource.YOLO, - frame: Optional[np.ndarray] = None, ) -> Dict: - """对检测结果执行 决策 → 规则 → 聚合 → LLM 二次判断 → 融合 管道。 + """对检测结果执行 决策 → 规则 → 聚合 三段管道。 - 在 ``result_data`` 中追加以下字段: + 在 ``result_data`` 中追加两个字段: - ``candidate_events``: List[dict] 决策引擎产出的候选事件 - ``alert_events``: List[dict] 规则命中后经聚合的预警事件 - - ``llm_results``: List[dict] LLM 分析结果 (仅 LLM 启用时) """ if not result_data.get('success') or not result_data.get('detections'): @@ -669,185 +543,6 @@ class DetectionService: return result_data - async def _apply_llm_pipeline( - self, - result_data: Dict, - frame: Optional[np.ndarray] = None, - ) -> Dict: - """LLM 二次判断管道 (MVP-3 / D35)。 - - 在已有 ``alert_events`` 的基础上: - 1. 将候选事件喂给 LLMTrigger 判断是否需要 LLM 复审 - 2. 对触发的决策调用 LLMAnalysisService - 3. 用 ResultFusion 融合 YOLO + LLM 结果 - 4. 用 LLMCostTracker 记录成本 - - 追加字段: - - ``llm_results``: List[dict] LLM 分析结果列表 - """ - - if not self._llm_enabled: - result_data['llm_results'] = [] - return result_data - - alert_events = result_data.get('alert_events', []) - if not alert_events: - result_data['llm_results'] = [] - return result_data - - logger.info( - "[LLM管道] 开始评估 | alert_events=%d | 有frame=%s", - len(alert_events), - frame is not None and frame.size > 0, - ) - - try: - # 1. 从 alert_events 重建 CandidateEvent 列表供 LLMTrigger 评估 - from models.event_schemas import CandidateEvent, AlertEvent, EventType, SeverityLevel, UnifiedDetection - candidates_for_llm = [] - for evt_dict in alert_events: - try: - # AlertEvent 用 detections(列表), CandidateEvent 用 detection(单个) → 取首个 - dets = evt_dict.get('detections', []) - det = dets[0] if dets else None - - candidate = CandidateEvent( - event_type=EventType(evt_dict['event_type']), - severity=SeverityLevel(evt_dict.get('severity', 'high')), - confidence=evt_dict.get('confidence', 0.5), - detection=det, - source_id=evt_dict.get('source_id'), - timestamp=evt_dict.get('timestamp', evt_dict.get('first_seen')), - triggered_rules=[evt_dict.get('rule_name')] if evt_dict.get('rule_name') else [], - ) - candidates_for_llm.append(candidate) - except Exception as exc: - logger.debug("[LLM管道] 重建候选事件失败: %s | 原始数据: %s", exc, {k: v for k, v in evt_dict.items() if k != 'detections'}) - continue - - if not candidates_for_llm: - logger.info("[LLM管道] 无有效候选事件,跳过") - result_data['llm_results'] = [] - return result_data - - # 2. LLMTrigger 评估 - trigger_decisions = self._llm_trigger.evaluate(candidates_for_llm) - - if not trigger_decisions: - logger.info("[LLM管道] 触发器评估: 未达到触发条件 (需连续%d帧或critical严重性)", self._llm_trigger.min_consecutive_hits) - result_data['llm_results'] = [] - return result_data - - logger.info( - "[LLM管道] 触发器命中 %d 个决策 | 详情: %s", - len(trigger_decisions), - [{d.candidate.event_type.value: d.reason} for d in trigger_decisions], - ) - - # 3. 逐个触发决策调用 LLM + 融合 - llm_results = [] - fused_alert_events = [] - for decision in trigger_decisions: - llm_result = None - - # 检查成本追踪器是否允许调用 - if self._cost_tracker and not self._cost_tracker.can_call(): - logger.info( - "[LLM管道] 调用被降级拦截: %s", - self._cost_tracker.reason_for_block(), - ) - elif self._llm_service: - # 构造 LLM 请求 - frames = [frame] if frame is not None and frame.size > 0 else [] - logger.info( - "[LLM管道] 调用 LLM 分析 | 事件=%s | 置信度=%.2f | 图片=%d张", - decision.candidate.event_type.value, - decision.candidate.confidence, - len(frames), - ) - request = LLMAnalysisRequest( - candidate=decision.candidate, - frames=frames, - ) - llm_result = await self._llm_service.analyze_with_fallback(request) - - if llm_result: - logger.info( - "[LLM管道] LLM 返回 | confirmed=%s | confidence=%.2f | reasoning=%.80s", - llm_result.confirmed, - llm_result.confidence, - (llm_result.reasoning or ''), - ) - else: - logger.warning("[LLM管道] LLM 返回空结果 (可能降级到 mock)") - - # 记录成本 - if self._cost_tracker and llm_result: - self._cost_tracker.record( - llm_result, - image_count=len(frames) or 1, - ) - - # 4. 找到对应的 AlertEvent 进行融合 - alert_dict = None - for evt_dict in alert_events: - if ( - evt_dict.get('event_type') == decision.candidate.event_type.value - and evt_dict.get('source_id') == decision.candidate.source_id - ): - alert_dict = evt_dict - break - - if alert_dict is not None and self._result_fusion: - try: - alert_event = AlertEvent(**alert_dict) - outcome = self._result_fusion.fuse(alert_event, llm_result) - if outcome.alert is not None and not outcome.suppressed: - fused_alert_events.append(outcome.alert.model_dump(mode='json')) - llm_results.append({ - 'trigger_reason': decision.reason, - 'llm_confirmed': llm_result.confirmed if llm_result else None, - 'llm_confidence': llm_result.confidence if llm_result else None, - 'llm_reasoning': llm_result.reasoning if llm_result else None, - 'fusion_strategy': outcome.strategy, - 'fusion_reason': outcome.reason, - 'final_confidence': outcome.final_confidence, - 'suppressed': outcome.suppressed, - }) - logger.info( - "[LLM管道] 融合完成 | 策略=%s | 最终置信度=%.2f | 抑制=%s | 原因=%s", - outcome.strategy, - outcome.final_confidence, - outcome.suppressed, - outcome.reason, - ) - except Exception as exc: - logger.error("LLM 融合失败: %s", exc) - llm_results.append({ - 'trigger_reason': decision.reason, - 'llm_confirmed': None, - 'error': str(exc), - }) - - # 用融合后的结果替换原始 alert_events - if fused_alert_events: - result_data['alert_events'] = fused_alert_events - - result_data['llm_results'] = llm_results - logger.info( - "[LLM管道] 完成 | 总决策=%d | LLM结果=%d | 融合后事件=%d", - len(trigger_decisions), - len(llm_results), - len(fused_alert_events), - ) - - except Exception as e: # noqa: BLE001 - logger.error("LLM 管道执行失败: %s", e) - result_data['llm_results'] = [] - result_data['llm_pipeline_error'] = str(e) - - return result_data - def draw_detections( self, @@ -884,7 +579,6 @@ class DetectionService: 'helmet': (255, 255, 0), 'no_helmet': (255, 0, 255), 'cigarette': (0, 165, 255), - 'illegal_parking': (0, 0, 255), # 兼容旧模型类别 'violence': (0, 0, 255), 'fight': (0, 0, 255), diff --git a/apps/server/services/event/decision_engine.py b/apps/server/services/event/decision_engine.py index 062c765..2ec3192 100644 --- a/apps/server/services/event/decision_engine.py +++ b/apps/server/services/event/decision_engine.py @@ -34,7 +34,9 @@ DEFAULT_CLASS_TO_EVENT: Dict[str, EventType] = { # 火灾 "fire": EventType.FIRE, "flame": EventType.FIRE, + "火焰": EventType.FIRE, "smoke": EventType.SMOKE, + "烟雾": EventType.SMOKE, # 抽烟 "smoking": EventType.SMOKING, "cigarette": EventType.SMOKING, diff --git a/apps/web/src/components/ImageDetection.vue b/apps/web/src/components/ImageDetection.vue index 1e6278e..65253db 100644 --- a/apps/web/src/components/ImageDetection.vue +++ b/apps/web/src/components/ImageDetection.vue @@ -583,8 +583,13 @@ const handleUploadSuccess = (response) => { if (response.data.alerts && response.data.alerts.length > 0) { alerts.value = response.data.alerts response.data.alerts.forEach(alert => { + // 同步到全局预警中心,供 AlertList 展示 + alertStore.addAlert(alert) + // 兼容两种字段名: 旧格式 {type, message} / 新格式 {event_type, metadata.description} + const alertType = alert.type || alert.event_type + const alertMessage = alert.message || (alert.metadata && alert.metadata.description) || '' ElMessage({ - message: `行为告警: ${alert.type} - ${alert.message}`, + message: `预警: ${alertType} - ${alertMessage}`, type: 'warning', duration: 3000 }) @@ -1117,8 +1122,6 @@ onUnmounted(() => { .image-container { width: 100%; - aspect-ratio: 16 / 9; - min-height: 400px; max-height: 600px; display: flex; align-items: center; @@ -1130,8 +1133,8 @@ onUnmounted(() => { } .display-image { - width: 100%; - height: 100%; + max-width: 100%; + max-height: 600px; object-fit: contain; background: #000; }