简介:这是一套基于TensorRT加速引擎的YOLO部署项目,同时实现实例分割与目标检测功能,并提供C++和Python两种开发语言版本,支持在Linux与Windows系统上运行。资源包共含1523个文件,主要内容有C++头文件与源文件、Python脚本、工程构建配置、动态与静态库、可执行程序,以及技术文档等,压缩包整体约314MB,目录结构清晰,便于定位所需代码和资料。目前已有307人学习下载。这套资源适合需要部署目标检测能力的开发者、研究人员及企业工程人员,可应用于实时监控、工业产品质检、智能交通、医疗图像辅助分析等场景。除完整工程外,还附带了依赖库、运行环境和部署说明,能够帮助使用者快速搭建TensorRT推理环境,直接验证分割与检测效果,并为后续跨平台集成和性能优化提供基础。
1. TensorRT 部署 YOLO 值不值:从 50ms 到 3ms 的差距与适合的人
一条产线上用 YOLO 做瑕疵检测,同样的 640×640 输入,CPU 推理要 50ms 一帧,换到 T4 上用 TensorRT 压一遍能到 3ms 左右。这不是模型变了,是部署方式变了。这类问题在监控、工业质检、智能交通里几乎天天遇到:模型训出来了,跑不动,或者跑起来的延迟撑不住多路视频流。这个资源解决的就是这一段——把 YOLO 的目标检测和实例分割同时部署到 TensorRT 上,C++ 和 Python 两套代码都给好,Linux 和 Windows 都能跑,拿过来改改路径就能复现。适合三类人:要把检测功能集成进现有项目的开发者,需要验证算法落地性能的研究人员,以及想把 YOLO 塞进监控和质检系统的实施工程师。
2. 从 YOLO 权重到 TensorRT engine:ONNX 这层转换避不开
2.1 为什么必须经过 ONNX
YOLO 的权重一般是 PyTorch 的 .pt 文件,TensorRT 不能直接消费它,必须先把模型转成 ONNX,再交给 TensorRT 的 parser 解析成 engine。这个过程看起来多了一层,实际是给两端解耦:PyTorch 只管训练,TensorRT 只管推理,ONNX 是中间语言。很多第一次接触的人试图跳过 ONNX 直接用 .pt 文件去 build engine,浪费一晚上最后还是要回来走 ONNX 这条路。
导出这一步通常用官方脚本完成。以 YOLOv8 为例,导出检测模型和分割模型分别是:
yolo export model=yolov8s.pt format=onnx opset=12 yolo export model=yolov8s-seg.pt format=onnx opset=12opset 建议保持在 12 到 17 之间。TensorRT 对过新的 opset 支持不一定及时,太旧的又可能丢失一些算子语义。如果你是 YOLOv5,导出路径是python export.py --include onnx --opset 12,原理一样,只是脚本入口不同。导出完成后先用onnx库快速检查一下输出节点,确认输出名和 shape,这一步能省掉后面很多定位问题的时间。
2.2 检测和分割的输出头不是一回事
这是整个项目里最容易翻车的地方。目标检测模型转出的 ONNX 通常是一个输出,形状是[1, 84, 8400]。这个 84 由 4 个框坐标(cx, cy, w, h)加 80 个 COCO 类别的置信度组成,8400 是三个尺度特征图上的候选框总数(80×80 + 40×40 + 20×20)。
实例分割模型的 ONNX 则有两个输出:一个是[1, 116, 8400],其中 4 是框坐标,80 是类别置信度,多出来的 32 是每个候选框对应的 mask 系数;另一个是[1, 32, 160, 160],这是原型 mask。最终掩码等于原型 mask 和 mask 系数的加权和,后面 Python 和 C++ 后处理里都要用这个关系。
拿到的资源包里如果同时有检测和分割两个任务,建议先用 Netron 打开 onnx 文件看一眼输出头,两个任务的输出个数和维度不同,后处理代码也完全不同。写推理脚本前先确认输出头的名字和顺序,不要盲信代码里的变量名。
2.3 构建 engine:trtexec 和 Python API 两条路
TensorRT 的 engine 构建有两种常见方式。第一种是直接用 trtexec 命令行,适合快速验证转换链路通不通:
trtexec --onnx=yolov8s-seg.onnx --saveEngine=yolov8s-seg.engine --fp16 --memPoolSize=workspace:2G--fp16开启半精度推理,对 YOLO 这类模型一般能带来 30% 到 50% 的提速,精度损失很小;--memPoolSize=workspace:2G限制构建时的工作空间,显存小的卡建议调低到 1G。如果后续要在多路视频流场景下跑,建议在 trtexec 命令里加上--minShapes=images:1x3x640x640 --optShapes=images:4x3x640x640 --maxShapes=images:8x3x640x640,让 engine 支持动态 batch。注意:动态 batch 的 engine 在 Python 和 C++ 代码里都要手动调用set_input_shape,否则推理时直接报错。
第二种是用 Python API 在程序里构建,适合要做批量化处理或需要精细控制构建参数的场景:
import tensorrt as trt logger = trt.Logger(trt.Logger.INFO) builder = trt.Builder(logger) network = builder.create_network(1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)) parser = trt.OnnxParser(network, logger) with open("yolov8s-seg.onnx", "rb") as f: parser.parse(f.read()) config = builder.create_builder_config() config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 2 << 20) config.set_flag(trt.BuilderFlag.FP16) engine = builder.build_serialized_network(network, config) with open("yolov8s-seg.engine", "wb") as f: f.write(engine)重点是EXPLICIT_BATCH这个标志:YOLO 系列模型的 ONNX 导出默认是动态维度,不加这个标志没办法解析输入维度。构建完的 engine 是一个序列化文件,里面包含推理时的 kernel 调度方案和权重,但注意它绑定当前 GPU 架构和 TensorRT 版本,换机器换驱动就要重建,这是后面坑章里要展开的事。
3. 环境搭建:Linux 与 Windows 的版本矩阵与两种工程的骨架
3.1 CUDA、cuDNN、TensorRT 的版本对应关系
TensorRT 对版本敏感,这个项目的坑一半埋在环境里。老手会先看 NVIDIA 官方兼容矩阵,这里给几个我实际跑通过的组合,方便你对照自己的显卡驱动来选:
| 组合 | CUDA | cuDNN | TensorRT | 适合场景 |
|---|---|---|---|---|
| 组合 A | 11.6 | 8.4 | 8.4.1 | Ubuntu 20.04,老卡 T4 / V100 |
| 组合 B | 11.8 | 8.6 | 8.5.3 | 我常用的组合,稳定性最高 |
| 组合 C | 12.0 | 8.9 | 8.6.1 | 40 系新卡,Windows 原生跑 |
一个原则:如果已经装了 CUDA 12.x,就别硬塞 TensorRT 8.4,版本不匹配的典型症状是 engine 节点报错或者构建到一半崩掉。T4 这类数据中心卡用组合 A 或 B 都行,消费级 30 系、40 系卡优先考虑 CUDA 11.8 或 12.0。
3.2 Linux 端:Ubuntu 上的安装与路径配置
Linux 下推荐用 tar.gz 包解压安装,不用 deb,因为 deb 会把库文件散到系统目录里,后续找路径麻烦。TensorRT 的 tar.gz 解压后,核心库在lib/下,头文件在include/下,把这两个目录加到环境变量即可:
export TRT_RELEASE=/opt/TensorRT-8.5.3.1 export LD_LIBRARY_PATH=$TRT_RELEASE/lib:$LD_LIBRARY_PATH export CUDA_HOME=/usr/local/cuda-11.8装好之后用python3 -c "import tensorrt; print(tensorrt.__version__)"验证 Python 端,用trtexec --version验证命令行工具。如果 import 报错,基本是LD_LIBRARY_PATH没包含 TensorRT 的 lib 目录,这是 ubuntu 安装 tensorrt 最常见的状况。
CMake 工程里要做的事很简单:把 TensorRT 的 include 和 lib 路径显式指出来,不要指望系统自动找到。
set(TRT_INCLUDE_DIR "/opt/TensorRT-8.5.3.1/include") set(TRT_LIB_DIR "/opt/TensorRT-8.5.3.1/lib") target_include_directories(yolo_infer PRIVATE ${TRT_INCLUDE_DIR}) target_link_libraries(yolo_infer PRIVATE nvinfer nvonnxparser)注意链接的是nvinfer和nvonnxparser两个库,只链nvinfer的话,OnnxParser 相关代码会链接失败。
3.3 Windows 端:VS 工程与 DLL 依赖
Windows 上跑这个项目,建议别用 WSL 绕,TensorRT 的 Windows 原生库表现更稳定,而且资源包里 C++ 工程本来就是按 Windows 目录结构组织的。Visual Studio 2019 或 2022 的工程里,需要配置三个地方:VC++ 目录 → 包含目录指向 TensorRT 的include,库目录指向lib,然后在链接器输入里加上nvinfer.lib、nvonnxparser.lib、cudart.lib。
Windows 下最隐蔽的坑是 DLL 路径。TensorRT 的bin目录和 CUDA 的bin目录必须加到系统 PATH,否则编译通过但运行时提示找不到nvinfer.dll,或者找不到cudart64_*.dll。项目代码没问题、报错却发生在启动阶段,优先查这个。
Python 端在 Windows 下需要额外注意 pycuda 的版本要和 CUDA 匹配,pip install pycuda装出来的默认版本可能不匹配你装的 CUDA,建议用源码编译或者直接找对应 CUDA 版本的预编译包。如果只是跑 Python 脚本不碰 pycuda,用 tensorrt 自带绑定也行。
4. Python 推理实现:engine 加载、letterbox 预处理与后处理解码
4.1 engine 加载与输入输出 buffer 分配
Python 端的推理骨架不复杂,核心就三步:反序列化 engine、创建 context、分配 host 和 device 端的 buffer。
import tensorrt as trt import numpy as np import ctypes logger = trt.Logger(trt.Logger.INFO) runtime = trt.Runtime(logger) with open("yolov8s-seg.engine", "rb") as f: engine = runtime.deserialize_cuda_engine(f.read()) context = engine.create_execution_context() context.set_input_shape("images", (1, 3, 640, 640))这里set_input_shape是必须的,因为前面导出时模型是动态维度,不在推理前显式指定,execute_v2会直接抛错。绑定名称不要写死,用engine.get_tensor_name(i)遍历拿,不同版本导出时输出节点命名可能不一样,写死字符串在换模型文件时容易出问题。
4.2 letterbox 预处理:比例、填充和坐标还原
YOLO 系列模型的预处理要求把任意尺寸原图等比缩放后填充成 640×640,这个操作叫 letterbox。缩放比例和填充尺寸必须在预处理时记录,后处理还原框坐标时要用同一套参数。
def letterbox(img, new_shape=(640, 640), color=(114, 114, 114)): h, w = img.shape[:2] ratio = min(new_shape[0] / h, new_shape[1] / w) new_h, new_w = int(h * ratio), int(w * ratio) dw, dh = (new_shape[1] - new_w) // 2, (new_shape[0] - new_h) // 2 resized = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LINEAR) canvas = np.full((new_shape[0], new_shape[1], 3), color, dtype=np.uint8) canvas[dh:dh + new_h, dw:dw + new_w] = resized return canvas, ratio, (dw, dh) img, ratio, (dw, dh) = letterbox(original_img) blob = img.transpose(2, 0, 1)[None].astype(np.float32) / 255.0最后一步除以 255 的归一化不可省,YOLO 在训练时的输入就是归一化后的,推理时不归一化,置信度输出会整体漂移。ratio和(dw, dh)先存好,后处理时每个框都要用它换算回原图坐标,这就是新手最容易漏掉的地方。
4.3 检测后处理:把 8400 个候选框裁成最终结果
推理执行后拿到输出,形状是[1, 84, 8400],维度顺序是[C, N],不是[N, C]。常规做法是先转置成[8400, 84]再做解算,忽略这个布局的后果是置信度和坐标混在一起,画出来的框全是乱的。
outputs = [context.get_tensor(name) for name in output_names] det_out = outputs[0][0].transpose(1, 0) # [8400, 84] class_ids = det_out[:, 4:].argmax(axis=1) confidences = det_out[:, 4:].max(axis=1) mask_conf = outputs[0][0][4 + 80:4 + 80 + 32] # [32, 8400] keep = np.where(confidences > 0.25)[0] boxes = det_out[keep, :4] scores = confidences[keep] cls = class_ids[keep]YOLOv8 输出的框坐标是cx, cy, w, h,需要换算成x1, y1, x2, y2,再按 letterbox 的 ratio 和 pad 还原回原图。还原之后用torchvision.ops.nms或cv2.dnn.NMSBoxes做非极大值抑制,NMS 的 IoU 阈值一般取 0.45 到 0.5,置信度阈值取 0.25 是起步值,实际部署按误检和漏检的容忍度去调。
4.4 实例分割 mask 还原:原型和系数的矩阵乘法
实例分割多出来的工作是把 mask 系数和原型 mask 组合起来。这一步的公式是掩码等于 sigmoid 后的原型矩阵与系数矩阵相乘,得到一个[160, 160]的掩码,再缩放到原始尺寸。
proto = outputs[1][0] # [32, 160, 160] coeff = mask_conf[:, keep] # [32, num_keep] coeff = coeff.transpose(1, 0) # [num_keep, 32] masks = (proto.reshape(32, -1).T @ coeff.T).T masks = 1.0 / (1.0 + np.exp(-masks)) # [num_keep, 160*160]masks每一行是一个候选框的 mask,注意 reshape 成[160, 160]后只是 letterbox 画布上的结果,需要按前面记录的 ratio 和 pad 裁剪出原图中的有效区域。实例分割的 mask 是低分辨率的(160×160),边缘会有锯齿,如果项目对边缘质量要求高,可以在还原后做一次轻微的高斯模糊再二值化。这一步放在 Python 端做没问题,C++ 端就是把这个矩阵乘法用原生循环实现,后面一章拆开讲。
5. C++ 推理实现:IRuntime、IExecutionContext 与双语言对齐的细节
5.1 C++ 端加载 engine 的标准骨架
C++ 端的推理流程和 Python 完全对应,只是所有显存操作要手动管理。加载 engine 用IRuntime接口,反序列化得到ICudaEngine,再从 engine 创建IExecutionContext:
IRuntime* runtime = createInferRuntime(logger); std::ifstream file("yolov8s-seg.engine", std::ios::binary); std::vector<char> data(std::istreambuf_iterator<char>(file), {}); ICudaEngine* engine = runtime->deserializeCudaEngine(data.data(), data.size()); IExecutionContext* context = engine->createExecutionContext(); const char* inputName = engine->getIOTensorName(0); context->setInputShape(inputName, Dims4{1, 3, 640, 640});getIOTensorName的作用是遍历拿到输入输出节点的名字,不要假设第 0 个一定是输入,某些模型导出后输出排在前面。setInputShape和 Python 端一样,只对导出时声明了动态轴的 ONNX 模型才需要调用,如果你的 engine 是从静态 batch 的 onnx 构建的,这一步可以跳过。
5.2 解码循环和 NMS:C++ 端的布局与性能账
C++ 端的解码要比 Python 多一层思考:输出 buffer 是连续内存,你要自己算清楚每个元素的位置。YOLOv8 检测头输出布局是[C, N],第 4 到第 83 行是类别分数,遍历时按列读数据:
void decodeYolo(const float* output, int numBoxes, int numClasses, float confThres, std::vector<Box>& boxes) { for (int i = 0; i < numBoxes; ++i) { const float* ptr = output + i * (4 + numClasses); float bestClass = 0.0f; int bestIdx = -1; for (int j = 0; j < numClasses; ++j) { float score = ptr[4 + j]; if (score > bestClass) { bestClass = score; bestIdx = j; } } if (bestClass < confThres) continue; Box b; b.cx = ptr[0]; b.cy = ptr[1]; b.w = ptr[2]; b.h = ptr[3]; b.score = bestClass; b.label = bestIdx; boxes.push_back(b); } }注意这段代码里索引的计算方式:因为内存是连续的,第 i 个候选框的起始位置是i * (4 + numClasses),类别分数从+4开始。配好缓存命中和循环展开,这个解码循环在 8400 个候选框规模下耗时一般不到 1ms,属于可忽略的开销。
NMS 在 C++ 端不推荐自己写 O(n²) 循环,工程上我一般直接用std::sort按置信度排序,再做线性的重叠剔除,数据量小,逻辑清晰,调试也方便。追求极致性能可以后续换成 GPU 端 NMS 插件,但先跑通链路更重要。
5.3 与 Python 对齐时的三个检查点
C++ 和 Python 两套代码如果跑出来的结果不一样,优先排查三个点。第一是预处理参数,ratio、dw、dh的计算方式两边必须完全一致,比如填充值的默认颜色是 114 还是 0,两边不一致会导致同一张图推理结果完全不同。
第二是输出 buffer 的数据精度。如果构建 engine 时开了--fp16,输出端的数据类型可能是FP16,C++ 端如果没有把半精度转成 float 就直接按float*去解析,拿到的是垃圾数据,这种情况 log 完全没有报错,但检测结果全乱。处理方式是在拿到输出 buffer 后,先查engine->getTensorFormat(outputName),是 FP16 就做一次 half 转 float 的转换。
第三是坐标还原的逻辑。C++ 端还原坐标时,x = (cx - dw) / ratio这行公式要写在 decode 之后、NMS 之前还是之后,直接影响了画的框在原图上的位置。我的习惯是在 decode 阶段就把坐标还原回原图再进 NMS,这样后续不管接的是 OpenCV 画框还是传给别的模块,拿到手的都是原图坐标,也方便调试。
6. 部署避坑与验证:trtexec 基准、三类高频报错的排查顺序
先把验证这一步放在最前面。任何一次改造之后,先用 trtexec 跑一个基准,拿到整机延迟和吞吐的锚点,再判断自己的代码写的对不对:
trtexec --loadEngine=yolov8s-seg.engine --shapes=images:1x3x640x640输出里的GPU Compute Time是单帧推理延迟,Throughput是吞吐。如果你的程序整体延迟比 trtexec 高了一倍以上,先怀疑前处理或后处理耗时,不要盯着推理代码改。
常见问题排查顺序,按我实际踩过的频率排:
第一,engine 换机器就起不来。现象是deserializeCudaEngine返回空指针,或者推理时输出全 0。原因是 TensorRT 的 engine 文件和 GPU 架构、TensorRT 版本、驱动版本都绑定,不是跨平台通用的可执行文件。解决方法是每台机器各自用 trtexec 重新构建,不要把构建好的 engine 文件直接拷到别的机器上,这点在 Linux 和 Windows 之间尤其明显。
第二,检测框整体偏移但置信度正常。现象是框能框住物体但位置偏左上或偏右下,主要是 letterbox 的 pad 计算和后处理还原时用的不是同一组dw、dh。解决方式是把预处理参数和还原公式放到同一个公共段代码里,两边共用,不要在 Python 里写一遍、C++ 里再重新推导一遍。从那以后我每次部署都强制走一遍 trtexec 基准验证,然后单独检查预处理参数是否双语言一致,这个习惯帮我省了不少排查时间。
第三,显存缓慢增长和推理耗时抖动。现象是程序跑几个小时之后显存溢出,或者每隔一段时间单帧耗时翻倍。原因一般是循环里反复创建IExecutionContext,或者 host 端 buffer 用了普通内存导致拷贝阻塞。解决方法是 context 创建一次复用,显存 buffer 也提前分配好,输入输出走 pinned memory,推理结束后记得释放 engine 和 context 对象,别偷懒不写清理。
另外一个容易被忽略的陷阱是动态 shape 下没设置最优 batch。多路视频流场景建议把检测输入做成[N, 3, 640, 640]的 batch 推理,N 根据显存来定,T4 上常见做法是 4 路或 8 路一个 batch。配合 RTSP 拉流,四路的耗时通常只比单路多 20% 到 30%,吞吐却接近线性上涨。
如果后续要在 Jetson 这类嵌入式设备上跑,DLA 核的调度和 INT8 量化校准是另一套门道,但前提是先把这套 C++ / Python 双端跑通。资源包里已经帮这两套语言写好了对应代码,拿到手后建议先看 docs 目录里的部署文档,把版本矩阵里的组合三选一配好,再从 Python 推理开始验证,最后切到 C++ 做集成,这条路走完基本能把 70% 的坑提前避开。希望帮到你。
本文还有配套的精品资源,点击获取