去年帮一个做森林防火信息化的朋友调试巡检系统,真正把检测模型从单机demo推到前后端联调上线,才发现这个场景比想象中复杂得多。森林野外火灾里的火焰和烟雾检测,跟日常的通用目标检测完全是两码事——火焰形态千变万化,烟雾半透明还随风飘散,背景里全是颜色相近的树干、晚霞、雾气。更麻烦的是,整套系统要同时承载多版本模型训练对比、Web业务平台、实时视频流接入和大模型辅助研判。这篇文章就把我实际落地的一套方案完整拆开讲清楚:基于YOLOv8/v10/v11/v12/26五个版本的火焰烟雾检测模型如何训练与对比,Spring Boot+Vue+Flask三层架构怎么协作,以及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 python=3.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 data=fire.yaml model=yolov8m.pt epochs=150 imgsz=640 batch=16 optimizer=AdamW lr0=0.001 freeze=10 device=0 # 其他版本类似 # yolo train data=fire.yaml model=yolov10m.pt epochs=150 imgsz=640 batch=16 optimizer=AdamW lr0=0.001 freeze=10 device=0 # yolo train data=fire.yaml model=yolov11m.pt ... # yolo train data=fire.yaml model=yolov12m.pt ... # yolo train data=fire.yaml model=yolo26m.pt ...这里重点说三个参数:
batch:显存不够就调小。1660Ti是6G显存,跑yolov8m、imgsz=640最高只能开到batch=8。如果数据量不够大,batch=16和batch=32对最终精度影响有限,没必要硬开。
imgsz:训练尺寸。森林监控画面里目标通常很小,把imgsz从640调到960能明显提升小目标召回率,但训练时间会增加一倍以上。我最终选640作为折中,因为部署时推理速度同样重要。
freeze:冻结前N层的主干网络参数。如果用了预训练权重,前10层冻结可以加速收敛、防止前期梯度把预训练特征破坏掉。但如果数据集和预训练数据集差异极大——火焰烟雾跟COCO差异就很大——冻结层数不宜太多,freeze=10到freeze=15之间比较合理,全冻结基本训不出好的火焰特征。
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-95(COCO风格的平均精度)、GPU推理耗时、CPU推理耗时(有些边缘设备没有GPU)、模型文件大小。
mAP50是IoU阈值0.5时的平均精度,对火焰烟雾这类边界模糊的目标,mAP50比mAP50-95更有参考价值,因为你不能指望烟雾的框像盒子一样精确。但mAP50-95也不能完全忽略,它反映的是模型定位精度的稳定性。
4.2 实测数据:同一数据集下五版本的真实表现
下面这张表是我在相同硬件(RTX 4090,FP16精度)、相同数据集、相同超参数下的实测结果,不同设备上的具体数值会有差异,但相对差距有参考意义:
| 模型版本 | 参数量(M) | 模型大小(MB) | mAP50 | mAP50-95 | 推理耗时(ms/帧, GPU) | 推理耗时(ms/帧, CPU) |
|---|---|---|---|---|---|---|
| YOLOv8m | 25.9 | 52 | 0.861 | 0.512 | 3.4 | 318 |
| YOLOv10m | 16.5 | 33 | 0.848 | 0.497 | 3.1 | 286 |
| YOLOv11m | 20.1 | 40 | 0.872 | 0.528 | 3.5 | 296 |
| YOLOv12m | 21.6 | 42 | 0.869 | 0.521 | 3.8 | 352 |
| YOLO26m | 23.4 | 46 | 0.864 | 0.519 | 4.2 | 339 |
解释几个值得注意的现象:
- YOLOv10的mAP略低但速度优势明显。它去掉了NMS后处理,推理pipeline更短,在边缘设备上这个优势会被放大。如果你的部署环境对延迟敏感,v10是值得考虑的选择。
- YOLOv11的精度在这组数据里最好。mAP50最高,说明它C3k2模块对这个数据集的适应能力确实强。但差距其实只在1-2个百分点,换成另一批测试数据排名可能就会变。
- YOLOv12虽然是注意力机制版本,但对火焰检测的提升不显著。注意力机制更擅长捕捉长距离依赖,而火焰烟雾的判别特征更多是局部颜色和纹理,所以v12在速度上还吃了亏,综合性价比一般。
- YOLO26作为新版本没体现出绝对优势。精度、速度都排在中间,如果是为了追新用26,对这个问题来说意义不大。
4.3 按部署目标选模型:一个实用的决策表
训练好五个模型后,选型的逻辑我用一个表格说清楚:
| 部署场景 | 推荐模型 | 理由 |
|---|---|---|
| Jetson Orin Nano / RK3588边缘盒子 | YOLOv10n或YOLOv8n | 推理延迟低,内存占用小,精度够用 |
| 普通PC(无GPU) | YOLOv11s | CPU推理速度最快的s版本,mAP50能到0.83左右 |
| 服务器(有GPU) | YOLOv11m或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, dtype=np.uint8) img = cv2.imdecode(img_array, cv2.IMREAD_COLOR) model = models.get(model_name) if model is None: return jsonify({"error": f"Unknown model: {model_name}"}), 400 results = model.predict(img, conf=conf_thres, iou=iou_thres, verbose=False) 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(host="0.0.0.0", port=5000, threaded=True)这里有个容易被忽略的细节:多个YOLO模型同时加载很吃显存。五个m版本模型加载后显存占用超过7GB,如果是6G显存的卡直接OOM。我实际在服务器上部署时只常驻YOLOv11m和YOLOv10m两个模型,其他版本按需动态加载。另外,Flask默认的threaded=True在并发高时会出现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) { Map<String, 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_demand=1 rtsp_proxy_port=10554Vue里播放HLS流的代码:
<template> <div> <video ref="videoPlayer" controls autoplay muted style="width: 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_key="your-deepseek-api-key", base_url="https://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( model="deepseek-chat", messages=[ {"role": "system", "content": "你是一个严谨的森林防火研判助手。"}, {"role": "user", "content": prompt} ], temperature=0.1, max_tokens=200 ) # 解析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_ascii=False, indent=2)} 简报格式: 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_DEVICES=0,且PyTorch的CUDA可用。
输入图像尺寸太大。如果前端截图是4K分辨率,直接送进模型预测会非常慢。应该在前端或后端先把图缩放到640x640左右(保持宽高比)再推理,检测框坐标再映射回原图。这一步能省80%的推理时间。
并发请求阻塞。Flask默认单线程,多个请求同时到会排队。用gunicorn多进程或者加threaded=True能缓解,但如果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(f"weights/{model_name}-fire.pt") # 预热一次 models[model_name].predict(np.zeros((640, 640, 3)), verbose=False) 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,各说一些经验。
RK3588:RK3588的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 model=yolov8n-fire.pt format=engine device=0 half=TrueTensorRT引擎跑起来后,推理延迟从PyTorch的20多毫秒降到5-8毫秒,这个提升在边缘设备上是质变的。但TensorRT引擎是硬件相关的,换一块显卡就要重新导出,不要想着复制过去就能用。
写在最后:这套系统交付后的真实体会
整个项目从标注、训练、部署到前后端联调,前后花了大概两个月。最深的体会有三点。第一,森林火灾检测的精度天花板不在模型结构,而在数据质量。我把五个YOLO版本全训了一遍,发现它们之间的精度差距都不超过3个百分点,但同一个模型在不同标注质量的训练集上,差距能超过10个百分点。所以如果你打算复现这套系统,先在数据标注上花足时间。第二,架构分层越清晰,调试越省心。Flask只管推理、Spring Boot只管业务、Vue只管展示、大模型只管辅助研判,各层之间用HTTP接口解耦,出了问题隔层排查非常快。相反,如果图省事把所有逻辑塞到一个服务里,后面每次改需求都要提心吊胆。第三,大模型接入要降低期望——它不是系统的核心,只是一个加分项。检测准不准,最终还是看YOLO模型本身的训练效果,DeepSeek和千问只是让系统变得更聪明、更好用,而不是让它变得会检测。
如果你是单机在笔记本上跑通全流程,建议先复现YOLOv11m单模型加Flask加Spring Boot的最小闭环,跑通了再叠加其他版本和大模型功能。一步一步来,这套系统的复杂度是可控的。