☰
TensorRT部署YOLO实例分割与目标检测:C++/Python跨平台实战
2026/10/3 2:46:09 网站建设 项目流程

简介:本资源是一套基于 TensorRT 与 YOLO 算法的深度学习部署项目,面向需要将目标检测与实例分割能力落地到实际工程中的开发者、算法研究人员及企业技术团队。项目同时提供 C++ 与 Python 两种实现方式,可在 Linux 和 Windows 平台上编译运行,帮助读者解决跨平台推理部署、性能优化与多语言集成等实际问题,适用于安防监控、工业质检、智能交通及医疗图像分析等场景。压缩包共 1523 个文件,约 314.08MB,以 hpp、h、cpp、cu 等 C++ 与 CUDA 源码,py 脚本,以及 cmake、vcxproj、sln 等构建工程文件为主,另含 dll、lib、engine、whl 等运行依赖与模型文件,并配有 md、txt 文档和示例图片,目录结构完整。目前已有 307 人学习下载。资源内含完整项目文档与可运行工程,读者可据此快速搭建环境、理解推理流程、对照 C++ 与 Python 两套代码进行集成与二次开发,并参考文档完成数据配置与结果验证。

1. TensorRT 部署 YOLO 实例分割与目标检测:一套代码同时吃下 Linux 和 Windows

同一个 YOLO 权重,PyTorch 里跑 30 FPS,换到 TensorRT 上能到 120 FPS,但代价是你要先把.pt转成.engine,再写一套 C++ 或 Python 的推理前后处理,还得同时照顾 Linux 和 Windows 两套编译环境。这就是「TensorRT 部署 YOLO 项目:实例分割 + 目标检测,C++ 和 Python 两种方式,支持 Linux 和 Windows」这个标题真正要解决的问题。它面向的是已经能训练出 YOLO 模型、但卡在工程落地这一步的从业者:模型精度够了,延迟下不去,显存吃满,或者想把检测和分割统一到一个推理管线里。下面按「先立住原理、再动手复现、最后讲坑」的顺序拆开讲,C++ 和 Python 两条路都会给到可抄的命令和参数。

2. 为什么 TensorRT 能把 YOLO 推理压到极致:从计算图到 kernel 选择

2.1 TensorRT 到底对 YOLO 做了什么

YOLO 这类单阶段检测器,推理时的计算量集中在卷积、BN、激活和最后的 NMS 上。PyTorch 的 eager 模式每层单独调度 kernel,中间张量反复读写显存,这是延迟的主要来源。TensorRT 做的事情可以概括成三层:图层融合、精度校准、kernel 自动调优。

图层融合最典型的是 Conv + BN + SiLU 合并成一个节点,YOLOv8/v11 的 backbone 里这种结构密集出现,融合后 kernel 启动次数能降三到四成。精度校准指的是 FP16 和 INT8:FP16 在 Turing 及以后的卡上几乎无损,INT8 需要校准集生成 scale 因子,检测头对量化更敏感,实例分割的 mask 分支尤其容易掉点。kernel 自动调优是 TensorRT 在 build 阶段用实际 shape 去试不同的 CUDA 实现,把最快的那个固化进 engine,所以同一个模型在不同卡上 build 出来的 engine 不能混用。

实例分割比纯检测多一条 mask 分支,YOLOv8-seg 的输出是[1, 116, 8400]这种拼接张量,前 4 个是 box,中间 80 个是类别分数,后 32 个是 mask 系数。TensorRT 对这条分支的优化空间比 backbone 小,所以分割模型的加速比通常低于纯检测,这点要有预期。

2.2 选 C++ 还是 Python:先看部署边界

Python 路线的优势是开发快,pycuda或cuda-python配合tensorrt包,几十行就能跑通,适合验证和内部工具。但 Python 的 GIL 和解释器开销在高并发场景下会成为瓶颈,而且目标机器上要装匹配版本的 Python、CUDA、TensorRT,依赖地狱很常见。

C++ 路线的优势是部署干净,编译出一个可执行文件加几个 so/dll,目标机器只要有 CUDA runtime 和 TensorRT runtime 就能跑,延迟也更稳。代价是前后处理要自己写,图像解码、letterbox、NMS、mask 后处理都得手撸,调试成本高。

我的建议是:先用 Python 把 engine 跑通、把前后处理逻辑验证对,再把这套逻辑平移到 C++。两条路共用同一个.engine文件,这是关键——engine 是硬件和 TensorRT 版本相关的,但和调用语言无关。

2.3 环境版本怎么配才不翻车

