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"),
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,6 +62,12 @@ async def detect_image(
data={}
)
# 判断是否启用复合火灾检测
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
)

View File

@@ -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']:

View File

@@ -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,10 +149,31 @@ 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()
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, {
@@ -160,7 +182,6 @@ class DetectionService:
'stats': None
}
try:
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,

View File

@@ -40,12 +40,21 @@ class ModelService:
self.model_configs = {
'fire_detection': {
'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',
'classes': ['Fire', 'Smoke'],
'labels': {'Fire': '火焰', 'Smoke': '烟雾'},
'size': '61MB',
'description': '基于YOLOv10的火灾烟雾检测模型',
'name': '火灾检测'
'size': '33MB',
'description': '基于YOLOv10-M的火灾烟雾检测模型来自GitHub开源项目123K+图片训练)',
'name': '烟雾检测 (YOLOv10-M)'
},
'helmet_detection': {
'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>
</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
v-model="config.algorithmConfig"
@@ -129,7 +159,7 @@
</template>
<script setup>
import { ref, watch } from 'vue'
import { ref, watch, computed } from 'vue'
import {
Setting,
Picture,
@@ -167,9 +197,15 @@ const config = ref({
model: props.defaultModel || (props.models.length > 0 ? props.models[0].id : ''),
confidence: 0.5,
iou: 0.45,
composite: false,
algorithmConfig: {}
})
// 判断当前是否是火灾检测模型
const isFireDetectionModel = computed(() => {
return config.value.model === 'fire_detection'
})
const formatConfidence = (value) => {
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)
}, { deep: true })
// 监听模型变化,如果不是火灾模型,关闭复合检测
watch(() => config.value.model, (newModel) => {
if (newModel !== 'fire_detection') {
config.value.composite = false
}
})
</script>
<style scoped>

View File

@@ -112,6 +112,21 @@
<div class="stat-label">检测数量</div>
<el-tag size="large" type="primary">{{ stats.total_detections }} </el-tag>
</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-label">平均置信度</div>
<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',
confidence: 0.5,
iou: 0.45,
composite: false,
algorithmConfig: {}
})
@@ -392,6 +408,11 @@ const uploadUrl = computed(() => {
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) {
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',
confidence: 0.5,
iou: 0.45,
composite: false,
algorithmConfig: {}
})
@@ -346,7 +347,8 @@ const startCamera = async () => {
config: {
model_id: config.value.model,
confidence: config.value.confidence,
iou: config.value.iou
iou: config.value.iou,
composite: config.value.composite
}
}
@@ -416,7 +418,8 @@ const updateCameraConfig = () => {
config: {
model_id: config.value.model,
confidence: config.value.confidence,
iou: config.value.iou
iou: config.value.iou,
composite: config.value.composite
}
}

Binary file not shown.