简介:一份围绕火灾预警场景、基于YOLOv11的视频流实时检测算法工程化部署技术文档,面向计算机视觉算法工程师、目标检测开发者与智慧消防系统设计人员。内容从城市火灾隐患和传统传感预警的局限切入,系统讲解YOLOv11算法原理,包括骨干网络、颈部网络、检测头、损失函数及训练评估,并针对视频流实时检测梳理摄像头选型、视频解码、图像增强去噪、目标跟踪和硬件加速等优化要点。工程化部署部分给出从环境搭建、数据准备、模型训练到系统集成、功能与性能测试的完整流程,同时提供代码实现基础框架与优化效果对比,并附有大型仓库、森林等场景案例。全文档共36页,PDF单文件2.12MB,目录完整支持跳转,便于按章节查阅。已有123人学习下载,适合正在落地视觉检测项目或构建火灾预警系统的人群参考。
1. 火灾预警系统 + YOLOv11:这不是论文,是一套能落地的工程化方案
拿到这份 36 页的 PDF 时,我以为是又一篇拿 YOLO 蹭热度的课程设计。翻完目录和正文发现,它其实把「视频采集 → 图像预处理 → YOLOv11 推理 → 报警联动 → 系统测试」整条链路都串起来了,章节顺序就是标准的工程化部署流程:算法原理、视频流技术要点、环境搭建、模型训练、系统集成、性能测试,最后还给了一个仓库案例和一个森林防火案例。对正在做毕业设计、竞赛 demo,或者公司想快速验证「AI 火灾识别到底能不能用」的人来说,这份文档的价值在于:它把 YOLOv11 从模型层面拉到了系统层面,告诉你摄像头怎么选、RTSP 流怎么接、帧率怎么定、模型怎么训、部署到 Jetson 还是服务器。适合两类人:一是刚接触目标检测、需要一个完整项目骨架的学生;二是想评估视觉火灾预警方案可行性、需要快速搭出原型验证效果的工程师。接下来我按自己的习惯拆一遍,把文档里没写透的参数边界和踩坑点补上。
2. 为什么是 YOLOv11:从单阶段检测到火灾场景的选型逻辑
2.1 单阶段检测的回归本质与 YOLO 系列演进脉络
目标检测分两派:两阶段(R-CNN 系)先提候选区域再分类回归,精度高但慢;单阶段(YOLO 系)直接把检测当成回归问题,一次前向就输出边界框、类别和置信度。YOLOv1 把图切成 S×S 网格,每个网格负责预测 B 个框和 C 个类别,思路激进但定位精度差,小目标几乎全丢。YOLOv2 引入锚框(Anchor Boxes)和批归一化,解决了收敛慢和框不准的问题。YOLOv3 上 FPN 做多尺度预测,YOLOv4 换 CSPDarknet53 + PANet + Mosaic 数据增强,YOLOv5 把训练和部署流程做成了开源工具链,PyTorch 生态从此统一。
到 YOLOv11,Ultralytics 延续了 v5 以来的工程化基因,改动主要集中在骨干网络和检测头上:骨干里融入轻量卷积和注意力,颈部继续用特征金字塔做多尺度融合,检测头保持解耦结构。对火灾检测这个场景,看中的不是某一层结构,而是训练好的 COCO 预训练权重可以直接迁移——火焰、烟雾这些类别虽然不在 COCO 80 类里,但特征提取的底层能力是通用的,用少量火灾数据微调就能收敛,这是工程上能快速落地的前提。
2.2 边界框表示、mAP 指标与火灾检测的评价特殊性
目标检测的边界框有两种表示:[x1, y1, x2, y2]对角线坐标,和[cx, cy, w, h]中心点加宽高。YOLO 系列训练时内部用后者归一化到 [0,1],推理输出再映射回原图尺寸。评估指标方面,精准率(Precision)管「检出来的框有多少是真火」,召回率(Recall)管「真火被漏了多少」,mAP 是各类别 AP 的平均。火灾预警有个特殊性:漏报比误报严重得多。漏报直接导致错过最佳扑救窗口,误报最多是让安保人员多跑一趟。所以调参时我对 recall 的容忍度更高,宁可 precision 掉一两个点,也要保证真实火焰在测试集上不漏。
2.3 YOLOv11 网络结构与复合损失函数的定位
YOLOv11 的结构还是三件套:Backbone 提特征,Neck 做金字塔融合,Head 出预测。文档里给了一段简化版 PyTorch 实现,骨干是轻量卷积块叠加一个 SE 式注意力模块:
import torch import torch.nn as nn class LightweightConvBlock(nn.Module): def __init__(self, in_channels, out_channels): super().__init__() self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=1, padding=1) self.bn1 = nn.BatchNorm2d(out_channels) self.relu = nn.ReLU(inplace=True) def forward(self, x): return self.relu(self.bn1(self.conv1(x))) class AttentionModule(nn.Module): """轻量通道注意力:全局池化后通过两个全连接层生成通道权重""" def __init__(self, channels): super().__init__() self.avg_pool = nn.AdaptiveAvgPool2d(1) self.fc = nn.Sequential( nn.Linear(channels, channels // 16, bias=False), nn.ReLU(inplace=True), nn.Linear(channels // 16, channels, bias=False), nn.Sigmoid() ) def forward(self, x): b, c, _, _ = x.size() y = self.avg_pool(x).view(b, c) y = self.fc(y).view(b, c, 1, 1) return x * y.expand_as(x)两个要点:注意力模块里channels // 16是压缩比,SE 结构用 1/16 降维可以显著减少参数量,火灾场景通道数不大时这个比例不必改动;expand_as是逐通道广播乘法,实现的是特征重标定,让模型更关注火焰区域的高响应通道。实际用 Ultralytics 官方仓库时你不会手写这个 Backbone,但理解了这层结构,才知道改width_multiple参数调整模型尺寸时,注意力模块的通道数会跟着缩放。
损失函数是三段式:边界框用 GIoU,考虑的不只是 IoU 重叠,还惩罚了两个框不包含时的距离;类别用交叉熵;置信度用二元交叉熵。代码里的giou_loss是个占位函数,实际工程我直接用torchvision.ops里的实现:
from torchvision.ops import generalized_box_iou def giou_loss(pred_boxes, target_boxes): """pred_boxes/target_boxes: [N, 4] 格式为 cx,cy,w,h,需先转 x1,y1,x2,y2""" pred_xyxy = cxcywh_to_xyxy(pred_boxes) target_xyxy = cxcywh_to_xyxy(target_boxes) return 1.0 - generalized_box_iou(pred_xyxy, target_xyxy).diag().mean()注意输入格式,GIoU 计算要求的是x1,y1,x2,y2,YOLO 训练内部是cx,cy,w,h,转一次再算,否则边界框损失曲线会异常震荡。
3. 视频流实时检测的技术底座:采集、预处理、跟踪与提速
3.1 摄像头选型与 RTSP 拉流实践:从 USB 到网络枪机的取舍
摄像头选型直接影响检测效果。室内仓库和车间,优先网络高清枪机,2K 或 4K 分辨率能捕捉更小的火苗细节;室外森林或建筑工地,防护等级要 IP66 以上,带红外夜视才能在低照度下继续工作。接口协议上,我强烈建议选带 RTSP 协议的网络摄像头,而不是 USB 摄像头。USB 传输距离限制在 5 米内,而一个仓库的摄像头往往要拉几十米的网线。RTSP 流的地址格式一般是:
rtsp://username:password@192.168.1.64:554/Streaming/Channels/101用 OpenCV 拉流有玄学要处理,直接给VideoCapture传 RTSP 地址经常出现启动慢、掉线重连不了的问题。我一般会在外面包一层拉流重试:
import cv2, time def create_rtsp_capture(rtsp_url, retry=3, timeout=10): """带超时和重试的 RTSP 拉流,解决摄像头掉线后无法自动恢复的问题""" for attempt in range(retry): cap = cv2.VideoCapture(rtsp_url) cap.set(cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, timeout * 1000) cap.set(cv2.CAP_PROP_READ_TIMEOUT_MSEC, timeout * 1000) if cap.isOpened(): return cap cap.release() print(f"第 {attempt + 1} 次拉流失败,1 秒后重试") time.sleep(1) return NoneCAP_PROP_OPEN_TIMEOUT_MSEC和CAP_PROP_READ_TIMEOUT_MSEC这两个参数很关键,默认值有时是 0,等于不设超时,摄像头网络抖动时cap.read()会一直卡住,整个检测链路就假死了。另外,读取频率不要盲目拉满,网络摄像头一般设置 25fps,但检测端如果用 CPU 跑 YOLOv11s 根本来不及处理,后面要讲到抽帧策略。
3.2 视频预处理与抽帧策略:解码、缩放、归一化的正确顺序
摄像头输出的 H.264 或 H.265 编码流要先解码成原始帧,OpenCV 的VideoCapture.read()内部完成了这一步。接下来不是直接送模型,而是做三步预处理:缩放保留宽高比、填充、归一化。
import cv2 import numpy as np def letterbox(img, new_shape=(640, 640), color=(114, 114, 114)): """等比缩放后填充,避免直接 resize 导致火焰形状扭曲""" shape = img.shape[:2] r = min(new_shape[0] / shape[0], new_shape[1] / shape[1]) new_unpad = (int(round(shape[1] * r)), int(round(shape[0] * r))) img = cv2.resize(img, new_unpad, interpolation=cv2.INTER_LINEAR) dw = (new_shape[1] - new_unpad[0]) / 2 dh = (new_shape[0] - new_unpad[1]) / 2 top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1)) left, right = int(round(dw - 0.1)), int(round(dw + 0.1)) return cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color)这段代码抄的 YOLOv5 的 letterbox 逻辑,为什么不能直接用cv2.resize?因为直接拉伸会改变火焰的宽高比,目标检测网络虽然对长宽比有一定鲁棒性,但固定输入尺寸下,保持原始比例可以避免小目标被拉变形。填充边界的灰度值用 114,这是 COCO 训练时采用的均值填充,数值本身不是玄学,是 Ultralytics 代码里的默认值,保持一致对后期部署没有额外影响。
预处理完整链路是:解码帧 → letterbox 到 640×640 →np.float32转类型 → 除以 255 归一化 → 转 CHW 布局 → 推理。每一步都有性能开销,在 CPU 上全部合成一次np.ascontiguousarray能省几毫秒,这个后面测试章节再看。
3.3 目标跟踪:火灾检测到底要不要引入跟踪器
文档花了一节讲目标跟踪,实际工程里碰到的困惑是:检测已经逐帧做了,为什么还要跟踪?两个理由:一是检测器偶尔漏帧,跟踪可以做时序平滑,补上中间丢的那几帧,报警不会因为一帧漏检就中断;二是检测框会抖动,直接画在监控画面上很晃眼,跟踪器能稳定框的位置。
但我的建议是:火灾预警系统第一版先别上跟踪。因为火焰和烟雾的形状是高度非刚性的,传统的 IoU 匹配跟踪(如 DeepSORT 的级联匹配)对形变目标效果很差,火焰每次形变都可能导致跟丢重跟,反而引入不必要的复杂度。真正要上跟踪的场景是「确认火焰蔓延方向」或「计算燃烧持续时间」,这属于第二阶段功能。第一版把检测的置信度阈值和帧间逻辑做好,用状态机做「连续 N 帧确认报警」比跟踪器更实用:
class FireAlertStateMachine: """连续确认机制:连续 detect_need 帧检测到火焰才触发报警,抑制单帧误报""" def __init__(self, detect_need=3, release_after=5): self.detect_need = detect_need self.release_after = release_after self.hit_count = 0 self.miss_count = 0 self.alert_active = False def update(self, has_fire): if has_fire: self.hit_count += 1 self.miss_count = 0 if self.hit_count >= self.detect_need: self.alert_active = True else: self.miss_count += 1 self.hit_count = 0 if self.miss_count >= self.release_after: self.alert_active = False return self.alert_active状态机比跟踪器简单得多,效果也够用。阈值别拍脑袋定,检测帧率如果是 10fps,连续 3 帧确认大约 300ms,这个延迟对火灾报警完全可接受,但能过滤掉大部分因为光照突变或摄像头抖动导致的单帧误检。
3.4 实时性优化:CPU 推理、TensorRT 加速与帧率平衡的边界
实时检测的性能瓶颈在推理。一条经验原则:摄像头采集帧率是 25fps,检测端不必追求每秒 25 次推理,火灾是慢变过程,5~10fps 的检测频率足够。真正要优化的是单次推理时延。CPU 上推理 YOLOv11s、640×640 输入,OpenVINO 量化后大约 30~80ms,看 CPU 型号;NVIDIA GPU 上用 TensorRT FP16 转 engine 后可以压到 3~8ms。
部署到 Jetson Nano 这类边缘设备时,我踩过最大的坑是 TensorRT 版本匹配问题。Jetson 上的 JetPack 版本和 x86 的 TensorRT 不通用,必须用 JetPack 对应的.whl包。实际项目里,如果时间紧,第一步直接用torch.no_grad()+ 半精度在 GPU 上跑原始 PyTorch 模型就能满足 demo 需求,TensorRT 优化放到系统验证阶段再做,避免一开始就被环境问题卡住。
4. 工程化部署实战:从环境搭建到系统集成的完整步骤
4.1 硬件与软件环境配置清单:GPU 服务器、Jetson 到纯 CPU
按文档的部署章节补一个我自己常用的环境矩阵。训练阶段建议一张显存至少 8GB 的 GPU,RTX 2080 起步,因为火灾数据集图片分辨率往往偏高,batch size 太小 BN 层统计不稳定。推理阶段根据需求分三种:
| 场景 | 硬件 | 推理方案 | 预期时延 |
|---|---|---|---|
| 单路视频预警 | Intel i5 / i7 CPU | OpenVINO 量化 INT8 | 50~100ms |
| 多路视频预警 | NVIDIA T4 / 3080 | TensorRT FP16 | 3~8ms/路 |
| 边缘小站 | Jetson Orin Nano | TensorRT FP16 | 10~20ms |
软件环境以 Ultralytics 套件为主,Python 版本建议 3.9~3.11。一个干净的安装流程:
# 创建虚拟环境,避免和系统 Python 包冲突 conda create -n fire_yolo python=3.10 -y conda activate fire_yolo # 安装 PyTorch,注意 CUDA 版本要和驱动匹配 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121 # 安装 Ultralytics 和配套依赖 pip install ultralytics opencv-pythonCUDA 版本的匹配最关键,cu121指的是 CUDA 12.1,先nvidia-smi看驱动支持的最高版本,再选对应 index-url。装完python -c "import torch; print(torch.cuda.is_available())"必须输出 True,False 的话后面训练直接跑 CPU,慢几十倍。
4.2 数据集准备与标注转换:Labelme 格式转 YOLO 格式的三个要点
火灾检测的数据集是最难的一环,公开数据集很少能直接商用,大多要自己标。标注工具用 Labelme 或者 LabelImg 都行,但 YOLOv11 训练需要的是 txt 标注格式——每行一个目标:类别 id、归一化中心 x、归一化中心 y、归一化宽、归一化高。Labelme 默认导出 JSON 格式,需要写一次转换脚本。我自己常用的转换脚本:
import json, os import numpy as np from glob import glob from PIL import Image def labelme_to_yolo(json_path, img_dir, out_dir, class_names): """把 labelme 的 json 标注转成 yolov11 训练用的 txt 格式(这是必备脚本,一次写好后反复用)""" with open(json_path, 'r', encoding='utf-8') as f: data = json.load(f) img_name = data['imagePath'] if not os.path.exists(os.path.join(img_dir, img_name)): print(f"图片 {img_name} 不存在,跳过") return False # 读取原始图片尺寸,归一化必须用真实宽高,不能用 json 里记录的 shape 尺寸 img = Image.open(os.path.join(img_dir, img_name)) img_w, img_h = img.size lines = [] for shape in data['shapes']: label = shape['label'] if label not in class_names: continue cls_id = class_names.index(label) # labelme 的点是 [x, y],任意多边形,取外接矩形作为边界框 points = np.array(shape['points'], dtype=np.float32) x_min, y_min = points.min(axis=0) x_max, y_max = points.max(axis=0) # 注意给定点集合定义的外接矩形可能超出图像边界,必须裁剪到 [0,1] 范围内 x_min = np.clip(x_min, 0, img_w) x_max = np.clip(x_max, 0, img_w) y_min = np.clip(y_min, 0, img_h) y_max = np.clip(y_max, 0, img_h) cx = (x_min + x_max) / 2 / img_w cy = (y_min + y_max) / 2 / img_h w = (x_max - x_min) / img_w h = (y_max - y_min) / img_h lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") if lines: txt_name = os.path.splitext(img_name)[0] + '.txt' with open(os.path.join(out_dir, txt_name), 'w') as f: f.write('\n'.join(lines)) return bool(lines)脚本里三个必须注意的细节:一是外接矩形的计算,多边形转框,火焰边缘不规则,Labelme 里如果画的是多边形而不是矩形,必须取点集的外接框;二是越界裁剪,火焰烟雾靠近画面边缘时框很容易出界,不 clip 会导致归一化坐标超过 1,训练时边界框损失爆炸;三是类别映射顺序要固定,写进一个.yaml文件里,前后别改,不然训到一半换类别顺序,模型就废了。
数据集划分我按 8:1:1 做训练、验证、测试,并且保证同一段视频的不同帧只进其中一个集合,防止数据泄露。做法是先把视频按时间段切成片段,再从片段抽帧分集,而不是直接随机抽帧。
4.3 模型训练:YAML 配置、命令参数与训练策略调整
数据准备好后,训练命令很直接:
yolo detect train \ data=fire.yaml \ model=yolov11s.pt \ epochs=100 \ batch=16 \ imgsz=640 \ lr0=0.01 \ device=0 \ workers=4fire.yaml的内容是:
path: /data/fire_dataset train: images/train val: images/val names: 0: fire 1: smoke几个参数的选择逻辑:model=yolov11s.pt用的是官方网站预训练权重做迁移学习,如果从零开始训,纯火灾数据集基本不可能训出可用的结果,样本量不够;lr0=0.01是针对迁移学习的常用初始学习率,如果发现 loss 前 10 个 epoch 不降,降到0.005试试;batch=16占用显存约 12GB,你的卡显存不够就降 batch 而不是降 imgsz,保持 640 输入尺寸更有利于小火焰检测。训练完的产物是runs/detect/train/weights/best.pt,保存的是验证集上 mAP 最高的权重,不是最后一个 epoch 的。
4.4 模型部署:从best.pt到 ONNX / TensorRT 的转换链路
训练好的 PyTorch 权重直接上线推理可以,但性能差一截。标准路径是先导出 ONNX:
yolo export model=best.pt format=onnx imgsz=640 dynamic=False simplify=Truedynamic=False保持固定输入尺寸,推理引擎不需要动态 shape 的分支,性能更稳定;simplify=True用 ONNX Simplifier 做图优化,去掉冗余算子。然后在目标平台上用 TensorRT 转换:
trtexec --onnx=best.onnx --saveEngine=best_fp16.engine --fp16--fp16把模型量化到半精度,精度掉得极少,但速度翻倍。之后用 Ultralytics 的 API 加载 engine 文件,后缀名换成.engine,代码里不需要任何改动,这个兼容性设计是 YOLO 生态做得最好的地方。如果你部署在 Jetson Nano 上,trtexec换成 JetPack 附带的版本,一定不要用 x86 宿主机上的 TensorRT 去转 Jetson 的模型,算力架构(GPU 架构编号)不同,转换出来的 engine 直接跑不了。
4.5 系统集成:RTSP 多路接入与报警联动逻辑
系统集成是工程化部署里最像「工程」的部分。多路视频接入时,每路开一个线程独立拉流和推理,用Queue做帧缓冲:
import threading, queue, time import cv2 from ultralytics import YOLO class DetectWorker(threading.Thread): """单路视频的推拉流 + 推理 worker,多路监控时每路开一个线程独立处理""" def __init__(self, rtsp_url, model_path, conf_thres=0.25, queue_size=2): super().__init__() self.daemon = True self.rtsp_url = rtsp_url self.model = YOLO(model_path) self.conf_thres = conf_thres self.frame_queue = queue.Queue(maxsize=queue_size) self.running = True def run(self): while self.running: cap = create_rtsp_capture(self.rtsp_url) if cap is None: time.sleep(5) continue while self.running: ret, frame = cap.read() if not ret: print("读取失败,尝试重连") break if self.frame_queue.full(): # 队列满了直接丢帧,保证处理的是最新画面而不是积压的历史帧 try: self.frame_queue.get_nowait() except queue.Empty: pass self.frame_queue.put(frame) cap.release()这段代码的核心技巧是queue_size=2:推理速度赶不上采集速度时,队列只保留最新的一两帧,旧帧直接丢。这比「把所有帧都塞进队列然后排队处理」实时性更好,火灾检测要的是当前画面的状态,不是补处理几秒前的旧帧,那会导致检测结果滞后于现实。报警联动按文档里的架构,检测到火焰后调用 MQTT 或 HTTP 接口推送预警信息,同时写日志。我这里不展开具体协议,第一版只要保证「检测结果 → 消息推送 → 记录日志」链路通就行,后续再对接消防喷淋或者门禁系统。
5. 工程化部署避坑指南:数据、模型、部署三个层面的问题
5.1 数据层面:小目标火焰漏检的根源在标注
现象:训练完模型对远处小火苗完全无感,近处大火能检测到,但置信度也偏低。 原因:火灾数据集中小目标样本太少。火焰是慢变目标,初始阶段占画面面积很小,如果标注框面积小于原图的 5%,YOLO 在特征图下采样 32 倍之后,目标在最高层特征图上可能只剩 1~2 个像素点,特征几乎完全丢失。 解决:一是数据层面做过采样,专门把小火焰样本复制多份并加随机扰动;二是训练时开启mosaic=1.0增强,把多个小目标拼到一张图里;三是换yolov11m.pt或yolov11l.pt这种更大的模型,小目标特征提取能力更强,代价是推理变慢。主流做法是先用小模型跑通流程,确认漏检瓶颈确实是模型容量不足,再换大模型。
5.2 数据层面:标注框越界导致损失不收敛
现象:损失函数前期波动极大,训练到 30 个 epoch 还在上下跳,验证集 mAP 一直上不去。 原因:标注工具允许框画到图像边缘之外,YOLO 格式归一化后坐标出现负数或大于 1 的数值,导致 GIoU 损失计算异常。 解决:转换脚本里必须做坐标裁剪。在 4.2 代码段中的np.clip(x_min, 0, img_w)这一行就是干这个的。另外可以用一行命令清洗已有数据集:
python -c " from glob import glob import numpy as np for txt in glob('labels/train/*.txt'): lines = [] for line in open(txt): parts = line.strip().split() cx, cy, w, h = map(float, parts[1:]) # 归一化后的框必须同时满足:w/h <= 1 且中心点在图像范围内 if w > 1.0 or h > 1.0 or cx < 0 or cx > 1 or cy < 0 or cy > 1: print(f'异常框: {txt} -> {line.strip()}') continue lines.append(line.strip()) open(txt, 'w').writelines([l + '\n' for l in lines]) "训练前批量检查一遍,把这些坏数据剔除或修正,损失曲线会立刻正常。这条经验适用于任何 YOLO 项目,不只火灾检测。
5.3 模型层面:误报集中在光线突变和反光区域
现象:系统部署到实际场景后,晴天日光变化、灯光开关、金属表面反光都会触发误报,测试集上没这些问题。 原因:训练数据多样性不够。火灾测试集通常是在相对稳定的光照条件下采集的,没有覆盖真实监控场景的光线突变。模型学到的是「亮橙色 + 动态变化」的纹理特征,而不是「燃烧」的物理特征。 解决:在训练数据里混入大量负样本——夕阳、红色车灯、电焊火花、红色装修布景、炉灶火光——标注为背景类别(不给框)。负样本的泛化能力提升效果远大于正样本的机器增强。比例上,负样本数量至少和正样本相当,我一般做到正:负 = 1:1.5。
5.4 部署层面:TensorRT engine 在 Jetson 上加载失败
现象:在 x86 服务器上转换好的.engine文件拷贝到 Jetson Nano,YOLO('best.engine')加载时直接报错。 原因:.engine文件和硬件设备绑定,x86 的 GPU 架构是 Ampere 或 Turing,Jetson Nano 是 Maxwell / Ampere,ILP(指令级并行)和算子实现都不同。 解决:必须在目标设备上用trtexec重新转换。操作流程是:把best.onnx拷贝到 Jetson,然后在 Jetson 终端执行trtexec --onnx=best.onnx --saveEngine=best_fp16.engine --fp16。这个坑我踩了整整两天,从那以后任何部署到边缘设备的模型,我都在目标板上重新走一遍 ONNX → TensorRT 转换,绝不拿别的机器上的 engine 直接拷贝。
5.5 部署层面:长时间运行内存泄漏导致系统崩溃
现象:系统运行十几个小时后,内存占用持续上升,推理速度越来越慢,最终进程被杀。 原因:OpenCV 的VideoCapture在重连时没有释放旧资源,或者推理线程里每一帧都调用了YOLO(frame)而内部没有释放中间 tensor。最容易忽略的是cv2.imwrite如果每帧都写日志图片,会累积大量文件句柄。 解决:写一个内存监控脚本,每小时打一次psutil.Process().memory_info().rss看趋势:
import time, psutil def monitor_memory(interval=3600): """每 n 秒打印一次当前进程内存占用,定位是否线性增长泄漏""" p = psutil.Process() while True: mem = p.memory_info().rss / 1024 / 1024 print(f"[{time.strftime('%H:%M:%S')}] 内存占用: {mem:.1f} MB") time.sleep(interval)如果内存随时间线性增长,先检查VideoCapture重连逻辑里有没有cap.release();再检查推理代码里是否每帧都向 Logger 列表追加数据。修复后跑 72 小时稳定性测试,内存曲线应该在一个水平线附近小幅波动。注意别把cap放在循环外创建却让它在内部无限重连,这是内存泄漏的头号来源。
6. 系统测试与参数调优:从演示指标到可交付的性能验证方法
6.1 性能测试脚本:吞吐量、时延和丢帧率的量化
系统合入后,别再拿「看起来流畅」当依据,要上指标。测试脚本里我最看重三个数:单帧推理时延(latency)、处理帧率(fps)、丢帧率(FRR)。推理时延和丢帧率是矛盾指标,要提高帧率就要丢更多帧,最终目标是在不丢帧的前提下稳定运行。用下面这个脚本测试:
import time import cv2 from ultralytics import YOLO def benchmark(model_path, video_path, conf_thres=0.25, gpu=True): """压测:连续推理 5 分钟,统计时延、帧率和丢帧率""" model = YOLO(model_path) cap = cv2.VideoCapture(video_path) if not cap.isOpened(): print("视频打开失败") return total_frames = 0 processed_frames = 0 start_time = time.time() test_duration = 300 # 连续跑 5 分钟 while time.time() - start_time < test_duration: ret, frame = cap.read() if not ret: cap.set(cv2.CAP_PROP_POS_FRAMES, 0) # 循环读同一段视频 continue total_frames += 1 t0 = time.time() results = model.predict(frame, conf=conf_thres, verbose=False) inference_time = time.time() - t0 processed_frames += 1 if processed_frames % 50 == 0: print(f"已处理 {processed_frames} 帧,最近一次推理时延: {inference_time * 1000:.1f} ms") elapsed = time.time() - start_time fps = processed_frames / elapsed # 丢帧率 = 1 - 处理帧数 / 总帧数,这里因为循环读视频,总帧数等于读到的帧数 drop_rate = 1 - processed_frames / total_frames print(f"平均推理时延: {elapsed / processed_frames * 1000:.1f} ms") print(f"处理帧率: {fps:.2f} fps") print(f"丢帧率: {drop_rate * 100:.1f}%")conf=thres决定检测框的灵敏度,但不要为了提高性能指标而调高它——置信度阈值 0.25 是个偏保守的起点,火灾场景下掉到 0.15 也可以,误报靠负样本去解决,不能靠提高阈值压掉,那是把问题推给了漏报。
6.2 功能测试边界:夜间、逆光、遮挡三种工况的打分法
功能测试不能只测大白天正常光。我按三种必测工况设计测试用例:夜间(红外模式)、逆光(窗户强光)、遮挡(烟雾扩散遮挡火焰)。每种工况准备 20 段短视频,标注真值和预测框的 IoU,计算 mAP。夜间和逆光的效果差距几乎全部来自训练数据是否覆盖了这些光照条件。如果你的数据集没有夜间样本,用归一化到 0~255 再做 gamma 校正生成一批模拟夜间数据:
import cv2, numpy as np def simulate_night(frame, gamma=2.2): """模拟低照度效果:降低亮度 + 提高对比度,用于扩充夜间训练样本""" hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV).astype(np.float32) hsv[..., 2] = hsv[..., 2] * 0.3 # V 通道压暗 hsv[..., 2] = np.clip(hsv[..., 2], 0, 255) dark = cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2BGR) # gamma 校正在暗部引入对比度失真,让模型适应红外模式下纹理模糊的缺点 return np.power(dark / 255.0, 1.0 / gamma) * 255.0这种模拟数据能补一部分光照泛化能力,但别指望完全替代真实夜间数据。真实红外模式下的成像特性和压暗图差距很大,尤其是噪点分布和色彩偏移,有条件还是要去现场采集。
6.3 调参心法:先数据后模型,先速度后精度
写在最后。这套系统我从拿到文档到完成 demo 验证,走通全流程大概花了两天。第一天卡在数据标注转换脚本上,第二天卡在 TensorRT 环境匹配上,真正训练和调参反而顺利。从那以后我每次做 YOLO 类项目都强制走一遍「先检查标注 → 再训小模型 → 上指标 → 换大模型」的顺序,不在环境搭建上恋战。希望这份拆解能帮你把文档里的骨架变成能跑的系统,少走我走过的那段弯路。
本文还有配套的精品资源,点击获取