基于深度学习的火焰烟雾检测系统:YOLOv8训练到Django Web部署全流程 最近在帮朋友调试一个火灾监控方向的深度学习项目核心是一套基于深度学习的火焰烟雾检测系统模型侧把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结构精度上确实比前几代更高但模型复杂度和推理耗时也上去了环境依赖上偶尔需要额外的编译步骤。模型版本核心结构训练/部署难度推理速度精度表现适合场景YOLOv5anchor-basedCSPDarknet低资料成熟快中等老项目维护、低配环境YOLOv8anchor-freeC2f低API友好较快较高首选推荐通用场景YOLOv11C3k2模块升级低兼容性好较快更高中小目标、精度优先YOLOv12R-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 datadata.yaml modelyolov8n.pt epochs100 imgsz640 batch16 device0参数说明一下modelyolov8n.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指标mAP0.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_namebest.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}, status400) 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}, status400) detector get_detector() results detector.predict(img, conf0.25, imgsz640) 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)}, status500)注意代码里火焰类别用红色框(0,0,255)烟雾类别用黄色框(0,165,255)前端展示时视觉区分更明显。conf0.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, namedetect_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 memorybatch过大或imgsz过大batch降到8/4imgsz降到512开启AMP混合精度训练很多轮loss都不降学习率不当、标注数据有错学习率调到0.001检查labels文件夹里的标注框是否错位检测接口返回500错误模型路径不对或图片解码失败先用单独的Python脚本测试模型推理再接入Django前端上传中文文件名报错编码处理不规范后端用uuid重命名上传文件路径中不要出现中文页面开启摄像头后白屏浏览器权限未开或HTTP环境限制确认浏览器摄像头权限页面改用localhost或IP访问检测结果误报高、把红灯橙色灯当火焰场景数据不足conf阈值太低加入负样本训练conf提高到0.3以上烟雾检测框特别小或漏检目标太小分辨率不够imgsz调到960或换大模型如yolov8mWeb页面加载模型首帧特别慢模型在第一个请求时才加载项目启动时预热模型直接在Django启动逻辑里手动调用一次get_detector()这里有几个排查思路值得单独展开。训练阶段OOM最让人头疼其实不用急着换硬件。YOLOv8和v11默认开启了AMP自动混合精度会在保证精度的前提下显著降低显存占用确认自己的训练命令有没有带ampTrue。如果再不行把imgsz从640降到512是一个相对无损的降级手段配合batch4基本能跑起来。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里固定了ultralytics8.x.x避免之后更新库导致旧权重读取失败。这套系统如果想继续扩展后续方向其实很多比如接入视频流做持续检测、加入火焰区域面积占比报警逻辑、对接消息推送系统、做模型量化部署到边缘设备等。就我目前的体验来说基于深度学习的火焰烟雾检测方案在准确率和实时性上已经能够满足大部分消防监控场景的需求核心难点往往不在模型本身而在于数据质量、工程封装和场景适配这三件事上。做一遍完整的训练、部署、演示流程远比零散看论文调代码更能理解目标检测项目到底是怎么落地的。