From 0e011dacfd3c81800ddbfb1430042507bd8f9d5d Mon Sep 17 00:00:00 2001 From: zhanjiesheng <210466500@qq.com> Date: Wed, 10 Jun 2026 14:18:43 +0800 Subject: [PATCH] =?UTF-8?q?=E7=81=AB=E7=81=BE=E6=A3=80=E6=B5=8B=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E7=94=B1=E5=9F=BA=E4=BA=8EYOLOv10=E7=9A=84=E7=81=AB?= =?UTF-8?q?=E7=81=BE=E7=83=9F=E9=9B=BE=E6=A3=80=E6=B5=8B=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E5=A4=8D=E5=90=88=E6=A8=A1=E5=9E=8B[?= =?UTF-8?q?=E5=9F=BA=E4=BA=8EYOLOv8=E7=9A=84=E7=81=AB=E7=81=BE=E6=A3=80?= =?UTF-8?q?=E6=B5=8B=E6=A8=A1=E5=9E=8B=EF=BC=88=E5=8D=95=E7=81=AB=E7=84=B0?= =?UTF-8?q?=E6=A3=80=E6=B5=8B=EF=BC=89=EF=BC=8BYOLOv10-M=EF=BC=8C=E4=B8=93?= =?UTF-8?q?=E7=94=A8=E7=81=AB=E7=81=BE=E7=83=9F=E9=9B=BE=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/server/api/detection.py | 15 +- apps/server/services/camera_service.py | 4 +- apps/server/services/detection_service.py | 150 ++++++++++++++++++-- apps/web/src/components/DetectionConfig.vue | 47 +++++- apps/web/src/components/VideoDetection.vue | 7 +- models/fire_detection/best.pt | 4 +- 6 files changed, 206 insertions(+), 21 deletions(-) diff --git a/apps/server/api/detection.py b/apps/server/api/detection.py index 6e64af6..1565a0d 100644 --- a/apps/server/api/detection.py +++ b/apps/server/api/detection.py @@ -20,7 +20,8 @@ async def detect_image( model_id: str = Query("fire_detection"), confidence: float = Query(0.5), iou: float = Query(0.45), - algorithm_config: Optional[str] = Query(None, description="算法配置JSON字符串") + algorithm_config: Optional[str] = Query(None, description="算法配置JSON字符串"), + composite: bool = Query(False, description="是否启用复合检测(火灾检测时同时检测火焰和烟雾)") ): """ 图片检测接口 @@ -61,9 +62,15 @@ async def detect_image( data={} ) - result = await detection_service.detect_image( - frame, model_id, confidence, iou, algorithm_config=algo_config - ) + # 判断是否启用复合火灾检测 + if composite and model_id == 'fire_detection': + result = await detection_service.detect_fire_composite( + frame, confidence, iou + ) + else: + result = await detection_service.detect_image( + frame, model_id, confidence, iou, algorithm_config=algo_config + ) if result['success']: annotated_frame = detection_service.draw_detections( diff --git a/apps/server/services/camera_service.py b/apps/server/services/camera_service.py index 46d2783..e80e94b 100644 --- a/apps/server/services/camera_service.py +++ b/apps/server/services/camera_service.py @@ -235,13 +235,15 @@ class CameraService: confidence = config.get('confidence', 0.5) iou = config.get('iou', 0.45) draw = True + composite = config.get('composite', False) processed_frame, result = await detection_service.detect_frame( frame, model_id=model_id, confidence=confidence, iou=iou, - draw=draw + draw=draw, + composite=composite ) if result['success']: diff --git a/apps/server/services/detection_service.py b/apps/server/services/detection_service.py index 2234afd..f815f9e 100644 --- a/apps/server/services/detection_service.py +++ b/apps/server/services/detection_service.py @@ -4,10 +4,11 @@ import numpy as np import time import uuid import logging -import torch from typing import Dict, List, Optional from PIL import Image, ImageDraw, ImageFont +import torch + from .loitering_service import get_loitering_service logger = logging.getLogger(__name__) @@ -148,19 +149,39 @@ class DetectionService: model_id: str, confidence: float = 0.5, iou: float = 0.45, - draw: bool = True + draw: bool = True, + composite: bool = False ) -> tuple: start_time = time.time() - model = await self.model_service.load_model(model_id) - if not model: - return frame, { - 'success': False, - 'detections': [], - 'stats': None - } - try: + # 如果是火灾检测模型且启用了复合检测 + if composite and model_id == 'fire_detection': + result_data = await self.detect_fire_composite(frame, confidence=confidence, iou=iou) + + if result_data['success']: + detections = result_data['detections'] + processing_time = time.time() - start_time + fps = 1.0 / processing_time if processing_time > 0 else 0 + + # 更新 stats 中的 fps 和处理时间 + result_data['stats']['fps'] = round(fps, 2) + result_data['stats']['processing_time'] = round(processing_time, 3) + + if draw: + frame = self.draw_detections(frame, detections, fps) + + return frame, result_data + + # 普通单模型检测 + model = await self.model_service.load_model(model_id) + if not model: + return frame, { + 'success': False, + 'detections': [], + 'stats': None + } + results = model(frame, conf=confidence, iou=iou, verbose=False) detections = [] @@ -282,6 +303,115 @@ class DetectionService: 'stats': None } + async def detect_fire_composite( + self, + image: np.ndarray, + confidence: float = 0.1, + iou: float = 0.45 + ) -> Dict: + """ + 复合火灾检测:同时检测火焰和烟雾 + """ + start_time = time.time() + + try: + # 1. 检测火焰 + fire_model = await self.model_service.load_model('fire_detection') + fire_results = fire_model(image, conf=confidence, iou=iou, verbose=False) + + fire_detections = [] + for result in fire_results: + for box in result.boxes: + try: + xyxy_values = box.xyxy.squeeze().tolist() + if len(xyxy_values) >= 4: + x1, y1, x2, y2 = float(xyxy_values[0]), float(xyxy_values[1]), float(xyxy_values[2]), float(xyxy_values[3]) + else: + continue + + conf = float(box.conf[0]) if hasattr(box.conf, '__getitem__') else float(box.conf) + cls = int(box.cls[0]) if hasattr(box.cls, '__getitem__') else int(box.cls) + class_name = result.names[cls] + + if class_name == 'Fire': + fire_detections.append({ + 'class': 'Fire', + 'label': '火焰', + 'confidence': round(conf, 3), + 'bbox': [int(x1), int(y1), int(x2), int(y2)], + 'type': 'fire' + }) + except Exception as e: + logger.error(f"火焰检测解析失败: {e}") + continue + + # 2. 检测烟雾(使用YOLOv10-M专用火灾烟雾模型,只保留 Smoke 类别) + smoke_model = await self.model_service.load_model('smoke_detection') + smoke_results = smoke_model(image, conf=confidence, iou=iou, verbose=False) + + smoke_detections = [] + for result in smoke_results: + for box in result.boxes: + try: + xyxy_values = box.xyxy.squeeze().tolist() + if len(xyxy_values) >= 4: + x1, y1, x2, y2 = float(xyxy_values[0]), float(xyxy_values[1]), float(xyxy_values[2]), float(xyxy_values[3]) + else: + continue + + conf = float(box.conf[0]) if hasattr(box.conf, '__getitem__') else float(box.conf) + cls = int(box.cls[0]) if hasattr(box.cls, '__getitem__') else int(box.cls) + class_name = result.names[cls] + + # 只保留 Smoke 类别(注意模型输出为大写 S) + if class_name == 'Smoke': + smoke_detections.append({ + 'class': 'Smoke', + 'label': '烟雾', + 'confidence': round(conf, 3), + 'bbox': [int(x1), int(y1), int(x2), int(y2)], + 'type': 'smoke' + }) + except Exception as e: + logger.error(f"烟雾检测解析失败: {e}") + continue + + # 3. 合并所有检测 + all_detections = fire_detections + smoke_detections + + # 4. 判定是否疑似火灾(火焰或烟雾任一检测到即判定为是) + suspected_fire = len(fire_detections) > 0 or len(smoke_detections) > 0 + + 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 { + 'success': True, + 'message': '复合火灾检测完成', + 'detections': all_detections, + 'stats': { + 'total_detections': len(all_detections), + 'fire_count': len(fire_detections), + 'smoke_count': len(smoke_detections), + 'avg_confidence': round(avg_confidence, 3), + 'suspected_fire': suspected_fire, + 'suspected_fire_label': '是' if suspected_fire else '否', + 'processing_time': round(processing_time, 3), + 'model_used': 'fire_composite' + } + } + + except Exception as e: + logger.error(f"复合火灾检测失败: {e}") + import traceback + logger.error(f"错误堆栈: {traceback.format_exc()}") + return { + 'success': False, + 'message': f'复合火灾检测失败: {str(e)}', + 'detections': [], + 'stats': None + } + def _apply_behavior_analysis( self, result_data: Dict, diff --git a/apps/web/src/components/DetectionConfig.vue b/apps/web/src/components/DetectionConfig.vue index 4d09f8c..3d2eb48 100644 --- a/apps/web/src/components/DetectionConfig.vue +++ b/apps/web/src/components/DetectionConfig.vue @@ -117,6 +117,36 @@
+ +复合火灾检测是什么?
+同时检测火焰和烟雾,提高火灾识别准确率
+检测内容:
+建议:火灾检测场景建议开启
+