Compare commits

...

3 Commits

8 changed files with 239 additions and 24 deletions

View File

@@ -20,7 +20,8 @@ async def detect_image(
model_id: str = Query("fire_detection"), model_id: str = Query("fire_detection"),
confidence: float = Query(0.5), confidence: float = Query(0.5),
iou: float = Query(0.45), 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={} 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']: if result['success']:
annotated_frame = detection_service.draw_detections( annotated_frame = detection_service.draw_detections(

View File

@@ -235,13 +235,15 @@ class CameraService:
confidence = config.get('confidence', 0.5) confidence = config.get('confidence', 0.5)
iou = config.get('iou', 0.45) iou = config.get('iou', 0.45)
draw = True draw = True
composite = config.get('composite', False)
processed_frame, result = await detection_service.detect_frame( processed_frame, result = await detection_service.detect_frame(
frame, frame,
model_id=model_id, model_id=model_id,
confidence=confidence, confidence=confidence,
iou=iou, iou=iou,
draw=draw draw=draw,
composite=composite
) )
if result['success']: if result['success']:

View File

@@ -4,10 +4,11 @@ import numpy as np
import time import time
import uuid import uuid
import logging import logging
import torch
from typing import Dict, List, Optional from typing import Dict, List, Optional
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
import torch
from .loitering_service import get_loitering_service from .loitering_service import get_loitering_service
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -148,19 +149,39 @@ class DetectionService:
model_id: str, model_id: str,
confidence: float = 0.5, confidence: float = 0.5,
iou: float = 0.45, iou: float = 0.45,
draw: bool = True draw: bool = True,
composite: bool = False
) -> tuple: ) -> tuple:
start_time = time.time() start_time = time.time()
model = await self.model_service.load_model(model_id)
if not model:
return frame, {
'success': False,
'detections': [],
'stats': None
}
try: 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) results = model(frame, conf=confidence, iou=iou, verbose=False)
detections = [] detections = []
@@ -282,6 +303,115 @@ class DetectionService:
'stats': None '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( def _apply_behavior_analysis(
self, self,
result_data: Dict, result_data: Dict,

View File