TensorRT 的版本和 CUDA、驱动、显卡架构强绑定。GTX 1070 是 Pascal 架构,算力 6.1,TensorRT 10.x 仍然支持,但 INT8 的收益有限,FP16 在 Pascal 上也没有 Tensor Core 加速,所以 1070 上 FP16 和 FP32 差距不大。真正吃 FP16 红利的是图灵及以后的卡。

组件Linux 推荐Windows 推荐说明
CUDA11.8 / 12.211.8 / 12.2与 TensorRT 版本对应
TensorRT8.6 / 10.x8.6 / 10.x10.x API 有 breaking change
cuDNN8.9+8.9+分割后处理不直接依赖
Python3.8–3.103.8–3.103.11 部分 wheel 缺失
编译器GCC 9+MSVC 2019+C++17 起步

提示:TensorRT 8.x 和 10.x 的 API 差异不小,IBuilder的创建方式、enqueueV2到enqueueV3的迁移都要注意。选版本前先确认你的显卡驱动能支持到哪个 CUDA。

3. 从 .pt 到 .engine:导出、转换与精度选择

3.1 导出 ONNX 的四个必调参数

不管走 C++ 还是 Python,第一步都是把.pt转成 ONNX。Ultralytics 的导出接口封装得比较好,但有几个参数直接决定后面能不能转成功。

from ultralytics import YOLO # 加载训练好的分割或检测模型 model = YOLO("yolov8s-seg.pt") # 检测模型换成 yolov8s.pt # 导出 ONNX,关键参数逐个说明 model.export( format="onnx", opset=12, # TensorRT 8.x 建议 12,10.x 可用 17 simplify=True, # 用 onnxsim 去掉冗余节点,减少转换报错 dynamic=False, # 固定 batch 和 shape,engine 性能最好 imgsz=640, # 必须和训练/推理一致 half=False, # 导出阶段不开 FP16,留给 TensorRT build 决定 )

opset选 12 是因为 TensorRT 8.6 对 13 以上的某些算子支持不完整,尤其是分割分支里的Resize和Concat。simplify=True会调用 onnxsim,能消掉导出时产生的一堆Identity和Constant节点,这些节点在 TensorRT 里会变成额外的 kernel。dynamic=False是性能关键:固定 shape 后 TensorRT 能做更激进的融合,动态 shape 会保留很多运行时判断。half=False是因为导出时转 FP16 会丢精度,精度控制应该放在 TensorRT build 阶段。

导出完成后用onnx.checker验一遍,再用onnxruntime跑一张图对比 PyTorch 输出,确认数值对齐再往下走。

3.2 trtexec 命令行转换:最快验证路径

TensorRT 自带的trtexec是最省事的转换工具,不用写代码就能验证 engine 能不能 build 出来。

# Linux 下 FP16 转换,固定 batch=1 trtexec \ --onnx=yolov8s-seg.onnx \ --saveEngine=yolov8s-seg-fp16.engine \ --fp16 \ --workspace=4096 \ --minShapes=images:1x3x640x640 \ --optShapes=images:1x3x640x640 \ --maxShapes=images:1x3x640x640 # Windows 下路径用反斜杠,其余一致 trtexec.exe --onnx=yolov8s-seg.onnx --saveEngine=yolov8s-seg-fp16.engine --fp16 --workspace=4096

--workspace=4096是给 TensorRT 的临时显存上限,单位 MB,分割模型建议给到 4096 以上,太小会 build 失败并报out of workspace。--fp16开启半精度,如果显卡不支持会静默回退到 FP32。minShapes/optShapes/maxShapes三个一致就是固定 shape,只写--shapes也行。

build 完成后 trtexec 会打印各层耗时和总延迟,重点看GPU Compute Time这一行。如果某个层耗时异常高,通常是该层没被融合,需要回看 ONNX 里是不是有 TensorRT 不支持的算子。

3.3 INT8 量化:分割模型要谨慎

INT8 能把延迟再压一半,但实例分割的 mask 分支对量化误差敏感。做法是准备 500 到 1000 张有代表性的校准图,用IInt8EntropyCalibrator2生成校准表。

