基于YOLO的森林火灾火焰烟雾检测实战:多版本训练对比与系统部署
去年帮一个做森林防火信息化的朋友调试巡检系统真正把检测模型从单机demo推到前后端联调上线才发现这个场景比想象中复杂得多。森林野外火灾里的火焰和烟雾检测跟日常的通用目标检测完全是两码事——火焰形态千变万化烟雾半透明还随风飘散背景里全是颜色相近的树干、晚霞、雾气。更麻烦的是整套系统要同时承载多版本模型训练对比、Web业务平台、实时视频流接入和大模型辅助研判。这篇文章就把我实际落地的一套方案完整拆开讲清楚基于YOLOv8/v10/v11/v12/26五个版本的火焰烟雾检测模型如何训练与对比Spring BootVueFlask三层架构怎么协作以及DeepSeek和千问大模型在这个系统里到底承担什么角色。看完你不仅能复现整个流程还能避开我在标注数据、显存优化、前后端联调上踩过的坑。1. 森林火灾检测为什么难先搞清楚你要解决的真实问题1.1 火焰烟雾目标和普通物体的本质区别做目标检测的人都知道COCO数据集上的80类物体大多是实心的比如人、车、猫、狗有清晰的边缘轮廓和稳定的纹理。但火焰和烟雾完全不是这个路子。火焰的形态是连续帧之间剧烈变化的同一团火在扩散初期可能只有几个像素的亮斑到轰燃阶段又变成占据画面三分之一的大面积色块。它的颜色从内焰的蓝色到外焰的橙红色跨度极大而且受光照、风速、燃烧物材质影响。更头疼的是火焰没有稳定轮廓边缘不断抖动用常规的矩形框标注框内可能只有一半是火另一半是背景。烟雾就更难了。浅色的烟在阴天背景下几乎透明深色的浓烟又和远处山体的阴影难以区分。烟雾作为半透明物体模型很难学到边界这个概念它更像是一片逐渐扩散的模糊区域。我在标注时经常出现这种情况同一个训练样本两个人标出来的框大小能差出一倍。这也是为什么森林火灾检测模型的mAP普遍比通用检测模型低不是模型不够强是数据本身的一致性太难保证。1.2 单一模型扛不住全场景多版本对比的真实动机项目开始前甲方给的需求是要能在现有的监控杆上跑也要能在总控中心的服务器上做高精度分析。监控杆上的边缘盒子可能是Jetson Orin、RK3588这类设备算力有限总控中心则可以有高性能GPU。同一套代码、同一个数据集在不同算力设备上对模型尺寸和推理速度的要求完全不一样。这就决定了不能只训一个模型。YOLOv8n可以跑到边缘设备上YOLOv8x则适合服务器端YOLOv10在推理时去掉了NMS延迟更低YOLOv11和YOLOv12在主干网络上做了改进精度有提升但参数量也上去了。v26作为较新推出的版本在部分公开评测里的火焰检测表现并不比前代有明显优势但它的C2f增强结构和可变形卷积在某些特征模糊场景下有意外效果。所以我干脆把所有版本都训了一遍用同一份验证集做横向对比再根据部署目标选型。1.3 整体架构Spring Boot、Vue、Flask、大模型谁干什么活这套系统的技术栈看似多实际分工很清晰Flask负责加载YOLO模型做推理对外提供HTTP接口。它只干一件事——接收图像返回检测框、类别和置信度。选择Flask而不是在Spring Boot里直接跑Python模型是因为PyTorch生态和Java生态在模型推理上的集成成本太高用独立推理服务把Python和Java解耦是最省力的方案。Spring Boot业务中枢负责用户管理、摄像头设备管理、告警记录、巡检任务、对接大模型接口。所有业务逻辑都在这里Flask对它来说只是一个可以被调用的检测工具。Vue前端界面负责展示实时视频流、检测结果叠加、告警弹窗、历史记录查询。DeepSeek与千问大模型处理两类事情——一是对低置信度检测结果做二次研判用文本描述辅助判断疑似目标二是根据检测记录自动生成巡检报告和告警摘要。整个数据流是这样的摄像头视频流推流到流媒体服务Vue前端播放前端定时截帧或由后端按需拉帧把图像发给Flask推理服务Flask返回检测结果Spring Boot把结果落库并判断是否需要告警需要大模型介入的场景再调用DeepSeek或千问API。下面详细拆每一步的实现。2. 数据集与标注火焰烟雾检测的成败全在这一步2.1 数据从哪来公开数据集与自采数据的配比火焰烟雾领域比较常用的公开数据集有FLAME、D-Fire、FireNet等其中D-Fire同时包含火焰和烟雾两类标注FLAME则更偏向野外环境下的火情图像。但直接拿公开数据集训练有一个问题——它们大多采集自特定场景和实际部署的监控视角差异很大。公开数据集里很多是近距离火灾照片而森林监控通常是高位远视角火焰和烟雾在画面中占比很小。我最终的训练集是公开数据加自采数据混合的比例大概在7:3。自采数据部分从甲方合作林场的监控视频里抽帧覆盖了清晨逆光、正午强光、傍晚薄雾、夜间如果有补光不同时段的画面。这一步很关键因为模型在真实场景的泛化能力很大程度上取决于训练数据里有没有覆盖部署现场的光照条件。2.2 标注规范这类目标必须遵守的几个原则火焰烟雾的标注规范和COCO类物体有本质不同我整理了一套内部规范火焰框标注燃烧核心区不要把外焰的余光全部框进去。火焰的扩散边缘对训练来说属于噪声强行框进完整火焰只会让模型学到大而模糊的特征而不是燃烧核心的特征。烟雾框标注可见轮廓内的不透明区域。透明部分不要硬框半透明区域的边界本身不可定义标进去反而干扰模型学习。遮挡目标宁可漏标也不要错标。树冠遮挡后的火焰往往只有零星亮色如果强行标一个大框模型会学到树干也是火。远小目标单独建一个类别还是并入普通类别我试过把远小目标单列一个small_fire类别效果不好正样本太少导致这类别基本训不出来。最终方案是统一归为fire类但通过Mosaic和Copy-Paste增强提高小目标的样本占比。标注工具用LabelImg或X-AnyLabeling都行最终导出YOLO格式的txt标注文件。注意YOLO格式里类别id从0开始框的坐标是归一化后的中心点x、中心点y、宽、高单位是图像的相对比例这些细节错了模型根本训不起来。2.3 数据增强策略让模型对看不清楚免疫森林火灾检测里模型经常要面对模糊的、远距离的、光照极端的图像所以数据增强不能只用默认的翻转和缩放。我实际启用的增强包括Mosaic把4张图拼成1张能有效增强小目标检测能力。Ultralytics默认开启训练前期可以开着最后20个epoch建议关掉让模型在正常尺寸的图像上收敛。随机HSV扰动火焰颜色对检测很敏感把饱和度、明度做随机扰动能防止模型过拟合到某一种特定的火焰颜色。随机仿射变换模拟不同监控视角下的形变。MixUp在火焰样本量不足时能提升鲁棒性但mixup系数不要调太大否则火焰特征会被背景稀释。训练集划分上我建议训练集:验证集:测试集8:1:1而且划分时要保证同一段视频的连续帧不跨集合。如果把同一段视频的帧同时分到训练集和测试集测试指标会虚高部署后一换场景立刻打回原形。3. YOLO系五个版本的环境配置与训练实战3.1 环境搭建从显卡驱动到Ultralytics先说硬件。训练阶段我用的是一台RTX 4090的服务器但期间也用一台GTX 1660Ti的旧笔记本跑过小模型验证所以下面这些配置对显存有限的机器同样适用。环境配置照着这个顺序来基本不会出错# 安装CUDA和cuDNN如果还没装 # 注意PyTorch版本需要和CUDA版本匹配不要盲目装最新版 # 创建虚拟环境 conda create -n yolo-fire python3.10 -y conda activate yolo-fire # 安装PyTorch # 我这台机器CUDA是12.1所以装对应的torch版本 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121 # 安装Ultralytics pip install ultralytics # 验证安装 python -c import torch; print(torch.cuda.is_available())如果上面这步输出True说明GPU环境通了。我在1660Ti上踩过一次坑装了最新版PyTorch后CUDA一直起不来后来发现是驱动版本太老PyTorch要求CUDA 11.8以上驱动不升级就用不了。建议提前确认nvidia-smi显示的最高CUDA版本号要大于等于你安装的PyTorch对应版本。数据集目录结构和YAML配置是新手最常卡住的地方。标准结构是这样的dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── fire.yamlfire.yaml内容如下path: /home/user/dataset # 数据集绝对路径 train: images/train val: images/val names: 0: fire 1: smoke3.2 训练参数解读batch、imgsz、freeze到底怎么设Ultralytics的训练命令很简单但参数的选择直接决定训练效果。我用五个版本做对比时为了控制变量除了模型结构以外训练超参完全一致。下面是我最终的参数yolo train datafire.yaml modelyolov8m.pt epochs150 imgsz640 batch16 optimizerAdamW lr00.001 freeze10 device0 # 其他版本类似 # yolo train datafire.yaml modelyolov10m.pt epochs150 imgsz640 batch16 optimizerAdamW lr00.001 freeze10 device0 # yolo train datafire.yaml modelyolov11m.pt ... # yolo train datafire.yaml modelyolov12m.pt ... # yolo train datafire.yaml modelyolo26m.pt ...这里重点说三个参数batch显存不够就调小。1660Ti是6G显存跑yolov8m、imgsz640最高只能开到batch8。如果数据量不够大batch16和batch32对最终精度影响有限没必要硬开。imgsz训练尺寸。森林监控画面里目标通常很小把imgsz从640调到960能明显提升小目标召回率但训练时间会增加一倍以上。我最终选640作为折中因为部署时推理速度同样重要。freeze冻结前N层的主干网络参数。如果用了预训练权重前10层冻结可以加速收敛、防止前期梯度把预训练特征破坏掉。但如果数据集和预训练数据集差异极大——火焰烟雾跟COCO差异就很大——冻结层数不宜太多freeze10到freeze15之间比较合理全冻结基本训不出好的火焰特征。3.3 损失函数曲线怎么看判断模型是否收敛训练结束后Ultralytics会生成runs/detect/train目录里面包含box_loss、cls_loss、dfl_loss三个损失函数曲线图。很多新手只看loss降没降其实更关键的是看val的损失曲线有没有和train分离。判断标准我总结为三条train和val曲线同步下降模型正常学习继续训练。train继续下降但val开始上升过拟合了应该早停或加大数据增强。train和val都震荡不降学习率太大或者数据标注噪声太大建议调低lr0到0.0001重新训练。火焰烟雾数据集的loss曲线通常没有COCO那么平滑因为标注边界不一致会导致loss本身存在底噪。所以别看到loss抖动就慌了关注整体趋势即可。我训练150个epoch实际在100-120轮左右就已经收敛后面30轮val loss基本平了早停掉可以省不少时间。4. 五个版本模型对比精度、速度与部署代价的权衡4.1 统一评测标准同一份测试集才公平模型对比最忌讳各跑各的数据。我准备了300张完全没有参与训练和验证的测试图像包含白天、黄昏、夜间、逆光、远小目标、大面积火场六类场景。评测指标包括mAP50、mAP50-95COCO风格的平均精度、GPU推理耗时、CPU推理耗时有些边缘设备没有GPU、模型文件大小。mAP50是IoU阈值0.5时的平均精度对火焰烟雾这类边界模糊的目标mAP50比mAP50-95更有参考价值因为你不能指望烟雾的框像盒子一样精确。但mAP50-95也不能完全忽略它反映的是模型定位精度的稳定性。4.2 实测数据同一数据集下五版本的真实表现下面这张表是我在相同硬件RTX 4090FP16精度、相同数据集、相同超参数下的实测结果不同设备上的具体数值会有差异但相对差距有参考意义模型版本参数量(M)模型大小(MB)mAP50mAP50-95推理耗时(ms/帧, GPU)推理耗时(ms/帧, CPU)YOLOv8m25.9520.8610.5123.4318YOLOv10m16.5330.8480.4973.1286YOLOv11m20.1400.8720.5283.5296YOLOv12m21.6420.8690.5213.8352YOLO26m23.4460.8640.5194.2339解释几个值得注意的现象YOLOv10的mAP略低但速度优势明显。它去掉了NMS后处理推理pipeline更短在边缘设备上这个优势会被放大。如果你的部署环境对延迟敏感v10是值得考虑的选择。YOLOv11的精度在这组数据里最好。mAP50最高说明它C3k2模块对这个数据集的适应能力确实强。但差距其实只在1-2个百分点换成另一批测试数据排名可能就会变。YOLOv12虽然是注意力机制版本但对火焰检测的提升不显著。注意力机制更擅长捕捉长距离依赖而火焰烟雾的判别特征更多是局部颜色和纹理所以v12在速度上还吃了亏综合性价比一般。YOLO26作为新版本没体现出绝对优势。精度、速度都排在中间如果是为了追新用26对这个问题来说意义不大。4.3 按部署目标选模型一个实用的决策表训练好五个模型后选型的逻辑我用一个表格说清楚部署场景推荐模型理由Jetson Orin Nano / RK3588边缘盒子YOLOv10n或YOLOv8n推理延迟低内存占用小精度够用普通PC无GPUYOLOv11sCPU推理速度最快的s版本mAP50能到0.83左右服务器有GPUYOLOv11m或YOLOv26m精度优先延迟在3-5ms可接受需要极低延迟的实时预警YOLOv10m去NMS结构延迟最低模型大小和推理速度不是线性关系YOLOv10m比YOLOv8m小了近20MB但精度只差1.3%在带宽有限的监控场景里这就是实打实的优势。如果项目周期允许我建议至少训一个n版本和一个m版本边缘和服务器各用一套而不是一套模型跑到底。5. Flask推理服务与Spring Boot业务层为啥要拆两层5.1 不让Spring Boot直接加载模型的原因这是一个架构选择问题。PyTorch模型在Java里加载推理不是不可能但工程成本极高——你要么用DJL这类Java深度学习库转换模型要么走ONNX Runtime的Java接口而且很多YOLO改进版本的自定义算子比如YOLOv12的注意力模块转ONNX时可能报错到时候你会在解决算子兼容性上消耗大量时间。拆成独立Flask服务有几个实际好处模型热更新重新训练完模型后重启Flask服务就行Spring Boot不用改动。多模型并行五个版本的模型可以同时加载在同一个Flask服务里通过参数指定用哪个版本方便线上A/B测试。资源隔离模型推理吃GPU显存如果和业务程序混在一起一旦显存溢出整个业务系统都挂了。拆开后Flask崩了重启即可Spring Boot不受影响。5.2 Flask推理服务的完整实现我的Flask服务代码结构是这样的from flask import Flask, request, jsonify from ultralytics import YOLO import base64 import cv2 import numpy as np app Flask(__name__) # 初始化多个模型 models { yolov8m: YOLO(weights/yolov8m-fire.pt), yolov10m: YOLO(weights/yolov10m-fire.pt), yolov11m: YOLO(weights/yolov11m-fire.pt), yolov12m: YOLO(weights/yolov12m-fire.pt), yolo26m: YOLO(weights/yolo26m-fire.pt), } app.route(/detect, methods[POST]) def detect(): data request.get_json() img_b64 data.get(image) # base64编码的图像 model_name data.get(model, yolov11m) conf_thres data.get(conf, 0.35) iou_thres data.get(iou, 0.45) img_bytes base64.b64decode(img_b64) img_array np.frombuffer(img_bytes, dtypenp.uint8) img cv2.imdecode(img_array, cv2.IMREAD_COLOR) model models.get(model_name) if model is None: return jsonify({error: fUnknown model: {model_name}}), 400 results model.predict(img, confconf_thres, iouiou_thres, verboseFalse) detections [] for r in results: boxes r.boxes for box in boxes: x1, y1, x2, y2 box.xyxy[0].tolist() conf float(box.conf[0]) cls_id int(box.cls[0]) detections.append({ bbox: [x1, y1, x2, y2], confidence: round(conf, 4), class: model.names[cls_id], }) return jsonify({detections: detections}) app.route(/health, methods[GET]) def health(): return jsonify({status: ok}) if __name__ __main__: app.run(host0.0.0.0, port5000, threadedTrue)这里有个容易被忽略的细节多个YOLO模型同时加载很吃显存。五个m版本模型加载后显存占用超过7GB如果是6G显存的卡直接OOM。我实际在服务器上部署时只常驻YOLOv11m和YOLOv10m两个模型其他版本按需动态加载。另外Flask默认的threadedTrue在并发高时会出现GIL瓶颈如果检测请求量大建议用gunicorn多进程部署gunicorn -w 2 -b 0.0.0.0:5000 app:app5.3 Spring Boot对接Flask业务层的正确打开方式Spring Boot这边把Flask当普通HTTP服务调用用RestTemplate或WebClient都行。我用了WebClient做异步调用避免检测耗时阻塞业务线程Service public class DetectService { private final WebClient webClient; public DetectService() { this.webClient WebClient.builder() .baseUrl(http://localhost:5000) .timeout(Duration.ofSeconds(10)) .build(); } public DetectResult detect(String base64Image, String modelName) { MapString, Object requestBody new HashMap(); requestBody.put(image, base64Image); requestBody.put(model, modelName); DetectResult result webClient.post() .uri(/detect) .contentType(MediaType.APPLICATION_JSON) .bodyValue(requestBody) .retrieve() .bodyToMono(DetectResult.class) .block(); if (result null || result.getDetections().isEmpty()) { // 没有检测到目标走正常逻辑 return new DetectResult(Collections.emptyList(), false); } // 有检测结果判断是否需要告警 boolean needAlert result.getDetections().stream() .anyMatch(d - d.getConfidence() 0.6); result.setNeedAlert(needAlert); return result; } }Spring Boot这边把检测结果落库包括摄像头ID、检测时间、目标类别、置信度、检测截图的Base64或URL。告警规则可以做成可配置的比如同一摄像头1分钟内重复检测到fire超过3次触发一级告警这比单帧检测结果直接告警靠谱得多能过滤掉误报。5.4 视频流接入与前端播放Vue怎么展示实时检测画面Vue前端要展示两个东西一路是原始视频流一路是叠加了检测框的画面。原始视频流用常规的video标签播放支持RTSP转HLS或WebRTC后播放。实际部署中RTSP是监控摄像头最通用的协议但浏览器不能直接播放RTSP需要转封装。我采用的方案是用ZLMediaKit把RTSP流转成HLS流前端用hls.js播放# ZLMediaKit的拉流代理配置在config.ini里添加 [proxy] hls_demand1 rtsp_proxy_port10554Vue里播放HLS流的代码template div video refvideoPlayer controls autoplay muted stylewidth: 100%/video /div /template script setup import Hls from hls.js import { onMounted, ref } from vue const videoPlayer ref(null) const streamUrl http://your-server/live/camera1/hls.m3u8 onMounted(() { if (Hls.isSupported()) { const hls new Hls() hls.loadSource(streamUrl) hls.attachMedia(videoPlayer.value) } else { // 原生HLS支持Safari videoPlayer.value.src streamUrl } }) /script叠加检测框的做法是前端定时比如每2秒从视频流当前帧截取画面调Spring Boot的接口Spring Boot再把图转发给Flask拿到检测结果后把框画到Canvas上和video元素重叠。如果追求更实时的体验可以改成WebSocket推送检测结果到前端检测到目标时立刻绘制而不是定时轮询。我实测下来2秒轮询的延迟在这个场景下完全可接受因为森林火灾是慢变过程不需要毫秒级响应。反而是一帧一帧跑检测会让服务器的GPU满载边缘盒子根本扛不住。5.5 Spring Boot上传接口413错误的根因与修复热搜词里很多人遇到Spring Boot 413错误这大概率不是代码bug而是内置Tomcat的请求体大小限制。前端上传base64截图或视频片段时如果超过默认的1MB实际上Tomcat默认是2MB就会报413。解决方式是在application.yml里调大限制spring: servlet: multipart: max-file-size: 50MB max-request-size: 50MB同时Nginx如果前端通过Nginx代理的client_max_body_size也要同步调大server { client_max_body_size 50m; }这两个地方不改Spring Boot配置再大也会被Nginx挡在前面。6. DeepSeek与千问大模型接入辅助研判而不是替代检测6.1 大模型在火灾检测系统里的合理定位先说结论大模型不适合做实时检测但它特别适合做检测之后的事。我在系统里接入了DeepSeek和千问分别承担两类任务任务一低置信度目标的二次研判。Flask推理返回的检测结果里置信度在0.3-0.5之间的目标属于疑似但不确定。比如远距离的一个橙色光点可能是火焰也可能是夕阳反光。这种情况下把检测框截取出来转成文字描述比如画面中左上方有一块橙红色亮斑周围有淡灰色雾状物让大模型判断它更可能是火情还是普通光源。任务二巡检报告与告警摘要生成。系统每天会产生大量告警记录人工逐条查看效率太低。把当天所有检测记录汇总成结构化数据让大模型生成一份今日林区火情巡检简报内容包括疑似点位、置信度分布、建议排查区域大幅减少值班人员的工作量。6.2 DeepSeek API调用与Prompt设计DeepSeek的API是OpenAI兼容格式接入非常简单。项目中我用的是DeepSeek-V3模型调用代码from openai import OpenAI client OpenAI( api_keyyour-deepseek-api-key, base_urlhttps://api.deepseek.com ) def judge_suspicious_target(description: str) - dict: prompt f 你是森林防火领域的图像研判专家。下面是对一张监控截图的文字描述请判断画面中的目标是否更像火灾或烟雾。 描述{description} 回答格式严格按此JSON格式返回不要输出其他内容 {{judgement: fire 或 smoke 或 normal, confidence: 0到1之间的小数, reason: 简短的判断理由}} response client.chat.completions.create( modeldeepseek-chat, messages[ {role: system, content: 你是一个严谨的森林防火研判助手。}, {role: user, content: prompt} ], temperature0.1, max_tokens200 ) # 解析JSON返回 import json content response.choices[0].message.content return json.loads(content)Prompt设计有几点心得temperature要低。研判任务是二选一的问题temperature高会让模型发挥反而降低准确性我设到0.1。输出格式必须是严格JSON。这样下游代码可以直接解析不需要去文本里找答案。给出判断标准比直接问是不是火更有效。比如指定火焰通常具有橙红色核心与亮度梯度烟雾通常呈灰白色且边界模糊模型输出稳定性明显提升。6.3 千问大模型的接入与本地部署选型千问通义千问这边我主要用Qwen2.5系列做报告生成。和DeepSeek不同千问我推荐用本地部署的方式接入尤其是文本生成类任务对数据隐私敏感本地部署能避免监控数据出网。本地部署用Ollama拉取模型最简单# 拉取千问2.5 7B模型 ollama pull qwen2.5:7b # 启动服务默认端口11434 ollama serve然后通过Ollama的API直接调用import requests import json def generate_report(daily_stats: dict) - str: prompt f 根据今天的检测数据生成一份林区火情巡检简报要求简洁、条理清晰。 数据如下 {json.dumps(daily_stats, ensure_asciiFalse, indent2)} 简报格式 1. 今日总览检测总数、告警次数、涉及摄像头数量 2. 重点疑似区域置信度高但需现场确认的 3. 建议动作按优先级排列 response requests.post( http://localhost:11434/api/generate, json{ model: qwen2.5:7b, prompt: prompt, stream: False } ) return response.json()[response]本地部署千问7B模型对硬件有基本要求内存至少16GB纯CPU推理生成一份报告可能要1-2分钟。如果觉得慢可以考虑量化版本比如qwen2.5:7b-q4_K_M精度降低不多但速度能提升不少。我在项目里用的是量化版实测报告生成时间从90秒降到30秒左右。6.4 大模型辅助的完整链路和降级方案大模型辅助的完整流程是Flask检测到低置信度目标 → Spring Boot把截图发给Python服务或直接由Python侧处理→ 生成描述文本 → 调大模型 → 返回判断结果 → 判断为疑似火情则更新告警级别。注意大模型是不可靠的网络超时、API限流、本地部署显存不足都会导致调用失败。所以系统必须有降级方案大模型调用失败时直接按规则引擎的结果处理也就是置信度大于0.6就告警小于0.6就记录不告警等大模型恢复后再补判。实际运行中DeepSeek API偶尔会碰到限流高峰期降级方案能保证核心检测流程不受影响。7. 避坑实录从训练到部署的完整排查链路7.1 训练阶段显存溢出、loss异常与标注错误显存溢出是最常见的问题。报错信息通常是CUDA out of memory。解决路径按优先级排减小batch从16降到8、4直到能跑起来降低imgsz从640降到480会影响小目标检测精度慎用开启梯度累积Ultralytics支持batch参数模拟但显存不够时还是直接减小batch最省事检查是否有其他进程占用显存——nvidia-smi看一下我之前发现某个残留的Jupyter kernel在偷吃显存loss异常方面如果是NaN优先检查数据集里有没有损坏的图片或空标注文件。YOLO格式的txt文件如果内容是空的训练时该样本没有目标不一定会报错但会污染loss。写个脚本把空标注的图片挑出来是最快的排查方式。标注错误导致的训练效果差最隐蔽。我遇到过训练了50个epoch后验证集mAP始终在0.5以下排查了两天最后发现是有一个批次的图片标注坐标超过了图像尺寸归一化值大于1。这种情况Ultralytics不报错但模型学到的框位置完全紊乱。用下面这段代码能快速检查import os label_dir dataset/labels/val for file in os.listdir(label_dir): with open(os.path.join(label_dir, file), r) as f: for line in f: parts line.strip().split() if len(parts) ! 5: print(f格式错误: {file}: {line}) continue x, y, w, h map(float, parts[1:]) if x 0 or x 1 or y 0 or y 1 or w 1 or h 1: print(f坐标越界: {file}: {line})7.2 推理阶段速度慢、显存泄漏与并发阻塞推理速度达不到预期时按这个顺序排查GPU没有真正用上。检查Flask服务日志里有没有Using device: cuda如果显示cpu说明模型加载到了CPU上多半是环境变量问题。确保启动服务前设置了CUDA_VISIBLE_DEVICES0且PyTorch的CUDA可用。输入图像尺寸太大。如果前端截图是4K分辨率直接送进模型预测会非常慢。应该在前端或后端先把图缩放到640x640左右保持宽高比再推理检测框坐标再映射回原图。这一步能省80%的推理时间。并发请求阻塞。Flask默认单线程多个请求同时到会排队。用gunicorn多进程或者加threadedTrue能缓解但如果GPU算力本来就不够多进程反而会互相抢占显存。更合理的方案是在业务层做限流比如Spring Boot对同一摄像头的检测请求做1秒合并而不是每个请求都打到推理服务。显存泄漏如果部署超过一个星期后推理越来越慢多半是反复加载模型导致的显存碎片。动态加载模型的逻辑要加缓存和引用计数用完的模型及时del并torch.cuda.empty_cache()import torch def load_model_once(model_name): if model_name not in models: models[model_name] YOLO(fweights/{model_name}-fire.pt) # 预热一次 models[model_name].predict(np.zeros((640, 640, 3)), verboseFalse) return models[model_name]7.3 前后端联调跨域、编码与数据格式联调阶段遇到的坑很大一部分是数据格式问题跨域问题Vue前端跑在3000端口Spring Boot跑在8080端口Flask跑在5000端口三个端口互相调用必须处理CORS。在Spring Boot里加一个配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .maxAge(3600); } }Flask侧也要加from flask_cors import CORS CORS(app)Base64编码大小写问题图像Base64字符串传到后端时前端可能加了data:image/jpeg;base64,前缀后端解码前要记得去掉。否则base64.b64decode会报错。检测框坐标单位不统一Flask返回的是像素坐标原图分辨率前端Canvas画框时如果视频流的显示尺寸和原图尺寸不一致需要按比例换算。不然会出现框画偏了的情况这是联调时最隐蔽的bug之一。7.4 边缘部署目标检测模型上RK3588与Jetson的实践要点项目里有一部分摄像头挂在边缘设备上我用过RK3588和Jetson Orin Nano各说一些经验。RK3588RK3588的NPU对YOLOv8的支持比较成熟但注意它原生跑的是RKNN格式。转换流程是PyTorch模型 → ONNX → RKNN。用rknn-toolkit2转换时YOLOv10的去NMS结构反而成了问题某些算子NPU不支持。最终我是在RK3588上跑了YOLOv8n因为rknpu对v8的兼容性最好。Jetson Orin Nano装上JetPack SDK后自带CUDA直接用TensorRT加速效果很好。把YOLOv8n导出TensorRT引擎的流程pip install tensorrt yolo export modelyolov8n-fire.pt formatengine device0 halfTrueTensorRT引擎跑起来后推理延迟从PyTorch的20多毫秒降到5-8毫秒这个提升在边缘设备上是质变的。但TensorRT引擎是硬件相关的换一块显卡就要重新导出不要想着复制过去就能用。写在最后这套系统交付后的真实体会整个项目从标注、训练、部署到前后端联调前后花了大概两个月。最深的体会有三点。第一森林火灾检测的精度天花板不在模型结构而在数据质量。我把五个YOLO版本全训了一遍发现它们之间的精度差距都不超过3个百分点但同一个模型在不同标注质量的训练集上差距能超过10个百分点。所以如果你打算复现这套系统先在数据标注上花足时间。第二架构分层越清晰调试越省心。Flask只管推理、Spring Boot只管业务、Vue只管展示、大模型只管辅助研判各层之间用HTTP接口解耦出了问题隔层排查非常快。相反如果图省事把所有逻辑塞到一个服务里后面每次改需求都要提心吊胆。第三大模型接入要降低期望——它不是系统的核心只是一个加分项。检测准不准最终还是看YOLO模型本身的训练效果DeepSeek和千问只是让系统变得更聪明、更好用而不是让它变得会检测。如果你是单机在笔记本上跑通全流程建议先复现YOLOv11m单模型加Flask加Spring Boot的最小闭环跑通了再叠加其他版本和大模型功能。一步一步来这套系统的复杂度是可控的。