@@ -40,12 +40,21 @@ class ModelService:
self.model_configs = { self.model_configs = {
'fire_detection': { 'fire_detection': {
'path': os.path.join(base_dir, 'models', 'fire_detection', 'best.pt'), 'path': os.path.join(base_dir, 'models', 'fire_detection', 'best.pt'),
'type': 'yolov8',
'classes': ['Fire'],
'labels': {'Fire': '火焰'},
'size': '22MB',
'description': '基于YOLOv8的火焰检测模型',
'name': '火焰检测'
},
'smoke_detection': {
'path': os.path.join(base_dir, 'models', 'fire_detection', 'yolov10_fire_smoke_best.pt'),
'type': 'yolov10', 'type': 'yolov10',
'classes': ['Fire', 'Smoke'], 'classes': ['Fire', 'Smoke'],
'labels': {'Fire': '火焰', 'Smoke': '烟雾'}, 'labels': {'Fire': '火焰', 'Smoke': '烟雾'},
'size': '61MB', 'size': '33MB',
'description': '基于YOLOv10的火灾烟雾检测模型', 'description': '基于YOLOv10-M的火灾烟雾检测模型来自GitHub开源项目123K+图片训练)',
'name': '火灾检测' 'name': '烟雾检测 (YOLOv10-M)'
}, },
'helmet_detection': { 'helmet_detection': {
'path': os.path.join(base_dir, 'models', 'helmet_detection', 'yolov8n.pt'), 'path': os.path.join(base_dir, 'models', 'helmet_detection', 'yolov8n.pt'),

View File

@@ -117,6 +117,36 @@
<div class="slider-value">{{ config.iou.toFixed(2) }}</div> <div class="slider-value">{{ config.iou.toFixed(2) }}</div>
</el-form-item> </el-form-item>
<!-- 复合检测开关仅对火灾检测模型显示 -->
<el-form-item v-if="isFireDetectionModel">
<template #label>
<span>复合火灾检测</span>
<el-tooltip placement="top" :show-after="200">
<template #content>
<div style="max-width: 300px; line-height: 1.6;">
<p><strong>复合火灾检测是什么</strong></p>
<p>同时检测火焰和烟雾提高火灾识别准确率</p>
<p style="margin: 8px 0;"><strong>检测内容</strong></p>
<ul style="margin: 8px 0; padding-left: 16px;">
<li>火焰检测识别明火区域</li>
<li>烟雾检测识别烟雾区域</li>
<li>综合判断根据两者结果评估火灾等级</li>
</ul>
<p><strong>建议火灾检测场景建议开启</strong></p>
</div>
</template>
<el-icon class="help-icon"><QuestionFilled /></el-icon>
</el-tooltip>
</template>
<el-switch
v-model="config.composite"
active-text="开启"
inactive-text="关闭"
:active-value="true"
:inactive-value="false"
/>
</el-form-item>
<!-- 算法配置仅对人员检测模型显示 --> <!-- 算法配置仅对人员检测模型显示 -->
<AlgorithmConfig <AlgorithmConfig
v-model="config.algorithmConfig" v-model="config.algorithmConfig"
@@ -129,7 +159,7 @@
</template> </template>
<script setup> <script setup>
import { ref, watch } from 'vue' import { ref, watch, computed } from 'vue'
import { import {
Setting, Setting,
Picture, Picture,
@@ -167,9 +197,15 @@ const config = ref({
model: props.defaultModel || (props.models.length > 0 ? props.models[0].id : ''), model: props.defaultModel || (props.models.length > 0 ? props.models[0].id : ''),
confidence: 0.5, confidence: 0.5,
iou: 0.45, iou: 0.45,
composite: false,
algorithmConfig: {} algorithmConfig: {}
}) })
// 判断当前是否是火灾检测模型
const isFireDetectionModel = computed(() => {
return config.value.model === 'fire_detection'
})
const formatConfidence = (value) => { const formatConfidence = (value) => {
return `置信度: ${value.toFixed(2)}` return `置信度: ${value.toFixed(2)}`
} }
@@ -191,9 +227,16 @@ watch(configType, (newType) => {
}) })
// 监听配置变化 // 监听配置变化
watch(() => [config.value.model, config.value.confidence, config.value.iou], () => { watch(() => [config.value.model, config.value.confidence, config.value.iou, config.value.composite], () => {
emit('config-change', config.value) emit('config-change', config.value)
}, { deep: true }) }, { deep: true })
// 监听模型变化,如果不是火灾模型,关闭复合检测
watch(() => config.value.model, (newModel) => {
if (newModel !== 'fire_detection') {
config.value.composite = false
}
})
</script> </script>
<style scoped> <style scoped>

View File

@@ -112,6 +112,21 @@
<div class="stat-label">检测数量</div> <div class="stat-label">检测数量</div>
<el-tag size="large" type="primary">{{ stats.total_detections }} </el-tag> <el-tag size="large" type="primary">{{ stats.total_detections }} </el-tag>
</div> </div>
<!-- 复合检测统计 -->
<div v-if="stats.fire_count !== undefined" class="stat-item">
<div class="stat-label">火焰数量</div>
<el-tag size="large" type="danger">{{ stats.fire_count }} </el-tag>
</div>
<div v-if="stats.smoke_count !== undefined" class="stat-item">
<div class="stat-label">烟雾数量</div>
<el-tag size="large" type="warning">{{ stats.smoke_count }} </el-tag>
</div>
<div v-if="stats.suspected_fire !== undefined" class="stat-item">
<div class="stat-label">疑似火灾</div>
<el-tag size="large" :type="stats.suspected_fire ? 'danger' : 'success'">
{{ stats.suspected_fire_label || (stats.suspected_fire ? '是' : '否') }}
</el-tag>
</div>
<div class="stat-item"> <div class="stat-item">
<div class="stat-label">平均置信度</div> <div class="stat-label">平均置信度</div>
<el-tag size="large" :type="getConfidenceType(stats.avg_confidence)"> <el-tag size="large" :type="getConfidenceType(stats.avg_confidence)">
@@ -310,6 +325,7 @@ const config = ref({
model: props.models.length > 0 ? props.models[0].id : 'fire_detection', model: props.models.length > 0 ? props.models[0].id : 'fire_detection',
confidence: 0.5, confidence: 0.5,
iou: 0.45, iou: 0.45,
composite: false,
algorithmConfig: {} algorithmConfig: {}
}) })
@@ -392,6 +408,11 @@ const uploadUrl = computed(() => {
iou: config.value.iou iou: config.value.iou
}) })
// 添加复合检测参数(火灾检测模型时)
if (config.value.composite && config.value.model === 'fire_detection') {
params.append('composite', 'true')
}
if (config.value.algorithmConfig && Object.keys(config.value.algorithmConfig).length > 0) { if (config.value.algorithmConfig && Object.keys(config.value.algorithmConfig).length > 0) {
params.append('algorithm_config', JSON.stringify(config.value.algorithmConfig)) params.append('algorithm_config', JSON.stringify(config.value.algorithmConfig))
} }

View File

@@ -232,6 +232,7 @@ const config = ref({
model: props.models.length > 0 ? props.models[0].id : 'fire_detection', model: props.models.length > 0 ? props.models[0].id : 'fire_detection',
confidence: 0.5, confidence: 0.5,
iou: 0.45, iou: 0.45,
composite: false,
algorithmConfig: {} algorithmConfig: {}
}) })
@@ -346,7 +347,8 @@ const startCamera = async () => {
config: { config: {
model_id: config.value.model, model_id: config.value.model,
confidence: config.value.confidence, confidence: config.value.confidence,
iou: config.value.iou iou: config.value.iou,
composite: config.value.composite
} }
} }
@@ -416,7 +418,8 @@ const updateCameraConfig = () => {
config: { config: {
model_id: config.value.model, model_id: config.value.model,
confidence: config.value.confidence, confidence: config.value.confidence,
iou: config.value.iou iou: config.value.iou,
composite: config.value.composite
} }
} }

Binary file not shown.