import tensorrt as trt import numpy as np import pycuda.driver as cuda class Calibrator(trt.IInt8EntropyCalibrator2): def __init__(self, calib_files, batch_size=1, cache_file="calib.cache"): super().__init__() self.files = calib_files self.batch_size = batch_size self.cache_file = cache_file self.current = 0 # 预分配 device 内存,shape 必须和 engine 输入一致 self.device_input = cuda.mem_alloc(1 * 3 * 640 * 640 * 4) def get_batch_size(self): return self.batch_size def get_batch(self, names): if self.current + self.batch_size > len(self.files): return None batch = np.stack([self._preprocess(f) for f in self.files[self.current:self.current+self.batch_size]]) cuda.memcpy_htod(self.device_input, batch.astype(np.float32).ravel()) self.current += self.batch_size return [int(self.device_input)] def read_calibration_cache(self): try: with open(self.cache_file, "rb") as f: return f.read() except FileNotFoundError: return None def write_calibration_cache(self, cache): with open(self.cache_file, "wb") as f: f.write(cache)

校准集要覆盖实际场景的分布,否则量化后的检测框会漂。分割模型建议先跑 FP16,确认精度达标后再试 INT8,并且用同一批测试图对比 mask IoU,掉超过 2 个点就退回 FP16。

4. Python 推理管线:从 engine 加载到 mask 后处理

4.1 加载 engine 并跑通一次前向

Python 侧用tensorrt加pycuda是最直接的组合。核心是创建 runtime、反序列化 engine、分配绑定内存、执行execute_v2。

import tensorrt as trt import pycuda.driver as cuda import pycuda.autoinit import numpy as np import cv2 TRT_LOGGER = trt.Logger(trt.Logger.WARNING) def load_engine(engine_path): with open(engine_path, "rb") as f, trt.Runtime(TRT_LOGGER) as runtime: return runtime.deserialize_cuda_engine(f.read()) def infer(engine, image): # 预处理:letterbox 到 640x640,归一化到 0-1,HWC 转 CHW blob, ratio, pad = preprocess(image, 640) context = engine.create_execution_context() # TensorRT 10.x 用 get_tensor_name,8.x 用 get_binding_name input_name = engine.get_tensor_name(0) output_name = engine.get_tensor_name(1) context.set_input_shape(input_name, blob.shape) # 分配 host 和 device 内存 d_input = cuda.mem_alloc(blob.nbytes) output_shape = context.get_tensor_shape(output_name) h_output = cuda.pagelocked_empty(trt.volume(output_shape), dtype=np.float32) d_output = cuda.mem_alloc(h_output.nbytes) stream = cuda.Stream() cuda.memcpy_htod_async(d_input, blob.ravel(), stream) context.set_tensor_address(input_name, int(d_input)) context.set_tensor_address(output_name, int(d_output)) context.execute_async_v3(stream_handle=stream.handle) cuda.memcpy_dtoh_async(h_output, d_output, stream) stream.synchronize() return h_output.reshape(output_shape), ratio, pad

set_tensor_address是 TensorRT 10.x 的写法,8.x 用set_binding_shape加execute_v2。execute_async_v3比同步版本快,因为它不阻塞 host。输出 shape 对分割模型是[1, 116, 8400],检测模型是[1, 84, 8400],这个差异决定了后处理分支。

4.2 检测头后处理:NMS 的阈值怎么定

输出张量要先转置成[8400, 116],再拆出 box、类别分数、mask 系数。box 是cx, cy, w, h格式,需要还原到原图坐标。

def postprocess_det(output, conf_thres=0.25, iou_thres=0.45): # output: [1, 84, 8400] -> [8400, 84] pred = output[0].T boxes = pred[:, :4] scores = pred[:, 4:] class_ids = scores.argmax(axis=1) confidences = scores.max(axis=1) # 置信度过滤 mask = confidences > conf_thres boxes, confidences, class_ids = boxes[mask], confidences[mask], class_ids[mask] # cxcywh -> xyxy xyxy = np.empty_like(boxes) xyxy[:, 0] = boxes[:, 0] - boxes[:, 2] / 2 xyxy[:, 1] = boxes[:, 1] - boxes[:, 3] / 2 xyxy[:, 2] = boxes[:, 0] + boxes[:, 2] / 2 xyxy[:, 3] = boxes[:, 1] + boxes[:, 3] / 2 # 按类别做 NMS keep = [] for c in np.unique(class_ids): idx = np.where(class_ids == c)[0] k = nms(xyxy[idx], confidences[idx], iou_thres) keep.extend(idx[k]) return xyxy[keep], confidences[keep], class_ids[keep]

conf_thres=0.25和iou_thres=0.45是 Ultralytics 的默认值,实际部署时如果漏检多就降 conf,误检多就升 conf。NMS 要按类别分开做,否则不同类别的重叠框会互相抑制。分割模型还要保留 mask 系数,后面和原型 mask 做矩阵乘。

4.3 分割分支:原型 mask 与系数矩阵乘

