TensorRT加速YOLOv5与DeepSORT行人检测跟踪部署实战
发布时间:2026/10/1 4:59:56来源:尧图网络
简介一套基于TensorRT加速的YOLOv5与DeepSORT行人检测跟踪部署方案面向需要在NVIDIA Jetson Xavier NX及X86平台落地目标跟踪应用的算法工程师与嵌入式开发者。资源完整覆盖模型转换、引擎加载到端到端推理流程提供可直接运行的Python检测与跟踪脚本内置TensorRT引擎、自定义插件库、依赖列表与测试视频便于快速复现结果并迁移到自有项目。压缩包共35个文件以26个Python源码文件为主辅以engine模型文件、配置文件、README说明及mp4演示视频总大小约42.95MB目录结构清晰DeepSORT相关代码与权重分离便于替换模型或调整跟踪参数。已有399人学习下载适合具备一定目标检测基础、希望借助TensorRT提升推理性能的开发者参考尤其适用于实时性要求较高的边缘计算场景。1. TensorRT部署YOLOv5和DeepSORT为什么先讲整体架构街边摄像头前的人群监控场景里用YOLOv5找行人的框再用DeepSORT把跨帧的框串成同一条轨迹是很多人搭行人检测跟踪系统的常用起点。可这套组合直接用PyTorch跑消费级显卡上往往只有每秒十几帧CPU环境下更直接卡到没法看实时性成了最大的坎。TensorRT部署这件事的价值就是把YOLOv5和DeepSORT用到的两个网络重新编译成NVIDIA显卡的原生执行计划在FP16精度下把推理延迟压下去让Jetson、老GTX这类算力不算强的设备也能跑到25帧以上。这篇文章按落地顺序写先从检测加跟踪的整体架构讲透为什么这么配再讲环境版本怎么选、.pt文件怎么一步步变成.engine最后是加载引擎、后处理、接入跟踪器的完整代码以及我实际部署时踩过的一串坑。适合手头已有YOLOv5模型、准备做行人计数或人流预警的读者跟着复现。2. 检测、跟踪、推理加速三方如何配合很多第一次做这个项目的人容易把注意力全放在“TensorRT怎么装”上结果模型一跑起来发现跟踪器要么认不出人、要么ID乱跳。先花半小时把三者的分工理清后面调参才不会像无头苍蝇。2.1 YOLOv5的输出恰好是DeepSORT需要的输入YOLOv5在行人场景里只干一件事给每一帧画出若干个检测框。模型输出的是形状为[1, 25200, 85]的矩阵前4个值是框的中心坐标和宽高第5个值是目标置信度剩下80维是类别得分其中行人这一类通常排在0号位。你还需要自己写后处理把置信度过滤、坐标转换、非极大值抑制NMS做完才能得到干净的框集合。DeepSORT接收的正是这些框外加每个框对应的“外观特征向量”。它的职责分四步第一步用卡尔曼滤波预测每条已有轨迹在下一帧的位置第二步计算每个检测框和每条预测轨迹之间的运动匹配度指标常用框的IoU也常用马氏距离第三步计算检测框外观特征和轨迹历史特征之间的余弦距离第四步把运动距离和外观距离加权后丢给匈牙利算法求出全局最优匹配。只看运动信息时行人一旦被遮挡或者密集交叉框重叠严重ID就很容易串。DeepSORT加入外观特征以后ID切换会明显减少这也是它在行人场景比单纯IOU跟踪器稳的原因。但反过来DeepSORT对外观特征质量非常敏感ReID模型如果转换时出了差池后面所有匹配都是白搭。这条链路里YOLOv5是眼睛ReID是记忆卡尔曼加匈牙利是决策中枢三者缺一不可。2.2 部署加速为什么优先选TensorRT同样一张NVIDIA显卡ONNX Runtime可能只把PyTorch模型提速30%TensorRT却常常能压到2到3倍延迟。关键区别在于TensorRT在做层融合、内存复用、内核优化时是站在“我熟悉这颗GPU架构细节”的前提下去做编译的。它会自动把权重和偏置合并、把激活层和前一个卷积融合还能为不同batch size挑选最合适的CUDA kernel这些都是泛化推理框架很难复刻的。对比维度TensorRTONNXRuntime GPUOpenVINO延迟优化深度很深算子级融合中等中等FP16/INT8支持原生且成熟FP16一般INT8较麻烦部分支持算子覆盖有缺需插件覆盖好覆盖一般部署体积序列化engine小依赖runtime和库依赖IPC库锁定程度绑定NVIDIA生态跨平台宽绑定Intel为主表格里供电但不能只看“谁最好”。如果你的目标是Jetson Orin或者服务器上的T4、A100TensorRT几乎是绕不开的选择因为这些平台的官方例程、算子加速、量化工具全都在往TRT上收敛。如果设备上还保留了CPU兜底推理需求我常用ONNX Runtime做回退路径两个engine并存不影响主战场。2.3 FP16与INT8量化该先上哪个TensorRT部署行人检测跟踪最常见配置是先开FP16再谈INT8。FP16对显卡的要求低GTX10系列之后的NVIDIA卡基本都能跑转换成本肉眼可见显存占用减半带宽压力下降帧率往往能翻倍。对应代价是尾数精度降低当行人距离远、目标只有十几个像素时检测框的置信度和定位精度会下滑。INT8比FP16更快但对YOLOv5来说是个危险选项。它需要你准备校准数据让TensorRT统计每一层激活值的分布才能把浮点计算映射到8位整数。行人检测面对的是对比度低、遮挡多、尺度变化极大的场景校准集稍微选偏模型就可能对某一类行人“选择性失明”。我的习惯是先让FP16版本跑通全流程再集成一个INT8并行评测版本在真实监控片段上逐帧对比检测召回率而不是看哪张卡跑得快就切哪边。3. 从.pt到.engineTensorRT环境与模型转换全流程不少人在这一步翻车。YOLOv5的.pt文件不能在TensorRT里直接加载你首先要把它导出成ONNX再由TensorRT把ONNX编译成.engine文件。环境版本配不对后面全部白费。3.1 版本选型GTX1070这类老卡怎么锁组合TensorRT是一个对版本组合非常挑剔的组件。它的安装包通常不带CUDA、cuDNN只是提供一个库文件和Python wheel运行时按预编译的版本规则去匹配驱动、CUDA runtime和cuDNN版本任意一个不匹配就会在初始化时报错。我刚在一个Pascal架构的GTX1070上做部署时走了两步错第一步是直接装最新版TensorRT初始化失败第二步是装了一个带CUDA 12的conda环境结果显卡驱动太老跑不动。最后退回的安全组合是Ubuntu 20.04、显示驱动470、CUDA 11.8、cuDNN 8.6、TensorRT 8.5.3。这个组合在GTX1070上很稳定Jetson上则直接使用JetPack自带的版本即可不需要另外装。组件版本说明操作系统Ubuntu 20.04 / 22.04老卡优先20.04显卡驱动470.x以上见过一批CUDA11.8老架构最稳cuDNN8.6与CUDA 11.8配套TensorRT8.5.3或8.6比10.x更适合GTX1070创建conda环境时别把TensorRT塞进YOLOv5训练环境里两个环境的依赖经常起冲突。我一般单独开一个conda create -n trt python3.8只装cudatoolkit、pycuda和TensorRT wheel训练环境继续留在另一个环境两者互不污染。3.2 YOLOv5导出ONNX与trtexec转换命令YOLOv5仓库自带export.py导出ONNX的命令很常规但有几个参数要严格按照部署需求来定。--img-size要和后续TRT输入尺寸保持一致--batch决定是否导出动态batch--opset建议设12或更高否则某些算子不被TRT识别。# 在YOLOv5项目目录下执行 python export.py --weights yolov5s.pt --include onnx --img 640 --batch 1 --opset 12这一步做完得到yolov5s.onnx。需要注意的是这个ONNX里没有NMS算子输出仍然是[1, 25200, 85]。在TensorRT工程里我强烈不建议你用NMS插件把后处理塞进engine因为NMS插件在不同TRT版本里接口变化大调试起来极其痛苦。更稳妥的办法是导出原始模型把后处理留在应用层自己写。用trtexec工具把ONNX编译成engine核心参数是开启FP16、指定动态batch范围trtexec \ --onnxyolov5s.onnx \ --saveEngineyolov5s_fp16.engine \ --fp16 \ --minShapesimages:1x3x640x640 \ --optShapesimages:1x3x640x640 \ --maxShapesimages:16x3x640x640trtexec会把ONNX中的weight和bias融合、把卷积加激活合并同时按最小、最优、最大三个shape范围生成一份带动态shape的优化计划。minShapes和maxShapes不是随便写的它们决定了后续运行时能接受的输入尺寸范围开太大反而让每次推理多出额外的shape调度开销。常见做法是min 1、opt 1、max 16既能满足一般批量处理又不至于让优化时间过长。3.3 DeepSORT特征网络一起转成engineDeepSORT的ReID特征提取网络通常是一个独立的分类网络。传统实现里用的是在行人ReID数据集上预训练过的模型输出特征维度通常是512或256。跟踪器拿到检测框后会先对框内图像裁剪缩放再送进特征网络提取向量然后存进每个轨迹的特征队列里。转换过程和YOLOv5类似但要额外留意两件事。第一导出ONNX前必须切换到eval模式并且冻结BatchNorm层否则统计量会被重新计算特征分布完全漂移。第二输入尺寸要和训练时一致常见是128宽、256高的宽高比裁图的时候要按这个比例resize。import torch model load_reid_model(osnet_x1_0, pretrainedTrue) model.eval() # 关键禁止BN层更新 for m in model.modules(): if isinstance(m, torch.nn.BatchNorm2d): m.track_running_stats False m.eval() dummy torch.randn(1, 3, 256, 128, devicecuda) torch.onnx.export( model, dummy, reid.onnx, input_names[input], output_names[feat], opset_version12, dynamic_axes{input: {0: batch}, feat: {0: batch}} )导出完成后再用trtexec把reid.onnx转一遍命令和YOLOv5几乎一样只是shape改成input:1x3x256x128。转出来的reid_fp16.engine体积会比YOLOv5小一个数量级但推理时它通常要运行N次因为每帧每一个检测框都要过一遍特征提取。所以ReID网络的优化反而更值得做实际工程里它的耗时经常超过YOLOv5。4. 加载TensorRT引擎完成行人检测跟踪主循环模型转换只是热身真正动手是在业务代码里把engine跑起来。这里每一步都要踩准尤其是TensorRT的binding机制容易和普通PyTorch的Tensor概念搞混。4.1 引擎加载与YOLOv5输出后处理TensorRT运行时要做的不是加载模型而是反序列化engine文件。engine里记录的是GPU可执行计划加载后创建context然后分配输入输出buffer。注意PyTorch的Tensor是在显存里动态分配而TensorRT希望你在推理前预留好整块buffer推理时它把数据原地读写不反复分配内存。import tensorrt as trt import numpy as np def load_engine(engine_path): runtime trt.Runtime(trt.Logger(trt.Logger.WARNING)) with open(engine_path, rb) as f: engine runtime.deserialize_cuda_engine(f.read()) context engine.create_execution_context() return engine, context engine, context load_engine(yolov5s_fp16.engine) # 查询binding信息并固定shape input_idx engine.get_binding_index(images) output_idx engine.get_binding_index(output) context.set_binding_shape(input_idx, (1, 3, 640, 640)) # 分配显存缓冲示意 d_input np.empty((1, 3, 640, 640), dtypenp.float32) d_output np.empty((1, 25200, 85), dtypenp.float32)set_binding_shape是动态shape最关键的调用它必须在每次execute前确认一遍。如果你朝着batch 16的方向优化又只设置了batch 1的shape运行时会有隐式的形状约束报错常常是“binding shape not符合optimization profile”。固定batch1部署的话整个项目会简单很多本文后续代码按batch 1跑。后处理阶段YOLOv5原始输出的坐标需要经过letterbox反转才能映射回原图。这儿有一个非常容易踩的偏差来源预处理时给图像等比缩放并填充灰边后处理时忘了把填充偏移量减回去结果跟踪框全偏移到右下角。def postprocess_yolov5(raw, ratio, pad_w, pad_h, conf_thres0.4): preds raw[0] # [25200, 85] scores preds[:, 4] * preds[:, 5:].max(axis1) valid scores conf_thres if not valid.any(): return np.empty((0, 4)), np.empty((0,)) boxes preds[valid][:, :4].copy() cx, cy, w, h boxes.T x1 (cx - w / 2 - pad_w) / ratio y1 (cy - h / 2 - pad_h) / ratio x2 (cx w / 2 - pad_w) / ratio y2 (cy h / 2 - pad_h) / ratio keep nms(x1, y1, x2, y2, scores[valid]) return np.stack([x1, y1, x2, y2], axis1)[keep], scores[valid][keep]ndarray的shape和维度必须严格匹配engine里的binding定义写错的常见后果是报“binding data type mismatch”或“misshaped tensor”。调试阶段建议先把输入固定成一张你清楚的测试图打印raw输出的数值范围确认预处理归一化不是0~255直接灌进去。4.2 DeepSORT跟踪器接入与参数选择DeepSORT的tracker update接口接收的是“检测结果集合”每个检测结果至少要包含边框坐标和一个外观特征向量。边框坐标在传入前要转换成[x, y, w, h]格式这是卡尔曼滤波器状态定义所依赖的顺序前后颠倒会直接让滤波器计算发散。参数选择是这里的玄学但有几个基本规律。max_dist控制外观特征匹配的余弦距离阈值越小越严格人群中容易漏关联越大会让不同人串IDmax_age控制轨迹丢失多少帧后删除行人被遮挡时这个值调大能保持ID不丢但调太大又会在目标离开画面后残影停留过久n_init表示连续命中几帧后才把轨迹从“未确认”转成“已确认”设为三帧能滤掉静止背景噪声误检。参数典型值调节依据max_dist0.2~0.3行人密度高时调小max_iou_distance0.7遮挡严重场景适当放宽max_age50~70遮挡频繁时调大n_init3想快速出ID就调小想稳就调大nn_budget100轨迹历史特征队列上限在实际项目里DeepSORT仓库原版默认参数是给MOT16数据集调出来的直接搬到监控俯拍场景经常会出现刚起步就丢ID的情况。我一般先把max_age调大再逐步收紧max_dist用一段30秒真实视频反复看ID切换次数比对着论文里的数值抄有用得多。4.3 完整主循环串起推理、检测和跟踪把上面的部分串起来主循环的骨架并不长。读一帧letterbox推理后处理提取ReID特征执行tracker.update画框画ID。import cv2 import numpy as np engine, context load_engine(yolov5s_fp16.engine) reid_engine, reid_ctx load_engine(reid_fp16.engine) tracker build_tracker(max_dist0.25, max_age60, n_init3) cap cv2.VideoCapture(street.mp4) writer cv2.VideoWriter(out.mp4, fourcc, 25, (1920, 1080)) while True: ok, frame cap.read() if not ok: break # 1. YOLOv5预处理和推理 input_tensor, ratio, pad letterbox(frame) np.copyto(buffers[input], input_tensor) context.execute_v2(buffers) # 2. 后处理得到框和置信度 boxes, scores postprocess_yolov5(buffers[output]) # 3. 对每个框裁剪并用ReID网络提特征 detections [] for bx in boxes: crop crop_person(frame, bx, reid_size(128, 256)) feat extract_feature(reid_engine, reid_ctx, crop) # DeepSORT内部会把框转成xywh det Detection(tlwhbx_xywh(bx), confidence1.0, featurefeat) detections.append(det) # 4. 跟踪器更新维护ID tracker.update(detections) # 5. 可视化并输出 for track in tracker.tracks: if track.is_confirmed() and track.time_since_update 1: draw_box(frame, track.tlbr, track.track_id) writer.write(frame) cap.release() writer.release()这段代码把最关键的五步任务都展现出来了实际工程还要补上大量的内存复用和异常保护。注意context.execute_v2后面要接一次同步否则GPU还在执行时CPU已经拿到未完成的输出我在初版就吃过这个亏画面时好时坏后来强制切换成execute_async_v2加cuda.Event同步才稳定下来。5. 避坑与排查TensorRTYOLOv5DeepSORT高发问题这个组合的问题集中在环境兼容、动态shape、特征导出、精度损失四个方向。每条记录按现象、原因、解决来写都是我实际用时间换来的经验。5.1 TensorRT 10.x 与GTX1070的兼容性门槛现象在GTX1070上安装新版TensorRT构建engine时报初始化错误提示CUDA driver版本或GPU架构不支持但CUDA和cuDNN版本明明都符合要求。原因TensorRT版本对GPU计算能力的下限在逐步抬升。Pascal架构计算能力是6.1早年的TRT 8.x还能覆盖较新的10.x版本优化重心放到Ampere以上架构对老卡的支持被明显弱化代码路径里甚至可能直接拒绝创建context。解决部署目标老显卡时先查算力再选版本。GTX1070锁定CUDA 11.8 TensorRT 8.5或8.6不要一味追新。Jetson系列则直接用JetPack预置的TRT版本不要在板子上手动覆盖因为互不匹配的驱动层会让你连demo都推不起来。5.2 dynamic shape能构建但运行时报错现象trtexec构建engine成功但加载到业务代码后第一次execute就报“Some input tensors have shape not meeting optimization profile”或直接段错误。原因构建时用了--minShapes、--optShapes、--maxShapes但运行时没有调用context.set_binding_shape或者传入的shape落入了min到max之外的区间。我见过同时开动态batch和不定尺寸的版本两者互相叠加quick一眼看过去shapes完全没问题但runtime在内部做profile选型时找不到对应项。解决每次推理前显式调用set_binding_shape并且把调用结果锁在一个变量里做断言。固定batch1部署的话就在engine加载后设置一次后续不再改动即可。尽量让每次输入尺寸保持一致除非你确定自己需要多尺寸处理否则动态shape带来的灵活性远小于它带来的排错成本。5.3 ReID模型导出后特征抖动ID被反复切换现象YOLOv5检测框很稳但DeepSORT里的ID每几秒就变一次同一个行人走几步就被当成新目标。原因ReID网络导出ONNX时没有冻结BatchNorm层或导出时用了没有eval模式的代码路径BN层在推理时重新计算batch统计量导致同一个人的特征在不同帧剧烈波动。另一个常规原因是裁剪区域没有做和训练时一致的填充行人紧贴框边缘。解决导出前遍历所有BatchNorm层设置track_running_statsFalse并调用model.eval()。再用导出后的ONNX跑同一张图两次对比两次输出向量的余弦距离如果距离不是接近1说明模型导出仍然有随机性回去检查预处理。还有一个必要动作把ReID的输入尺寸固定在128x256不要接受logistic这样前后两帧的特征空间才对齐。5.4 FP16推理丢小目标检测框被后处理吞掉现象FP16和FP32在同一个YOLOv5模型上近距离行人都能检出但视频中段、远景里的行人偶尔消失跟踪轨迹由此断掉。原因FP16的尾数精度有限在低对比度、小尺度目标上检测头的回归头和分类头输出都更容易触发精度损失。更深层的原因是后处理里用固定的conf_thres一刀切小目标置信度本来就偏低FP16把这个值再压一截正好跌破阈值。解决不要单独改一个阈值要多路并行对比。把FP16和FP32的置信度分布打到同一张图上看阈值重叠区间再决定把conf_thres从0.4下调到0.3还是0.25。顺带把NMS的IoU阈值从默认0.45放宽到0.5让密集人群里的低置信度检测有机会保留仅靠跟踪器的IoU匹配来消重实测对ID维持有效。更彻底的做法是对敏感层开启FP32混合精度让YOLOv5的最后一层head保持全精度这套路在TRT里通过层精度设置就能实现推荐优先尝试。6. 验证与进阶把fps测准再加流水线优化先别急着看整段帧率帧率会骗人。TensorRT推理绑定了GPU和显存CPU预处理和DeepSORT的特征提取同样耗资源三处叠加后才是一个真实可用的数字。我刚跑通时整机显示35fps实际上CUDA内核只在主线程里单个跑GPU空闲一大半。把耗时拆开来看才知道YOLOv5全流程只占14毫秒ReID特征提取反而占12毫秒预处理和NMS在CPU端占了剩余大半时间。测量时用CUDA事件测GPU段用Python的time.perf_counter测CPU段各段打上时间戳至少统计200帧取中位数不要取均值因为均值会被首帧的engine预热拉高。进阶方向是流水线化。把预处理、TRT推理、后处理加跟踪拆成独立线程帧采集线程不断往队列里塞原图推理线程固定从队列里取一帧处理跟踪线程消费检测结果。在内存充足的前提下流水线能把帧率稳到单线程1.5倍以上。具体做法是开一个预分配的双缓冲给预处理和推理各维护一份显存buffer避免每一帧重新分页和拷贝。如果目标平台是树莓派5这种非NVIDIA边缘板卡TensorRT这套方案就不适用需要换成NCNN或RKNN系列工具链那是另一个生态了。但如果你手上是Jetson Nano、Orin这类设备这套代码几乎可以原样平移只把环境换成JetPack自带版本即可。我最后养成的习惯是每改一个参数先录一段包含遮挡、密集、远距离三类场景的测试视频把ID切换次数和检测帧数统计起来做成一页对比数据再决定保留还是回滚。很多隐藏在细节里的误差不是靠肉眼看一组gif能发现的。希望这篇笔记能帮你少走几趟弯路。本文还有配套的精品资源点击获取
网站建设高端定制企业官网