最近在帮朋友调试一个火灾监控方向的深度学习项目,核心是一套基于深度学习的火焰烟雾检测系统,模型侧把YOLOv5/v8/v11/v12都跑通了,后端用Django做了Web服务,配上网页展示界面,整套系统包含训练代码、训练数据集和模型权重。因为涉及从数据标注、模型训练到Web落地的完整链路,过程中踩了不少坑,也有不少可以给后来人参考的经验,所以整理成这篇分享。
这套系统能做什么、解决什么问题?就是对摄像头画面或者上传的图片进行实时推理,把画面中的火焰和烟雾区域用检测框标出来,并给出置信度。适合三类人参考:一是做火灾检测方向毕业设计的学生,需要一个能演示、能答辩的全栈系统;二是想从纯算法转向完整工程项目、只做过单模型推理的开发者;三是消防安防行业的工程师,想快速评估深度学习目标检测在自己业务场景下的效果。
1. 系统整体架构与方案选型思路
1.1 一个完整的检测系统需要哪些组件
很多人一看到"火焰烟雾检测"这个标题,第一反应就是找个模型训练一下,然后拿几张图测测效果就完了。但一个能叫"系统"的东西,至少得包含下面这几块:
第一是数据层。深度学习模型的质量上限基本由数据决定,火焰烟雾这类目标尤其讲究。你需要一份包含火焰、烟雾不同场景、不同光照、不同距离的标注好的图片集,并且格式要跟训练框架对齐。
第二是模型层。这部分要解决"用什么模型检测"的问题。目标检测领域现在主流就是YOLO系列,这套系统里我分别用YOLOv5、v8、v11、v12做了训练和对比,核心原因后面细说。
第三是后端服务层。模型训练完只是得到了一个权重文件.pt,它本身不能对外提供服务。需要把它封装成接口,接收上传图片或视频帧,调用模型推理,然后返回结果。这里选择Django来做,理由在后面单独展开。
第四是前端展示层。一个演示系统必须有可视化的界面,比如上传图片、选择本地文件、开启摄像头检测实时画面,让使用者能直观看到检测框和置信度。
整个系统的数据流是:前端页面拿到图片 -> 通过HTTP请求发送给Django后端 -> 后端加载YOLO模型推理 -> 标出检测框 -> 把标注后的图片转成Base64编码返回给前端 -> 前端把图片渲染出来。
1.2 YOLOv5/v8/v11/v12到底怎么选
YOLO系列版本非常多,很多人选型时很纠结。我把自己实际测下来的感受做个对比,方便大家参考。
从架构差异来看,YOLOv5属于上一代经典模型,基于anchor机制,结构是CSPDarknet,训练和部署文档非常完善,很多老项目还在用。YOLOv8用anchor-free设计替代了anchor机制,骨干网络引入C2f结构,逻辑更简洁,训练时少了很多调anchor的超参数,对新手更友好,可以说是现在做目标检测的默认选项。YOLOv11在v8基础上更新了C3k2模块,并优化了颈部网络,在小目标检测上有一定改善,火焰烟雾这类目标有时候早期火焰区域很小,用v11会有一定帮助。YOLOv12是最新加入的版本,引入了区域注意力机制(Area Attention),骨干网络改成R-ELAN结构,精度上确实比前几代更高,但模型复杂度和推理耗时也上去了,环境依赖上偶尔需要额外的编译步骤。
| 模型版本 | 核心结构 | 训练/部署难度 | 推理速度 | 精度表现 | 适合场景 |
|---|---|---|---|---|---|
| YOLOv5 | anchor-based,CSPDarknet | 低,资料成熟 | 快 | 中等 | 老项目维护、低配环境 |
| YOLOv8 | anchor-free,C2f | 低,API友好 | 较快 | 较高 | 首选推荐,通用场景 |
| YOLOv11 | C3k2模块升级 | 低,兼容性好 | 较快 | 更高 | 中小目标、精度优先 |
| YOLOv12 | R-ELAN + 注意力机制 | 中,依赖稍复杂 | 中等 | 最高 | 精度演示、前沿对比 |
我的实际建议是:如果你是第一次做这类系统,直接选YOLOv8,因为它训练代码、文档、部署样例是最丰富的,遇到问题最容易搜到解决方案。如果答辩时需要展示"对多版本模型做过对比实验",可以训练一个v11做精度对比。让系统支持多模型切换其实很简单,后端接口加一个 model_name 参数即可,这个在后端部署部分会说明。
2. 火焰烟雾数据集准备与模型训练实操
2.1 数据集结构与标注格式
训练数据集是整个系统的地基。公开的火焰烟雾数据集其实不少,但质量参差不齐。我这套系统里用的是整理过的火焰、烟雾合并数据集,包含室内火灾、室外森林火灾、夜间火焰、工厂烟雾、隧道烟雾等常见场景,总计约2000张图片。
这里重点说一下标注格式。YOLO系列模型统一使用txt格式的标注文件,每张图片对应一个同名的txt文件,每一行表示一个目标,格式是:
类别编号 归一化中心点x 归一化中心点y 归一化宽度 归一化高度比如一张图片的宽是1920像素、高是1080像素,一个火焰目标框左下角在(500, 300)、右上角在(900, 700),换算后就是中心点(700, 500)除以宽高得到(0.3646, 0.4630),宽400高400归一化后是(0.2083, 0.3704),对应的标注行就是:
0 0.3646 0.4630 0.2083 0.3704我用的是两类标注:类别0表示fire火焰,类别1表示smoke烟雾。如果你的场景是室内火灾为主、烟雾和火焰往往同时出现,建议两类都保留,让模型同时学习两种目标的特征,这样比只检测火焰要好,因为很多火灾初期烟雾往往先于明火出现。
有个特别重要的实操经验必须强调:火焰和烟雾的标注边界和普通目标不一样。普通目标比如人、车,边界清晰,框子贴着物体画就行。但火焰边缘是动态变化的,标注时不要试图把每一丝火光都框进去,框住明亮火焰的主体部分就好。烟雾是半透明的、形状不定,标注时框住密度较高、肉眼清晰可见的主烟团,不要把大面积稀薄烟气都画进去,否则模型训练时学到的特征会很杂,反而降低检测精度。
数据划分我建议按8:1:1的比例划分为训练集、验证集、测试集,并且划分的时候要注意同场景的图片不要都跑到同一个集合里,比如同一个房间不同角度的照片要均匀分配。目录结构按YOLO官方约定就行:
dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yamldata.yaml文件内容如下:
train: dataset/images/train val: dataset/images/val test: dataset/images/test nc: 2 names: ['fire', 'smoke']2.2 训练参数配置与训练命令
数据集准备好之后,训练这一步就不复杂了。以YOLOv8为例,Ultralytics提供了一体化的命令行工具,直接执行:
yolo detect train data=data.yaml model=yolov8n.pt epochs=100 imgsz=640 batch=16 device=0参数说明一下:model=yolov8n.pt表示加载预训练权重,n是nano版本,模型最轻量,显存占用小,适合快速验证。如果你的机器显存够用,可以换成yolov8s.pt或yolov8m.pt,精度会更高一些。训练epochs设为100,输入图片尺寸imgsz设为640,这是YOLO系列最常用的默认尺寸,兼顾检测精度和速度。
如果是YOLOv5,用的是它的官方仓库,训练命令有点不一样:
python train.py --img 640 --batch 16 --epochs 100 --data data.yaml --weights yolov5s.ptYOLOv11和v12也走Ultralytics命令,基本和v8一致,只要把权重文件换成对应的yolov11n.pt、yolov12n.pt即可。
有几个训练参数值得单独说。第一是batch size。如果训练途中报 CUDA out of memory,把batch降到8甚至4,同时可以考虑把imgsz从640降到512。火焰烟雾检测对分辨率其实很敏感,因为早期火焰可能占画面比例很小,imgsz过低会漏检严重。第二是训练轮数。100轮对于一个两千数据量的场景通常够用,但每次训练完一定要看results.png里的loss曲线,如果val/loss还在明显下降,说明没收敛,要继续加大epochs到150甚至200。第三是数据增强。YOLO默认开启了Mosaic、随机翻转、HSV扰动等增强策略,火焰烟雾场景下有个注意点:HSV颜色扰动虽然能增加模型对不同光照环境的适应能力,但辐值和饱和度扰动太大会让火焰变成奇怪的紫色、绿色,反而学不到真实的火焰颜色特征。如果发现训练出来的模型对红色物体误报高,优先检查这一项。
训练完成后,在训练输出目录runs/detect/train/下会生成best.pt和last.pt两个权重文件。last.pt是最后一轮的权重,best.pt是验证集上表现最好的权重,部署时一定要用best.pt。一般还会看验证集上的mAP指标,mAP@0.5表示IoU阈值取0.5时的平均精度均值,火焰烟雾二分类场景正常情况应该能达到0.85以上,低于0.7说明数据或训练参数有问题,不建议直接拿去部署。
3. Django Web部署与系统集成
3.1 为什么用Django而不是Flask或FastAPI
模型训练完,接下来就是工程化部署。选Django,我是做过对比的。
Flask确实轻量,代码少,上手快,但它只有最基本的请求路由和视图函数,没有数据库ORM、没有Admin后台、没有用户认证体系,想做一个"成套系统"需要自己拼装很多东西。FastAPI性能好,自动生成接口文档,但团队协作和传统开发模式上Django的老牌生态更扎实。Django自带ORM、Admin后台、表单处理、模板引擎、静态文件管理,对于一个需要展示"前端页面 + 后端接口 + 数据库 + 算法模型"的完整系统来说,Django一套就能串起来,做毕设或者课题演示时系统架构图也更好画。
用Django还有个隐形好处:部署到云服务器时,Django应用可以用uwsgi或gunicorn配合Nginx部署,教程非常多,踩坑概率低。对一个检测系统来说,算法部分反而是核心,硬件环境搭建别在Web框架上浪费太多时间才是正道。
3.2 模型推理服务封装与API接口实现
部署的第一步是把模型推理能力封装成一个独立的模块,避免在视图函数里直接堆模型代码。我会单独建一个model_loader.py文件,用单例模式加载模型,确保Django服务启动后模型只加载一次,而不是每个请求都加载一遍权重。
# detect_app/model_loader.py import os from threading import Lock from ultralytics import YOLO _lock = Lock() _detectors = {} def get_detector(model_name="best.pt"): with _lock: if model_name not in _detectors: weights_path = os.path.join(os.path.dirname(__file__), "..", "weights", model_name) _detectors[model_name] = YOLO(weights_path) return _detectors[model_name]将模型加载放在锁机制里是为了防止多线程并发请求时重复加载。Django默认的多线程模型下,这个单例锁非常有必要。
接着写视图处理函数。这里做图片上传检测接口,接收前端传过来的图片文件,用OpenCV解码,送入模型推理,把结果画到图上,再编码成Base64字符串返回JSON给前端。
# detect_app/views.py import base64 import cv2 import numpy as np from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from .model_loader import get_detector @csrf_exempt @require_POST def detect_image_api(request): try: img_file = request.FILES.get("image") if img_file is None: return JsonResponse({"error": "no image uploaded"}, status=400) img_bytes = img_file.read() nparr = np.frombuffer(img_bytes, np.uint8) img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) if img is None: return JsonResponse({"error": "invalid image"}, status=400) detector = get_detector() results = detector.predict(img, conf=0.25, imgsz=640) boxes_result = [] for box in results[0].boxes: xyxy = box.xyxy.tolist()[0] conf = float(box.conf[0]) cls = int(box.cls[0]) label = f"{results[0].names[cls]} {conf:.2f}" x1, y1, x2, y2 = map(int, xyxy) color = (0, 0, 255) if cls == 0 else (0, 165, 255) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, color, 2) boxes_result.append({ "label": results[0].names[cls], "conf": round(conf, 3), "xyxy": xyxy }) _, buffer = cv2.imencode(".jpg", img) img_base64 = base64.b64encode(buffer.tobytes()).decode("utf-8") return JsonResponse({"image_base64": img_base64, "boxes": boxes_result}) except Exception as e: return JsonResponse({"error": str(e)}, status=500)注意代码里火焰类别用红色框(0,0,255),烟雾类别用黄色框(0,165,255),前端展示时视觉区分更明显。conf=0.25是置信度阈值,低于这个分数的检测结果会被过滤,这个值可以根据实际误报情况调整,误报高就调高到0.3或0.35,漏检明显就调低到0.15。
URL路由配置很简单:
# detect_app/urls.py from django.urls import path from . import views urlpatterns = [ path("api/detect/", views.detect_image_api, name="detect_image"), ]然后在项目主路由urls.py里include进去。这样一个最基础的后端检测接口就完成了。
3.3 前端交互页面与浏览器摄像头接入
前端页面我用了一个单页HTML,页面顶部是标题栏,中间分成左右区域:左侧放图片上传控件和摄像头按钮,右侧显示检测结果图。核心交互是Ajax请求,避免整页刷新。
图片上传的HTML核心逻辑:
document.getElementById("uploadBtn").addEventListener("click", function() { const fileInput = document.getElementById("imageInput"); if (fileInput.files.length === 0) { alert("请先选择图片"); return; } const formData = new FormData(); formData.append("image", fileInput.files[0]); fetch("/api/detect/", { method: "POST", body: formData }) .then(res => res.json()) .then(data => { if (data.image_base64) { const img = document.getElementById("resultImage"); img.src = "data:image/jpeg;base64," + data.image_base64; img.style.display = "block"; } }); });摄像头实时检测稍微麻烦一些,我用的是浏览器的navigator.mediaDevices.getUserMedia接口获取摄像头画面,绘制到canvas上,然后定时截取canvas图像发送到后端检测接口,检测完把结果图再渲染到页面上。这里有个性能点必须注意:如果每帧都发送检测,后端推理速度和网络传输会成为瓶颈,实际使用中我会用每秒检测2到3帧的频率,也就是大约每300到500毫秒截取一次帧,这样既能做到准实时,又不会让页面卡死。
let timer = null; document.getElementById("cameraBtn").addEventListener("click", function() { navigator.mediaDevices.getUserMedia({ video: true }) .then(stream => { const video = document.getElementById("cameraVideo"); video.srcObject = stream; video.play(); if (timer) clearInterval(timer); timer = setInterval(captureFrame, 400); }); }); function captureFrame() { const video = document.getElementById("cameraVideo"); const canvas = document.getElementById("cameraCanvas"); canvas.width = video.videoWidth; canvas.height = video.videoHeight; canvas.getContext("2d").drawImage(video, 0, 0, canvas.width, canvas.height); canvas.toBlob(blob => { const formData = new FormData(); formData.append("image", blob, "frame.jpg"); fetch("/api/detect/", { method: "POST", body: formData }) .then(res => res.json()) .then(data => { if (data.image_base64) { const img = document.getElementById("resultImage"); img.src = "data:image/jpeg;base64," + data.image_base64; } }); }, "image/jpeg"); }Django开发调试模式下跑起来很简单,项目根目录执行:
python manage.py runserver 0.0.0.0:80000.0.0.0表示监听所有网卡,这样局域网内其他设备可以通过http://你的IP:8000访问演示页面。做演示时直接拿手机或者另一台电脑访问,效果比在一台机器上干巴巴展示截图好很多。
4. 运行演示实测与常见问题排查
4.1 演示流程与实测效果
系统跑通后,演示流程我总结了一个固定套路,按这个顺序走很少翻车:
第一步启动后端服务,先在命令行确认Django启动无报错,模型文件路径正确。第二步浏览器打开系统页面,先上传一张单场景的火焰图片,检查检测框是否准确、置信度是否高,这个作为基础验证。第三步换一张烟雾场景图片,确认烟雾也能检测出来。第四步开启摄像头实时检测,可以用手机打开一张火焰图片对着摄像头晃动,或者现场点一张纸制造一点安全烟雾(注意要在符合安全规范的条件下),体验实时检测帧率。
我用YOLOv8n模型实测,单张640x640图片在GTX级别的显卡上,推理耗时大约在10到20毫秒,加上前后处理,整体响应时间小于100毫秒,完全满足演示需求。YOLOv12精度更高但推理耗时约增长30%到50%,不过作为对比展示还是值得跑一下的。
实测下来,白天自然光场景下,火焰的检测置信度普遍在0.85以上;烟雾目标由于半透明特性,置信度会低一些,一般在0.6到0.8之间。室外远距离的小火焰目标,v8偶尔会漏检,v11和v12的表现明显好一截。这也是为什么我在系统里保持了多模型切换能力——可以让使用者自己对比体验。
4.2 常见问题排查速查表
实际操作中遇到的问题各种各样,我整理了一张排查表,按频率排序:
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 训练到一半CUDA out of memory | batch过大或imgsz过大 | batch降到8/4,imgsz降到512,开启AMP混合精度 |
| 训练很多轮loss都不降 | 学习率不当、标注数据有错 | 学习率调到0.001,检查labels文件夹里的标注框是否错位 |
| 检测接口返回500错误 | 模型路径不对或图片解码失败 | 先用单独的Python脚本测试模型推理,再接入Django |
| 前端上传中文文件名报错 | 编码处理不规范 | 后端用uuid重命名上传文件,路径中不要出现中文 |
| 页面开启摄像头后白屏 | 浏览器权限未开或HTTP环境限制 | 确认浏览器摄像头权限,页面改用localhost或IP访问 |
| 检测结果误报高、把红灯橙色灯当火焰 | 场景数据不足,conf阈值太低 | 加入负样本训练,conf提高到0.3以上 |
| 烟雾检测框特别小或漏检 | 目标太小,分辨率不够 | imgsz调到960,或换大模型如yolov8m |
| Web页面加载模型首帧特别慢 | 模型在第一个请求时才加载 | 项目启动时预热模型,直接在Django启动逻辑里手动调用一次get_detector() |
这里有几个排查思路值得单独展开。
训练阶段OOM最让人头疼,其实不用急着换硬件。YOLOv8和v11默认开启了AMP(自动混合精度),会在保证精度的前提下显著降低显存占用,确认自己的训练命令有没有带amp=True。如果再不行,把imgsz从640降到512是一个相对无损的降级手段,配合batch=4基本能跑起来。
Django检测接口报500,我的排查顺序是:先在Django项目目录里开一个Python交互环境,手动调用get_detector().predict()测一张图,如果模型本身有问题,这一步就能发现。如果模型没问题,问题多半出在图片读取上,重点检查cv2.imdecode解码是否返回None。还有一类隐蔽问题是对上传文件大小没做限制,几MB的大图解码和推理会非常慢,前端表现为长时间无响应,可以在API里做图片压缩处理,先等比缩放到最长边小于1280像素再去推理。
摄像头检测卡顿的问题,我用了一个比较实用的小策略:不直接降低帧率,而是维护一个"正在请求中"标记,上一次请求没返回前不发送下一帧,这样相当于按需取帧,既避免画面闪烁又不会堆积请求。
4.3 独家避坑经验分享
最后分享几个常规文档里不会写的经验。
第一个是关于数据集的负样本问题。火焰烟雾检测和普通目标检测有一点很大的不同:现实场景里跟火焰颜色相似的物体太多了。夕阳、路灯、红色车尾灯、电焊火花、甚至橙色的墙面,都有可能在某个角度下被模型误判为火。我第一次训练时纯用火灾正样本,测试时发现对着一个红色广告牌反复误报,置信度甚至到了0.7。后来我去网上收集了一批包含红色物体、橙色灯光但完全没有火灾的负样本,把它们标注成空标签文件,混合进训练数据里,误报率一下就降下来了。负样本不需要很多,大概占总数10%到20%就行。
第二个是关于烟雾检测的"看不见的手"。烟雾本身是半透明的,如果只是简单叠加在背景上,模型非常容易只学到背景纹理而不是烟本身。我给训练集做增强时,特意把一部分烟雾图片调低透明度叠加到不同的室内背景上,生成了一批合成样本。这种做法看起来原始,但对提升烟雾检测鲁棒性效果显著,原因是模型被迫学习"前景和背景之间的关系",而不是死记硬背某个固定背景。
第三个是关于演示部署时模型预热。我第一次给导师演示时,启动Django后第一个请求等了将近两秒才出结果,非常尴尬。原因很简单:模型是懒加载的,第一个请求进来时才加载权重、初始化CUDA。解决方式是在Django的apps.py里重写ready()方法,或者直接在启动脚本里调用一次get_detector()预先加载模型,让服务一旦就绪、模型就已经在显存里待命了。
第四个是版本兼容问题。YOLOv5的权重文件结构跟v8/v11/v12不同,不能混用,如果你在系统里保留了多模型切换,后端加载时一定要绑定对应的Ultralytics版本。我在requirements.txt里固定了ultralytics==8.x.x,避免之后更新库导致旧权重读取失败。
这套系统如果想继续扩展,后续方向其实很多:比如接入视频流做持续检测、加入火焰区域面积占比报警逻辑、对接消息推送系统、做模型量化部署到边缘设备等。就我目前的体验来说,基于深度学习的火焰烟雾检测方案,在准确率和实时性上已经能够满足大部分消防监控场景的需求,核心难点往往不在模型本身,而在于数据质量、工程封装和场景适配这三件事上。做一遍完整的训练、部署、演示流程,远比零散看论文调代码更能理解目标检测项目到底是怎么落地的。