YOLOv8-seg 的输出除了[1, 116, 8400],还有一路[1, 32, 160, 160]的原型 mask。后处理是把每个检测框的 32 维系数和原型 mask 做加权求和,再裁剪到框内。

def postprocess_seg(output, proto, boxes, ratios, pads, orig_shape): # output: [1, 116, 8400], proto: [1, 32, 160, 160] pred = output[0].T mask_coeffs = pred[:, 84:116] # 后 32 维是 mask 系数 proto = proto[0] # [32, 160, 160] masks = [] for i, box in enumerate(boxes): # 系数与原型做矩阵乘,得到 160x160 的 mask m = mask_coeffs[i] @ proto.reshape(32, -1) m = m.reshape(160, 160) m = 1 / (1 + np.exp(-m)) # sigmoid # 裁剪到检测框内,再还原到原图尺寸 masks.append(crop_and_resize(m, box, ratios[i], pads[i], orig_shape)) return masks

mask_coeffs的维度是 32,和原型 mask 的通道数一致,这是 YOLOv8-seg 的设计。矩阵乘后要过 sigmoid,再按检测框裁剪。裁剪时注意 letterbox 的 ratio 和 pad,还原回原图坐标时要把 pad 减掉再除以 ratio,否则 mask 会偏移。

5. C++ 推理管线:编译、内存绑定与跨平台差异

5.1 CMake 工程结构与依赖链接

C++ 侧的核心依赖是 TensorRT、CUDA、OpenCV。Linux 用 CMake 找库,Windows 用 MSVC 加环境变量。

