Compare commits
3 Commits
30ea6eb0fb
...
9be93340a7
| Author | SHA1 | Date | |
|---|---|---|---|
| 9be93340a7 | |||
| ca4f977ff0 | |||
| 0e011dacfd |
@@ -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
|
||||
)
|
||||
|
||||
@@ -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']:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BIN
models/fire_detection/best.pt
LFS
BIN
models/fire_detection/best.pt
LFS
Binary file not shown.
Reference in New Issue
Block a user