简介面向需要在Windows环境下快速部署YOLOv5目标检测能力的开发者这份资源提供了将YOLOv5模型与NVIDIA TensorRT推理优化引擎相结合的DLL动态链接库版本适用于嵌入式、边缘计算及实时视频分析等对响应速度要求较高的场景也适合作为深度学习模型工程化封装的参考范例。压缩包共8个文件约18KB以.h头文件、C示例程序、CMake构建脚本、说明文档及开源许可文件为主并划分出include与src模块目录结构精简便于直接集成到现有工程。已有79人学习下载。借助该DLL开发者无需重复编译或重新训练模型即可将优化后的目标检测功能嵌入自有软件获得更快的推理速度与更低的资源占用同时说明文档与示例代码可帮助理解TensorRT的模型转换与调用流程为YOLOv5实时检测项目提供了一条快捷的工程化落地路径。1. 约洛夫张量 DLL 到底是什么先别急着调接口我第一次看到 “约洛夫张量dll_yolov5 tensorrt 的dll版本.zip” 这个压缩包名时也愣了一下。这个名字其实就一句话把 YOLOv5 模型用 TensorRT 加速后封装成 Windows 下的 DLL给外部程序调用。别人拿到的不是一个 Python 推理脚本而是一个可以直接LoadLibrary的推理库通过 Init、Detect、Release 三个函数完成一整条目标检测流程。它能解决 Windows 上位机集成里的老大难问题C# 写的 GUI、Qt 做的客户端、LabVIEW 跑的上位机都不能直接调用 Python。常规做法是启动一个 Python 进程走 socket 或文件传图慢还容易崩。DLL 方式是把推理进程和调用进程合并到一起适合工业检测、安防摄像头、边缘盒子这类要交付给客户、运行环境不可控的场景。但难点从来不在 DLL 本身而在 TensorRT 版本、CUDA 运行时、引擎文件三者是否一致。版本错一个DLL 就可能报加载失败或者窗口闪退。先把这个前提搞清楚再去考虑接口怎么调。2. 环境选型与发布包结构TensorRT 版本是第一道闸2.1 TensorRT 10.x 与 GTX 1070为什么版本错一个就白下网上关于 “tensorrt 版本如果是 10.x 是否支持 gtx1070” 的帖子很多结论很明确TensorRT 10.x 已经不再支持 GTX 1070 所代表的 Pascal 架构。GTX 1070 的计算能力是 6.1而 TensorRT 10 在官方支持矩阵里只保留了 Turing 及之后的架构。也就是说你从某个压缩包里得到的 DLL如果它链接的是 TensorRT 10 的 nvinfer.dll那么在 GTX 1070 上无论怎么修复依赖都很难正常初始化。反过来TensorRT 8.x 对 Pascal 的支持是完整的。GTX 1070、GTX 1060、P100 这些卡至今仍在大量工业现场服役所以很多 DLL 发布包会特意指定 TensorRT 8.5 或 8.6。判断方法不复杂拿到压缩包后先找里面的 nvinfer.dll右键属性看产品版本。如果主版本号是 8大概率能兼容 1070如果是 10在 1070 上直接放弃别浪费时间修改配置。这个版本匹配不只在 DLL 层生效还牵扯到引擎文件。TensorRT 的 engine 文件与生成它的 TensorRT 版本强绑定8.x 生成的 .engine 文件基本无法用 10.x 的运行时去加载。所以版本一致性至少要同时满足三点DLL 使用的 TensorRT 运行时版本、引擎文件的生成版本、显卡架构是否在支持列表内。三者错一环整个包就是废的。对 1070 用户的实际建议是选择基于 TensorRT 8.6 编译的 DLLCUDA 配套使用 11.8这是目前老卡部署 YOLOv5 最稳的组合。如果你手里的包明确说是 TensorRT 10.x那就别在 1070 上折腾了换一台 RTX 20 或 30 系列的机器再做验证。把版本需求写在 README 里是这类 DLL 发布包最值钱的说明。2.2 典型发布包内有哪些文件DLL、引擎和运行依赖一个合格的 YOLOv5 TensorRT DLL 发布包通常不是只给一个 .dll 文件。常见的目录结构大致是yolov5_tensorrt_release/ ├── lib/ │ ├── yolov5_tensorrt.dll │ ├── yolov5_tensorrt.lib │ ├── yolov5_tensorrt.h │ └── nvinfer.dll ├── engines/ │ └── yolov5s.engine ├── demo/ │ ├── demo_csharp.exe │ └── demo_python.py └── README.txt运行时依赖往往比主 DLL 更关键。yolov5_tensorrt.dll 本身只是个外壳真正的计算在 nvinfer.dll、nvonnxparser.dll、cudart64_xx.dll、cublas64_xx.dll 这些底层库里。如果发布包只给了主 DLL 和引擎文件没给配套的 TensorRT 运行库调用方大概率会遇到 “找不到 nvinfer.dll” 或 WinError 1114。文件清单的作用就是让使用者知道哪些文件必须放在 exe 同目录哪些只是开发期才需要。用下表来对照会清楚一些文件类型部署时必须开发时需要典型文件主推理 DLL是是yolov5_tensorrt.dllTensorRT 运行库是是nvinfer.dll, nvinfer_plugin.dllCUDA 运行库是是cudart64_110.dllcuBLAS/cuDNN是是cublas64_11.dll, cudnn64_8.dll引擎文件是是yolov5s.engine导入库与头文件否是yolov5_tensorrt.lib, .h“dll 修复工具”这类软件在部署阶段经常被拿出来救急但它们只能修复缺失的 VC 运行库对 CUDA/TensorRT 这种带版次逻辑的库基本无效。正确做法是把上述依赖和主 DLL 放在同一个目录而不是扔进 System32。放 System32 能暂时跑通但未来另一个程序装了一个不同版本的 cudart整个系统就会被污染那是更惨的连环坑。2.3 自己编译 DLL 的步骤TensorRT 安装与 CMake 配置如果拿到的 DLL 版本不匹配或者不信任来路不明的二进制最靠谱的路是自己编译。TensorRT 安装教程看起来很多但 Windows 上的核心步骤只有三步解压腾讯网盘、配置环境变量、在 CMake 里指对路径。我用下来最可靠的组合是 CUDA 11.8 TensorRT 8.6 Visual Studio 2019/2022。下载 TensorRT 的 Windows zip 包后解压到固定目录比如D:/TensorRT-8.6.1.6。然后把它的 lib 目录加入 PATH或者在编译命令里直接传入路径。下面这套 CMake 命令是 Windows 下编译 DLL 的常见写法cmake -S . -B build -G Visual Studio 16 2019 -A x64 ^ -DTENSORRT_DIRD:/TensorRT-8.6.1.6 ^ -DCUDA_TOOLKIT_ROOT_DIRC:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v11.8 ^ -DCMAKE_BUILD_TYPERelease cmake --build build --config Release第一行指定源码目录和构建目录并用 VS2019 生成 x64 工程第二行是让 CMake 能找到 TensorRT 的头文件和导入库第三行是 CUDA 工具链的位置。编译完成后把 TensorRT 的 DLL 一并拷贝到输出目录是必做动作不然 Release 目录里的 yolov5_tensorrt.dll 在别的机器上根本起不来。这个拷贝动作看起来简单但在团队协作时经常漏掉导致本地能跑、部署机翻车。还要注意一点不要直接拿 conda 环境里的 tensorrt Python 包来给 C 工程用。pip 装的 TensorRT 里虽然有 DLL但它是按 Python 扩展模块路径组织的C 程序直接链接会出各种奇怪错误。我一般会单独维护一份 TensorRT 的 C 运行库和 CUDA 一起放在固定目录DLL 工程只认这份路径不依赖系统 PATH 里的其他副本。3. 用 C 封装 YOLOv5 推理逻辑核心代码与导出接口3.1 前处理与后处理YOLOv5 网络结构在 TensorRT 里的张量长什么样封装 DLL 前先把 YOLOv5 的推理路径理清。YOLOv5 网络结构图里输入是 NCHW 的 float 张量常见尺寸是 1×3×640×640输出在 TensorRT 里最常见的形式是一个 1×25200×85 的矩阵。25200 是三个检测头输出的预测框总数85 对应 4 个坐标、1 个目标置信度、80 个类别分数。如果是自定义类别数最后一维要改成 5类别数。DLL 内部要做的事包括把客户传入的 BGR 图像做 letterbox 缩放变成 640×640归一化到 0~1 之间的 float交给 TensorRT 推理最后把输出矩阵解码成坐标和类别。下面这段是后处理里最核心的解码逻辑它把 TensorRT 的输出变成 NMS 之前的候选框// output 是 TensorRT 输出张量的首地址 // 当前示例假设输出形状是 [1, 25200, 85] for (int i 0; i 25200; i) { const float* p output i * 85; float obj_conf p[4]; if (obj_conf 0.25f) continue; // 目标置信度阈值 int cls_id 0; float max_cls 0.0f; for (int j 5; j 85; j) { if (p[j] max_cls) { max_cls p[j]; cls_id j - 5; } } float score obj_conf * max_cls; if (score 0.35f) continue; // 最终得分阈值 float x p[0]; // 中心点坐标已在输入尺寸下解码 float y p[1]; float w p[2]; float h p[3]; boxes.emplace_back(x - w / 2, y - h / 2, w, h, cls_id, score); }这段代码有两个参数需要提一下目标置信度 0.25 和最终得分 0.35 不是固定值如果你训练时把置信度阈值调得过低或过高DLL 里的 25500 个候选框会大幅增减。实际开发时我一般会把这几个阈值暴露成 Init 接口的参数而不是硬编码在 DLL 里。coord 的 w/h 这里已经是完整宽高不再乘以 stride因为 TensorRT 加载 ONNX 时大多数算子已经完成了坐标解码只有在导出方式不同或使用了特殊 plugin 时才会出现需要按 stride 缩放的输出。3.2 导出 C 风格接口句柄、初始化与推理分离C 类不能直接被 C# 的DllImport加载所以 DLL 必须暴露一组 C 风格接口。我通常导出四个函数Init负责加载引擎和创建上下文Detect负责接收图像数据并返回检测框Release负责释放所有资源。用void*作为句柄避免把 C 类头文件泄露给调用方。下面这段是导出函数的典型写法#pragma pack(push, 8) typedef struct Detection { float x; float y; float w; float h; int class_id; float score; } Detection; #pragma pack(pop) extern C __declspec(dllexport) void* YOLOV5_Init(const char* engine_path); extern C __declspec(dllexport) int YOLOV5_Detect( void* handle, unsigned char* bgr_data, int width, int height, Detection* out, int max_out); extern C __declspec(dllexport) void YOLOV5_Release(void* handle);这里有几个设计要点。第一把句柄设计成void*而不是直接暴露std::shared_ptr调用方不需要知道内部实现也避免了不同编译器下 C ABI 不一致的问题。第二Detect输入的是内存中的 BGR 图像首地址而不是图片文件路径这样视频流的每一帧可以直接传给 DLL省掉磁盘读写。第三Detection结构体用#pragma pack(push, 8)对齐是为了让 C# 端的结构体布局能对上。如果不做这个处理64 位程序里可能出现字段错位。最重要的一点是YOLOV5_Init必须只加载一次并在整个进程生命周期内复用。不要在每一帧调用时都去加载 engine 文件那会让 TensorRT 的上下文构建耗时直接拖垮推理速度。同一个 DLL 实例支持多路视频时我一般会用多个上下文而不是多个进程。3.3 从 Python ctypes 和 C# 调用 DLL最小可运行示例发布 DLL 版本的好处之一是调用方不一定要写 C。Python 里通过 ctypes 可以很优雅地调用下面是经过大量项目验证的最小调用方式import ctypes import numpy as np dll ctypes.CDLL(rD:\release\yolov5_tensorrt.dll) dll.YOLOV5_Init.argtypes [ctypes.c_char_p] dll.YOLOV5_Init.restype ctypes.c_void_p dll.YOLOV5_Detect.argtypes [ ctypes.c_void_p, ctypes.POINTER(ctypes.c_ubyte), ctypes.c_int, ctypes.c_int, ctypes.POINTER(Detection), ctypes.c_int, ] dll.YOLOV5_Detect.restype ctypes.c_int handle dll.YOLOV5_Init(byolov5s.engine) img cv2.imread(demo.jpg) # BGR 顺序 img_data np.ascontiguousarray(img, dtypenp.uint8) buf img_data.ctypes.data_as(ctypes.POINTER(ctypes.c_ubyte)) buf_size img_data.shape[0] * img_data.shape[1] * 3 out_arr (Detection * 100)() count dll.YOLOV5_Detect(handle, buf, img.shape[1], img.shape[0], out_arr, 100)这里的argtypes和restype必须和 DLL 导出函数的签名完全一致。很多人在 Windows 上遇到OSError: [WinError 1114]之后第一反应是去修 DLL 系统文件其实大概率是 ctypes 里参数类型定义错了导致 DLL 内部在读取指针时访问违规。np.ascontiguousarray这一步也不能省它保证图像数据在内存里是一段连续排列否则传给 DLL 的指针可能跳过一行推理结果全是乱的。C# 端更简单一些只要用DllImport声明好函数和结构体剩下的事情交给 P/Invoke 层处理。调用YOLOV5_Detect时图像数据直接用byte[]传递C# 端不需要关心 CUDA 和 TensorRT。很多人第一次做这种 DLL 集成会忽略一点C# 进程默认是 x86 还是 x64取决于编译目标如果 DLL 是 x64 的C# 工程必须把平台目标设为 x64否则报的错根本不是代码能查出来的。4. DLL 部署避坑加载失败与推理异常的排查4.1 “OSError: [WinError 1114] DLL 初始化例程失败” 的真面目网上对这个报错的讨论非常多随手一搜就有大量 “dll 修复工具”“dll 修复工具免费版” 的推荐。但见多了就会发现WinError 1114 不是单纯的 “文件缺失”而是 DLL 的入口函数DllMain初始化失败了。在 YOLOv5 TensorRT 场景里最常见原因有三个依赖的 nvinfer.dll 或 cudart 版本没跟主 DLL 配对VC 2019 运行库没装DLL 内建了全局 CUDA context 初始化且显卡不可用。排查路径我一般是这样走的。先用Dependencies.exe或 Visual Studio 自带的dumpbin /dependents yolov5_tensorrt.dll看它依赖了哪些库并把它们全部列出来。然后把 TensorRT 和 CUDA 的 DLL 放到主 DLL 同目录而不是放进 System32。System32 里的旧版 cudart 会引发优先级混乱这是典型 “在其他机器上明明能用” 但在目标机器上报 1114 的元凶。第三方 “DLL 修复工具” 只能修复系统级别的 VC 运行库对 TensorRT 运行库无能为力。遇到 1114 时我建议先做减法把主 DLL 复制到一个空目录把依赖 DLL 一个个放进去每次只加一个直到能加载为止。这个过程虽然土但比任何自动工具都可靠。4.2 TensorRT 10.x 在 GTX 1070 上翻车不是 DLL 坏了是架构被放弃另一个高频问题是拿着 TensorRT 10.x 编译的 DLL在 GTX 1070 上加载时没有任何报错信息但一进入推理就卡死或者直接报 CUDA driver version 错误。这类问题的根子在于引擎文件里的算子集是按照新架构生成的Pascal 上的设备代码段根本不存在。解决办法只有一个换回 TensorRT 8.x。这意味着你不仅要换主 DLL还要重新生成引擎文件因为 TensorRT 10 生成的 .engine 文件结构与 8.x 不兼容。如果发布包里有README.txt但没写 TensorRT 版本号可以用strings nvinfer.dll | grep 8.的方式在 Linux 上查Windows 下直接看 nvinfer.dll 的文件版本即可。这套版本匹配在 GTX 1070 上极度敏感因为很多老机器还装着上古时期的 CUDA 10.2。如果 DLL 是 CUDA 11.8 编译的那它在 CUDA 10.2 驱动下也会起不来。我的习惯是发布包内额外附一份version_check.txt里面写明 CUDA 最小版本、TensorRT 版本、显卡架构并在 DLL 内部做一次运行时校验版本对不上就返回一个明确的错误码而不是让调用方看到 1114 无处下手。4.3 DLL 冲突同一个 cudart 从不同目录被加载两次DLL 冲突比缺文件更隐蔽现象是调用YOLOV5_Init时返回空句柄但 DLL 加载本身没有异常。原因在于同一个进程中OpenCV 可能已经静态链接了旧版 cudart而 YOLOv5 TensorRT DLL 又加载了新版 cudart两套 CUDA runtime 在设备上下文上相互踩踏。这在同时做图像显示和 GPU 推理的上位机里非常常见。解决方案有三种第一种是把 YOLOv5 DLL 做成 Delay-Load让 nvinfer 和 cudart 在实际调用时才加载第二种是在 Init 函数内部显式调用SetDllDirectory把 TensorRT 依赖锁定到 DLL 同目录第三种是在编译时尽量使用静态的 CUDA runtime减少外部依赖面。第三种代价是 DLL 体积变大但能显著降低现场冲突。如果你同时跑多个不同版本的 DLL 程序建议把公共依赖复制到各自目录不要去系统目录里共享版本。因为同一个 nvinfer.dll 可能有多个版本并存一旦系统 PATH 里出现两个目录的路径Windows 的加载顺序会按 PATH 优先级走这根本不是 “文件存在与否” 的问题而是加载顺序的玄学。4.4 推理不报错但结果全 0输入尺寸和 batch 没对上DLL 加载成功、Init 也正常但 Detect 返回的框全是空数组这种情况大多不是模型坏了而是调用方传入的图像尺寸或 batch 数和引擎期望的不一致。TensorRT 引擎分为固定 shape 和动态 shape 两种。固定 shape 的 engine 在构建时已经写死了输入是 1×3×640×640如果你传入一张 1920×1080 的图DLL 内部 letterbox 后如果没有正确 resize推理结果就会是一批空框。更隐蔽的在 batch 上。有的 DLL 发布包把 engine 的 batch 固定为 4调用方只填了一张图后端拿到的是一个未初始化的 batch 剩余部分推理结果自然不稳定。我自己做封装时一定会把输入尺寸和 batch 数作为Init参数暴露出来并让 DLL 内部在 letterbox 时用引擎请求的实际尺寸做缩放而不是默认 640。如果你用的是动态 shape 引擎还要额外处理一件事每次推理前调用setBindingDimensions设置输入维度否则 TensorRT 会沿用上一次的维度。这个点在 Windows DLL 封装里经常被漏掉尤其是当同一个 DLL 被 C# 多线程并发调用时两个线程互相修改 binding dimensions结果是现场偶发崩溃本地复现又很难。我建议 DLL 内部对 Detect 函数加锁串行化 TensorRT 推理损失一点吞吐量换稳定交付。5. 从训练自己的数据集到换引擎一条完整链路5.1 YOLOv5 训练自己的数据集目录结构与超参数设置DLL 版本的最终目的不是跑官方 COCO 模型而是跑你自己训练的数据集。 YOLOv5 训练自定义数据集有一个约定目录训练前要确保图片和标签配对完整否则后处理阶段的类别数会对不上。datasets/mydata/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── mydata.yaml训练命令我一般这样写python train.py --data mydata.yaml --weights yolov5s.pt --img 640 --batch 16 --epochs 100 --hyp hyp.scratch-low.yaml --device 0这段命令里--hyp是 yolov5 超参数文件hyp.scratch-low.yaml适合稳定小模型hyp.scratch-high.yaml适合追求精度的场景。训练时设置的--img会和推理 DLL 里的输入尺寸强绑定如果你用 640 训练但 DLL 里写死 320模型精度会明显下降。类别数由mydata.yaml里的nc决定这个数字必须传给 DLL 后端否则后处理时类别数组会越界。训练完成后要重点检查runs/train/exp/weights/best.pt。很多人忽略的是YOLOv5 在训练时会自动重算 anchor这些 anchor 已经写进了权重文件。如果后续导出 ONNX 时没有带上训练时的 anchor 信息TensorRT 的输出结构会和 your 的源码模型不一致。我习惯在训练时固定--anchor参数不开启自适应重算这样后面每一步的坐标解码逻辑都是确定的。5.2 从 PyTorch 权重导出 ONNX再用 trtexec 生成 engine拿到best.pt后下一步是转成 TensorRT engine。先用 YOLOv5 自带的export.py导出 ONNXpython export.py --weights runs/train/exp/weights/best.pt --include onnx --opset 12 --batch-size 1--batch-size 1在这里很关键。DLL 部署通常固定 batch 为 1这样引擎占用的显存更小动态 shape 的复杂度也降下来了。如果你的业务必须支持多路并发建议在 DLL 内部开多个上下文而不是把一个 engine 的 batch 调大。导出 ONNX 后用 TensorRT 自带的trtexec生成 enginetrtexec --onnxbest.onnx --saveEnginebest.engine --fp16 --workspace4096这里的--fp16在 GTX 1070 上没有 Tensor Core 加速但仍然可以启用因为 Pascal 支持 FP16 指令只是加速幅度不如 RTX 系列。有些人为了追求极致精度直接不开 fp16但我建议保留因为它能把显存占用砍掉一半对 8GB 显存的老卡很重要。生成 engine 后可以用trtexec --loadEnginebest.engine做一次基准测试确认平均延迟在合理范围内。如果 ONNX 里有自定义节点导出后要用onnxsim或polygraphy检查一遍算子兼容性。YOLOv5 官方导出的 ONNX 大多是标准算子集TensorRT 可以直接解析。一旦加了自定义 NMS 或特殊的 anchor 处理就需要引入 TensorRT pluginDLL 发布包就必须连带提供nvinfer_plugin.dll部署复杂度会立刻上升一个量级。5.3 新引擎接回 DLL要同步改的三个地方引擎文件叫best.engine但 DLL 并不知道它是什么模型它只知道按固定形状去推理。所以每换一个模型DLL 里有三处需要同步输入尺寸、类别数、后处理阈值。输入尺寸由engine决定DLL 在Init时读取引擎的 binding dims自动设置 letterbox 的目标尺寸类别数则需要显式告诉 DLL我一般是在Init里增加一个int num_classes参数后处理阈值每个应用不一样工业视觉里误检成本高阈值会调高到 0.5 以上。接入方式很简单还是调同一个接口void* handle YOLOV5_Init(best.engine, 2, 0.45f, 0.30f);这里第二个参数 2 是类别数第三个和第四个是最终得分阈值和 NMS IoU 阈值。相比把阈值写死在 DLL 里这样设计的好处是现场调参不用重新编译。如果你的类别数变了但 DLL 里还按 85 维去解读输出那解码结果会完全乱套。严格来说类别数本质上已经编码在 ONNX 输出形状里DLL 可以从 engine 的绑定维度推断出来但为了防御端到端的封装错误显式传入仍是首选。6. 给 DLL 加一个诊断模式一个能救场的验证手段真正到了客户现场最浪费时间的事情是环境问题而不是模型问题。我后来养成一个习惯在 DLL 里额外导出一个YOLOV5_GetInfo函数让使用方在调Init之前先看环境是否正常。这个函数不加载引擎只读取当前进程里加载的 TensorRT、CUDA 版本和显卡名称并返回一个直观的错误码。extern C __declspec(dllexport) int YOLOV5_GetInfo(char* buffer, int size);调用方执行一次YOLOV5_GetInfo如果返回 0说明 DLL 依赖的 TensorRT 运行库版本和 CUDA driver 都正常如果返回 1说明 nvinfer.dll 没找到返回 2说明 CUDA driver 版本太老。这个诊断模式把原本黑盒的 DLL 部署变成了白盒现场人员照着错误码找问题比看 WinError 1114 高效太多。即便发布包没有这个接口我拿到任何第三方 DLL 版本包时第一件事也会先检查依赖库再手动跑一遍GetLastError最后才谈推理业务。这套顺序已经成为我的固定动作版本错一个就重来环境乱了一路排查到底。希望帮到你。本文还有配套的精品资源点击获取