cmake_minimum_required(VERSION 3.16) project(yolo_trt_deploy CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # TensorRT 路径,Windows 下改成实际安装目录 set(TENSORRT_ROOT "/usr/local/TensorRT-8.6.1.6") find_package(OpenCV REQUIRED) find_package(CUDA REQUIRED) include_directories( ${TENSORRT_ROOT}/include ${CUDA_INCLUDE_DIRS} ${OpenCV_INCLUDE_DIRS} ) link_directories(${TENSORRT_ROOT}/lib) add_executable(yolo_trt main.cpp yolo.cpp preprocess.cpp postprocess.cpp) target_link_libraries(yolo_trt nvinfer nvinfer_plugin cudart ${OpenCV_LIBS} )

Windows 下nvinfer对应nvinfer.lib,路径在C:\Program Files\TensorRT\lib。如果链接时报LNK2019,多半是 TensorRT 版本和 CUDA 版本不匹配,或者 Debug/Release 混用。Linux 下如果运行时找不到 so,用ldd查依赖,缺哪个补哪个,或者设LD_LIBRARY_PATH。

5.2 内存绑定与 enqueueV3 调用

C++ 的内存绑定比 Python 啰嗦,但逻辑一致:分配 device 内存、拷贝输入、执行、拷回输出。

// 创建 context 和绑定 auto context = std::unique_ptr<nvinfer1::IExecutionContext>(engine->createExecutionContext()); void* buffers[2]; cudaMalloc(&buffers[0], inputSize * sizeof(float)); cudaMalloc(&buffers[1], outputSize * sizeof(float)); // TensorRT 10.x 用 setTensorAddress context->setTensorAddress("images", buffers[0]); context->setTensorAddress("output0", buffers[1]); // 异步执行 cudaStream_t stream; cudaStreamCreate(&stream); cudaMemcpyAsync(buffers[0], inputData, inputSize * sizeof(float), cudaMemcpyHostToDevice, stream); context->enqueueV3(stream); cudaMemcpyAsync(outputData, buffers[1], outputSize * sizeof(float), cudaMemcpyDeviceToHost, stream); cudaStreamSynchronize(stream);

enqueueV3是 TensorRT 10.x 的接口,8.x 用enqueueV2并传bindings数组。setTensorAddress的字符串名字要和 ONNX 里的输入输出名一致,用engine->getIOTensorName(i)可以查。如果输出 shape 是动态的,要先setInputShape再getTensorShape拿实际输出大小。

5.3 Linux 与 Windows 的编译差异清单

差异点LinuxWindows处理方式
库文件.so.dll / .libCMake 用条件判断
路径分隔/\用 std::filesystem
编译器GCCMSVC避免 GCC 扩展语法
运行时依赖LD_LIBRARY_PATHPATH部署时打包或设环境变量
内存对齐默认 16 字节默认 8 字节显式 alignas(16)

跨平台代码里最容易翻车的是std::filesystem的路径拼接和 OpenCV 的imread中文路径。Windows 下imread遇到中文路径会返回空 Mat,解决办法是用imdecode配合ifstream读二进制。另外 MSVC 对alignas的处理和 GCC 不同,TensorRT 的输入内存建议显式对齐到 16 字节。

6. 避坑与排查:那些让 engine 跑不起来的细节

6.1 现象:build engine 时报 unsupported operator

原因通常是 ONNX 里有 TensorRT 不支持的算子,或者 opset 版本过高。分割模型的Resize在 opset 13 以上会变成Resize加Scales输入,TensorRT 8.6 对动态 scales 支持不好。

解决:导出时把 opset 降到 12,或者用onnxsim把动态 scales 固化成常量。如果还不行,用polygraphy的surgeon工具把不支持的子图切出来单独处理。

6.2 现象:推理结果全是一堆重叠框

原因是 NMS 没生效,或者输出张量的维度理解错了。检测模型输出[1, 84, 8400],84 是 4 个 box 加 80 个类别,如果误以为是[1, 8400, 84]就会把 box 和类别搞混。

解决:先打印输出 shape,确认维度顺序。转置后再拆 box 和 scores。NMS 要按类别分开做,cv::dnn::NMSBoxes可以指定类别。

6.3 现象:分割 mask 整体偏移或缩放不对

原因是 letterbox 的 ratio 和 pad 在还原时算错了。letterbox 是等比缩放后居中填充,还原时要先减 pad 再除以 ratio。

解决:把 preprocess 返回的 ratio 和 pad 存下来,后处理时严格按(x - pad_x) / ratio还原。如果 mask 是 160x160 的,要先 resize 到 640x640 再还原,顺序不能反。

6.4 现象:Windows 下编译通过但运行时报 0xc0000005

这是 access violation,常见于 TensorRT 的 context 在析构时访问了已释放的 engine,或者 device 内存没同步就释放。

解决:确保 context 的生命周期短于 engine,用cudaStreamSynchronize后再释放内存。MSVC 下还要注意/MD和/MT运行库选项要和 TensorRT 的 lib 一致,混用会导致堆损坏。

6.5 现象:INT8 量化后检测框抖动严重

原因是校准集不具代表性,或者某些层的量化 scale 被截断。检测头的分类分支对量化最敏感。

解决:校准集覆盖所有类别和光照条件,数量加到 1000 张。如果还抖,把检测头的最后几层设为 FP16,用setPrecision逐层控制。分割模型建议直接放弃 INT8。

7. 进阶技巧:用 polygraphy 对比精度、用 batch 推理榨干吞吐

engine build 出来只是开始,真正上线前要做两件事:精度对齐和吞吐优化。

精度对齐用polygraphy,它能同时跑 ONNX Runtime 和 TensorRT,逐层对比输出差异。

polygraphy run yolov8s-seg.onnx \ --trt \ --fp16 \ --onnxrt \ --input-shapes images:1x3x640x640 \ --validate \ --atol 1e-2 --rtol 1e-2

--atol和--rtol是绝对和相对容差,FP16 下 1e-2 是合理范围。如果某层超差,polygraphy 会标出来,通常是该层被量化或融合后精度损失。这个工具在排查「为什么 TensorRT 结果和 PyTorch 对不上」时特别省事,比手动逐层 dump 快得多。

吞吐优化上,固定 batch 的 engine 只能跑固定 batch,要提吞吐就得 build 多 batch 的 engine,或者用动态 shape。我的习惯是 build 一个batch=1的低延迟 engine 和一个batch=8的高吞吐 engine,按请求量切换。多 batch 时 NMS 要按 batch 维度循环,mask 后处理也要逐样本做,别想着向量化,收益不大还容易出错。

还有一个容易被忽略的点:engine 的 build 时间。分割模型在 4090 上 build 一次要两三分钟,如果每次启动都 build 就太慢了。正确做法是 build 一次存成.engine文件,运行时直接反序列化。但要注意 engine 和 TensorRT 版本、CUDA 版本、显卡型号绑定,换机器就要重新 build。我一般会在部署脚本里加一个版本校验,engine 文件旁边放一个meta.json记录 build 时的版本信息,加载前先比对,不匹配就报错提示重新 build,省得运行时出玄学问题。

最后说个血泪教训:别在目标机器上装完整的 TensorRT 开发包,只需要 runtime 和对应的 so/dll。开发包体积大,还容易和系统里的 CUDA 版本冲突。部署时把libnvinfer.so、libnvinfer_plugin.so、libcudart.so这几个拷过去就行,Windows 下对应nvinfer.dll、nvinfer_plugin.dll、cudart64_*.dll。这个习惯帮我省过好几次现场排查的时间。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询