☰
Yolov5工业视觉落地:TensorRT加速与C++封装DLL全流程
2026/10/18 11:07:44 网站建设 项目流程

做工业视觉项目这些年,我越来越觉得,深度学习模型训练只是整个系统里最“干净”的一环。真正让人头疼的,是把模型塞进产线工控机、接上工业相机、再封装成能让上位机调用的动态链接库这一路。前阵子做了一个Yolov5目标检测的工业视觉方案,从相机取流、TensorRT推理到C++封装DLL整条链路都跑通了,中间踩了不少坑。这篇就把这套方案从选型到落地的完整过程拆开讲,重点讲清楚图片目标检测在工业场景下怎么落地,以及C++封装DLL时那些文档里不会写的细节。

这套方案能干什么,说人话就是:用工业相机拍产品,Yolov5模型实时检测目标并输出坐标,检测能力封装成一个standard的DLL,给MFC、C#或者LabVIEW写的上位机直接调用。适合正在做视觉检测项目、准备把Python训练的模型搬进C++工程,又没走通过全流程的人参考。我会尽量把每一步“为什么这么做”也讲明白,而不是只甩给你一堆代码。

1. 整体设计与选型思路

1.1 为什么选Yolov5而不是其他检测模型

很多人上来就问:现在都V8、V9了,为什么还在用Yolov5?这个问题我在实际项目里被问过不下十次。其实答案很朴素:工业项目要的不是“最新”,而是“最稳”。

Yolov5在目标检测这个领域,生态成熟度是其他版本没法比的。意思就是,你遇到的每一个坑,大概率都有人踩过并留下了解决方案。从Pytorch导出ONNX、ONNX转TensorRT引擎,到量化、裁剪、迁移学习,每个环节都有大量现成资料可以查。V8在这些方面逐渐赶上来,但如果你去搜一句“yolov5 export onnx dynamic batch报错”和“yolov8 export onnx报错”,得到的有效信息量差一个量级。

另外一个更现实的考量是部署后的排查。产线上的工控机显卡往往很老,驱动锁得死死的不能乱升级。Yolov5的C++部署范例多,TensorRT版本兼容性也相对宽松,出问题好定位。在实际测试里,同样的硬件环境下Yolov5s的FPS表现和V8s差距不大,而Yolov5的代码量更精简,二次改造成本低——这就够了。

1.2 工业相机选型的核心考量

做工业视觉,相机选型是第一步也是决定成败的一步。我这次用的是某国产主流工业相机品牌的千兆网(GigE)接口面阵相机,为什么这么选,有几个原因:

  • 接口稳定性:GigE相机用网线通信,线材便宜、抗干扰能力尚可,适合产线上常见的3-5米传输距离。USB3.0相机虽然理论带宽高,但线材质量参差不齐,距离远了容易掉线,在工业场景不如GigE省心。
  • SDK成熟度:工业相机SDK一般提供相机枚举、参数配置、软硬触发、图像回调等接口,这套东西在工业现场很成熟。相比直接用USB摄像头或RTSP网络摄像头,SDK能帮你绕开很多底层协议问题。
  • 像素格式多样性:产线上常见的是黑白相机(Mono8)、拜耳彩色相机(BayerRG8),以及带像素合并(Binning)模式。这些都需要SDK层面的支持,普通摄像头往往做不到。

这里还想多说一句:不要一上来就买几千上万的高分辨率相机。工业检测一般先看被检物体最小缺陷尺寸,再看视野大小,最后反推分辨率要求。比如视野100mmx100mm,需要检测0.1mm的缺陷,分辨率至少1000x1000,选130万或200万像素的相机就够用。盲目堆像素只会让传输带宽吃紧,反而拖累帧率。

1.3 为什么非要封装成DLL

这个问题很多从算法转过来的同学不理解,觉得“我明明写好了Python推理脚本,直接跑不就行了?”行是行,但产线不是实验室。

产线上位机程序绝大多数是C#、C++或者LabVIEW写的。这些程序要调用你的模型,总不能装个Python环境、再装一堆依赖、最后还要求操作员能看懂报错日志吧?把Yolov5推理能力封装成一个DLL,本质上是做了一个算法能力与上位机解耦的中间层:

  • 上位机只需要InitDetector()、DetectImage()、ReleaseDetector()几个函数,不需要关心内部是TensorRT还是ONNX Runtime。
  • 模型升级只替换DLL和模型文件,上位机代码一行不用改。
  • 相机取流、图像处理、推理这些耗时操作全部放在DLL内部线程里,上位机UI不会卡住。

这是工业软件里很经典的“稳定接口 + 易变实现”模式,和平时写代码追求的“开闭原则”是一个道理。

2. 环境与工具链准备

2.1 推理引擎选型:为什么上TensorRT

模型训练出来只是开始,真正决定能不能产线落地的是推理速度。刚开始我用Pytorch直接推理,640x640的输入在RTX 3060上大概跑20ms一帧,算下来50FPS,看起来还行。但注意,这只是纯模型推理时间,还没算相机采集、图像转换、预处理、后处理、显示这些环节。一整套下来一帧要60-70ms,产线要是节拍200ms一个工件,勉强够;一旦要求双相机同时检测,就捉襟见肘了。

所以后来我把推理引擎换成了TensorRT,做了FP16精度优化,模型的推理时间降到了11ms左右。TensorRT的原理说白了就是:把训练好的网络结构提前“编译”成针对特定GPU的最优执行计划,包括算子融合、精度校准、显存复用等等。相当于你训练时写的是“通用脚本”,TensorRT给你编译成“针对这台机器的机器码”。

还有条件的话可以做INT8量化,速度还能再提一截,但需要准备一批校准图片来避免精度掉太多。我的经验是:FP16精度损失很小,收益明显,适合大多数工业场景;INT8需要仔细评估检出率变化,特别是有小目标的时候要慎重。

2.2 开发环境与依赖版本

这一节我把环境列清楚,新手照着装基本不会踩坑。

组件推荐版本备注
操作系统Windows 10/11 专业版工控机用Win10居多,别用家庭版
CUDA11.4 或 11.8要和显卡驱动匹配
cuDNN8.x和CUDA版本对应
TensorRT8.4 或 8.5下载对应CUDA版本
OpenCV4.5.5+建议用官方预编译包
Visual Studio2019 / 2022要装C++桌面开发组件
CMake3.20+构建DLL工程用
Pytorch1.10+训练/导出模型用

这里有个特别容易出问题的点:TensorRT和OpenCV的版本搭配。OpenCV从4.x开始某些版本会引入和CUDA/TensorRT冲突的依赖库(比如opencv_world里自带的第三方库),导致DLL运行时出现莫名其妙的崩溃。我的习惯是尽量用官方预编译的OpenCV,并且把OpenCV、CUDA、TensorRT的bin目录按顺序配置到系统PATH里,避免覆盖冲突。

2.3 模型导出:从pt到ONNX再到engine

Yolov5模型训练好之后是.pt格式,不能直接给TensorRT用。标准流程是:.pt->.onnx->.engine。

第一步,把Pytorch模型导出成ONNX。Yolov5官方仓库的export.py已经封装好了,命令大概是这样:

python export.py --weights yolov5s.pt --img 640 --batch 1 --include onnx --opset 12 --simplify

注意几个参数:--img 640要和后续推理输入尺寸一致;--opset 12以上是TensorRT兼容性比较好的;--simplify会调用onnx-simplifier优化计算图,建议加上。如果模型里有自己改的检测头或者自定义层,导出后最好先用onnxruntime跑一下看看输出是不是正常,别等部署了才发现网络结构导错了。

第二步,把ONNX转成TensorRT引擎。可以用trtexec命令行工具,也可以写Python脚本。我直接用的命令行:

trtexec --onnx=yolov5s.onnx --saveEngine=yolov5s_fp16.engine --fp16

加上--fp16就是FP16精度。转出来的.engine文件是和具体GPU绑定的,换一张显卡就得重新转,这个要提前和产线确认好工控机配置,别拿自己电脑的engine丢产线去跑。

3. 相机采集与图像解码实操

3.1 初始化相机与关键参数配置

相机SDK提供的API大同小异,流程基本是:枚举设备 -> 打开设备 -> 设置采集模式 -> 开始采集 -> 获取图像。

用伪代码写一下大概是这样:

// 枚举相机 std::vector<CameraInfo> cameras = CameraSDK::EnumCameras(); if (cameras.empty()) { /* 处理无相机 */ } // 打开设备 CameraHandle handle = CameraSDK::Open(cameras[0].ip); // 设置参数 CameraSDK::SetExposureTime(handle, 3000.0); // 曝光时间 3ms CameraSDK::SetGain(handle, 10.0); // 增益 10dB CameraSDK::SetTriggerMode(handle, TRIGGER_MODE_SOFTWARE); // 软触发 // 注册图像回调 CameraSDK::RegisterImageCallback(handle, onImageCallback, userData); // 开始采集 CameraSDK::StartGrabbing(handle);

这里最有讲究的是触发模式。工业场景一般有两种选择:

  • 硬触发:外部传感器(光电开关、接近开关)给一个电平信号,相机才拍一张。适合运动速度固定、有明确触发位置的产线。
  • 软触发:上位机软件调用一次触发指令,相机拍一张。适合节拍由程序控制、需要随时抓拍的场景。

我这次用的软触发,因为产线节拍是上位机控制的,逻辑简单不容易出错。如果你的工件在流水线上高速运动,还要考虑运动模糊的问题——曝光时间要足够短,或者配频闪光源。

3.2 图像格式转换:别让Bayer坑了你

工业相机输出的原始图像格式,和普通网页摄像头的JPEG完全不是一回事。常见的有:

像素格式说明
Mono8单通道灰度图,每个像素8bit,直接转为单通道Mat
BayerRG8拜耳彩色图,每个像素只有R/G/G/B中的一个分量,需要去马赛克
RGB24 / YUV422彩色格式,需要转换成OpenCV的BGR

这个转换步骤看着简单,但实际项目里最容易在这里翻车。我遇到过一个情况:相机输出BayerRG8,我直接用OpenCV的cv::cvtColor转成BGR,结果图像看起来整体泛绿带条纹。原因就是Bayer的排列顺序不匹配——BayerRG和BayerGB、BayerGR之间都有区别,相机SDK文档里标注的RGBA顺序要和OpenCV的转换代码一一对应才行。

更稳妥的做法是用相机SDK自带的转换接口。工业相机SDK一般提供ConvertToBGR或者PixelConvert这类函数,它们会考虑相机内部的实际排列,比OpenCV的通用转换可靠得多。但SDK转换函数有的不能在线程里并发调用,这个要看具体文档,必要时要加锁。

另外,工业相机还有一个坑:图像步长(stride)可能不等于宽度。有些相机的每一行数据会按4字节或8字节对齐,导致stride >= width。如果你直接用cv::Mat(height, width, CV_8UC3, buffer)来构造Mat,行尾有多余的填充字节,图像就会歪斜或出现竖条纹。正确做法是:

int width = cameraInfo.width; int height = cameraInfo.height; int stride = cameraInfo.stride; // 相机给的行字节数 cv::Mat raw(height, height * stride, CV_8UC1, buffer); // 先按完整stride构造 cv::Mat img = raw(cv::Rect(0, 0, width, height)).clone(); // 裁掉填充部分

3.3 采集线程与缓冲队列设计

图像采集不能和推理放同一个线程里做,否则相机SDK回调一阻塞,后面的图像就会排队积压,延迟越来越大。我常用的结构是生产者-消费者:

  • 相机SDK的回调线程是生产者,拿到一帧图像后,做格式转换,然后压入一个无锁队列(或用互斥锁保护的队列)。
  • 推理线程是消费者,从队列里取最新一帧,做预处理和模型推理。

这里有个细节很多人容易忽略:队列长度和丢帧策略。工业视觉很多时候不需要每一帧都处理,比如相机采集30FPS,推理只能跑15FPS,那中间帧注定要被丢掉。如果队列积压了很多旧帧,推理线程取出来处理的是“几秒前的画面”,那检测结果毫无实时性可言。

所以我的队列实现里故意做成了只保留最新一帧的机制:新帧来了直接覆盖旧帧。这样虽然会丢帧,但保证了永远处理的是最新画面,延迟可控。对于产线节拍检测来说,“看到当前时刻”远比“一帧不漏”重要。

4. Yolov5模型的C++推理集成

4.1 预处理:LetterBox的每一项细节

把相机采集到的图像喂给模型之前,要做Yolov5标准的预处理。这一步看着简单,但里面的坑我整整排查了两天。

标准流程是:

  1. 将原始图像按比例缩放,长边缩放到640,短边按同比例缩放,不足的部分用灰色(114,114,114)填充。
  2. 将BGR图像转换为RGB。
  3. 将图像数据从HWC排列转为CHW排列。
  4. 归一化到0-1之间(除以255)。
  5. 转换为FP32,送入TensorRT。

C++里实现LetterBox时要特别注意:填充和缩放的计算必须和训练时保持一致。Yolov5训练时用的letterbox函数,当原图宽高比和目标宽高比不完全相等时,它选择的缩放比例是min(ratio_w, ratio_h),然后按这个比例缩放宽高,最后计算需要填充的像素数。

cv::Mat letterbox(const cv::Mat& src, int targetSize = 640) { int w = src.cols, h = src.rows; float ratio = std::min(float(targetSize) / w, float(targetSize) / h); int newW = std::round(w * ratio); int newH = std::round(h * ratio); cv::Mat resized; cv::resize(src, resized, cv::Size(newW, newH), 0, 0, cv::INTER_LINEAR); int padW = targetSize - newW; int padH = targetSize - newH; int top = padH / 2; int left = padW / 2; cv::Mat out; cv::copyMakeBorder(resized, out, top, padH - top, left, padW - left, cv::BORDER_CONSTANT, cv::Scalar(114, 114, 114)); return out; }

这里top和left的数值在后续坐标映射时要用到,所以必须记录下来。我一般用结构体保存:

struct LetterBoxInfo { float ratio; int top; int left; int padW; int padH; };

4.2 推理与后处理:NMS在C++里怎么实现

TensorRT推理有标准流程:创建ExecutionContext -> 绑定输入输出buffer -> 拷贝输入 -> enqueue推理 -> 拷贝输出。这部分代码模板网上很多,我就不贴完整代码了,重点说几个容易出问题的地方。

输入buffer是float*类型,size是1x3x640x640。要把预处理好的图像数据memcpy进去,注意TensorRT要求的输入顺序是NCHW,和OpenCV的Mat默认HWC不同,别搞混了。

输出Tensor的解析才是重头戏。Yolov5的输出形状是[1, num_anchors * num_classes + 5, output_count],在C++里拿到的直接就是这个三维Tensor展开后的数据。以Yolov5s为例,输入640x640时,三个检测头输出的anchor总数是(80*80 + 40*40 + 20*20) * 3 = 25200,每个anchor有5 + num_classes个值,COCO数据集就是5 + 80 = 85。

所以输出buffer的长度就是25200 * 85。解析阶段要做的:

  1. 遍历所有anchor,取前5个值。前4个是[cx, cy, w, h](注意这是模型输出空间的坐标,不是原图坐标),第5个是目标置信度obj_conf。
  2. 计算每个类别的置信度class_conf = obj_conf * cls_prob。
  3. 如果class_conf大于阈值(工业场景我常用0.25-0.4),保留这个候选框。
  4. 对保留的候选框做NMS(非极大值抑制),去掉重叠过高的框。

NMS的实现有几种选择:手写循环、用OpenCV的cv::dnn::NMSBoxes、用TensorRT自带的后处理插件。我个人建议先把cv::dnn::NMSBoxes用起来,它简单可靠,不需要你写排序和IoU计算。但注意NMSBoxes处理的是像素坐标(整数或float),它需要的是box的坐标格式是(x, y, w, h),和Yolov5输出的中心点坐标要先转换一下才能传进去。

如果你要处理的目标特别多(比如一张图几百个目标),OpenCV的NMS可能成为瓶颈,那时候再手写一个高效的NMS实现或者上TensorRT的NMS插件,收益会比较大。

4.3 检测框坐标映射回原图

模型输出的是在640x640输入空间里的坐标,要映射回原始图像,需要做LetterBox的逆变换:

float x1 = (box.x1 - left) / ratio; float y1 = (box.y1 - top) / ratio; float x2 = (box.x2 - left) / ratio; float y2 = (box.y2 - top) / ratio;

注意:如果LetterBox的宽或高padding是奇数,left和top和right、bottom不相等,必须严格按照之前记录的left/top来减。这个细节不处理好,检测框会出现整体偏移的情况,特别是在图像边缘的目标。

另外,映射完记得做一次clip,把坐标限制在原始图像的宽高范围内,防止坐标越界导致后续绘制或ROI提取崩溃。

x1 = std::clamp(x1, 0.0f, (float)src.cols - 1); y1 = std::clamp(y1, 0.0f, (float)src.rows - 1); x2 = std::clamp(x2, 0.0f, (float)src.cols - 1); y2 = std::clamp(y2, 0.0f, (float)src.rows - 1);

检测框坐标一旦映射回原图,后面就可以直接画框、裁ROI、做OCR或者其他进一步处理,和PyTorch里的结果是完全对应的。

5. C++封装DLL的完整流程

5.1 接口设计:稳定ABI是底线

DLL封装这块,我认为是最考验工程经验的环节,因为算法写得再好,接口设计烂了照样崩。第一个原则:DLL导出的接口尽量用C语言风格接口,不要直接导出C++的class。原因有两层:一是C接口没有C++的名称修饰(name mangling)问题,可以被C、C++、C#、Python等任何语言调用;二是C接口能避免STL、异常等跨模块传递带来的ABI兼容问题。

我设计的接口大概长这样:

// 初始化检测器 // 返回句柄,后续所有接口都基于这个句柄操作 HANDLE Detector_Init(const char* modelPath, int gpuId); // 传入一张BGR图像,执行检测,结果通过回调函数返回 int Detector_DetectImage(HANDLE handle, const unsigned char* bgrData, int width, int height, DetectCallback callback, void* userContext); // 释放检测器 void Detector_Release(HANDLE handle);

检测结果用回调函数返回的好处是,不需要在DLL内部维护一个结果缓存区,也不存在“调完函数再翻数据”的同步问题。回调函数的定义:

typedef void (*DetectCallback)(int objectCount, const DetectResult* results, void* userContext);

其中DetectResult是一个纯C结构体:

typedef struct { float x1, y1, x2, y2; float confidence; int classId; } DetectResult;

C#那边可以直接用Marshal.StructToStructure解析这个结构体。如果你把std::vector<DetectResult>传到DLL边界,C#调用方就傻眼了。

5.2 内部封装:句柄模式管理类实例

C语言接口不能直接传递C++对象指针,所以我们用句柄模式:HANDLE其实是一个不透明的指针,指向内部C++类的实例。

extern "C" HANDLE Detector_Init(const char* modelPath, int gpuId) { auto* detector = new Detector(modelPath, gpuId); return (HANDLE)detector; }

然后DetectImage里再把句柄转回来:

extern "C" int Detector_DetectImage(HANDLE handle, ...) { auto* detector = (Detector*)handle; detector->DetectImage(...); }

这个模式的好处是:你可以在DLL内部任意修改类的成员和依赖,只要对外接口签名不变,调用方完全无感。模型初始化、TensorRT引擎加载、相机实例管理这些全部藏在类内部。

Detector类内部我大致分了几块:

  • Engine:封装TensorRT引擎加载、推理。
  • Preprocessor:图像预处理。
  • PostProcessor:输出解析和NMS。
  • ResultQueue:异步结果队列(如果要做异步检测)。
  • CameraManager:相机采集管理。

5.3 回调机制与状态上报

回调函数是DLL和上位机之间最直接的沟通方式,但也是最容易出问题的地方。有几个经验值得分享:

回调线程必须明确。假设你在DLL的推理线程里调用回调函数,而上位机在UI线程里更新界面,那就涉及跨线程UI更新的问题。C#的UI控件不能在非UI线程直接更新,否则会抛异常。所以上位机那边拿到回调后,要自己通过Invoke或者BeginInvoke切回UI线程。这个不是你DLL能控制的,但接口文档里必须写清楚:回调在哪个线程执行,避免两边因为线程问题互相甩锅。

不要在回调里做耗时操作。如果上位机的回调里做了保存图片、数据库写入这类操作,会阻塞DLL的推理线程,导致帧率下降。规范做法是上位机回调里只把结果塞进自己的队列,耗时操作放到自己的后台线程。

另外,除了检测结果,DLL还可以定期上报状态信息,比如FPS、丢帧率、GPU温度、最近一次错误码。这些用另一个回调函数或者统一的回调对象传过来,对产线运维特别有用。我这次是定义了一个StatusCallback,每1秒推送一次状态。

5.4 内存管理与线程安全

DLL最容易崩溃的第二个点是内存管理。C接口的DLL有一个铁律:谁分配,谁释放。在DLL里分配的buffer,不能在调用方那边直接free或delete,反之亦然。

我设计的接口里,输入图像的内存是调用方分配的,DLL只读不写,这个简单。检测结果数组呢?如果每次回调传一个临时数组的指针,调用方直接用——这个没问题,因为回调是同步的,调用方在回调函数里读取这些数据时,DLL侧还没释放。

但如果要做异步检测,检测结果需要从推理线程传给回调,那就要考虑生命周期。我的做法是在推理线程内先new一个DetectResult[]数组,填充完把指针传给回调,回调结束后由DLL统一释放。这样设计,调用方不需要关心释放,DLL也不会出现“调用方还在用,DLL提前释放”的悬垂指针问题。

线程安全方面,实际场景里要处理几个并发:

  • 多个上位机线程同时调用DetectImage,但TensorRT推理本身是串行的,所以需要一把互斥锁保证同一时刻只有一个推理。
  • 相机采集线程和推理线程共享的图像队列,需要加锁或者用无锁队列。
  • 如果在同一个DLL里管理多个相机和多个模型实例,注意它们之间是共享资源还是独占资源,别搞混。

6. 常见问题与排查技巧实录

这一节把我实际踩过的坑和排查过程写出来,希望能帮你省几天时间。

6.1 相机SDK和GPU抢显存导致崩溃

这个坑非常隐蔽。某次联调时,DLL加载模型后一切正常,但相机一开,程序直接崩溃或者报CUDA error: out of memory。排查了很久才发现原因:相机SDK的某个功能模块默认会自己分配一大块GPU显存用于图像处理加速,而TensorRT引擎加载时已经把显存用得差不多了,两边撞上了。

解决办法有两个方向:一是调整相机SDK的GPU显存使用设置,在初始化时通过配置参数禁用或减少显存占用;二是把TensorRT的推理显存池设置小一点,给相机SDK留出余量。工业相机的SDK大多数都有类似参数,关键是你要知道有这回事,不然你怎么查都查不到。

6.2 检测框整体偏移或错位

一次现场调试,模型推理结果完全正确,画出来的框却整体向右下角偏了几十个像素。第一反应是坐标映射公式有问题,但检查了好几遍代码逻辑都对。最后发现是输入图像的步长问题——生产环境的工控机分辨率设置和开发机不同,相机SDK返回的图像行字节数多了一段padding,cv::Mat直接构造时没有裁掉,图像整体被拉伸了,检测框自然对不上。解决方案就是我前面提到的,构造Mat时先带stride构造,再裁掉多余部分。这个坑在分辨率越大时越明显,值得重点排查。

6.3 C#调用DLL总是崩溃

很多搞算法的人不熟悉C#调用C++ DLL的坑。最典型的问题是:C#的string传给C接口时,默认用UTF-16编码,而C++里的const char*是UTF-8/ANSI编码,要不你DLL里显式转编码,要不C#那边用Marshal.PtrToStringAnsi处理。两者不匹配,轻则乱码,重则栈溢出崩溃。

另外,C#侧的委托(Delegate)传给C++函数指针时,要确保委托对象在C++调用期间不被GC回收。最简单的方法是保持一个全局静态变量引用委托,或者用GCHandle.Alloc(handle, GCHandleType.Pinned)固定住。否则你会发现程序跑着跑着突然崩溃,毫无规律。

6.4 帧率上不去,瓶颈到底在哪

帧率不达标是工业视觉项目最常被diss的点。排查的思路不要靠猜,要逐段量化。我给一个标准流程:

  1. 只测相机采集到内存的时间,一般GigE相机控制在几毫秒到十几毫秒,如果超过50ms先查网络和驱动。
  2. 只测预处理时间,LetterBox加格式转换,OpenCV下5ms以内算正常。
  3. 只测推理时间,TensorRT FP16下Yolov5s约10-15ms,这步超时先怀疑显卡算力和引擎版本。
  4. 只测后处理时间,置信度过滤加NMS,一般1-3ms。

定位到具体环节后再针对性优化。如果每一段都快,但总帧率还是上不去,那就是线程调度和缓冲的问题了,优先检查队列是否积压、是否频繁加锁、是否存在不必要的cv::Mat深拷贝。

一些想法收尾

整个方案做下来,我最深的感受是:深度学习部署这件事,真正难的从来不是模型,而是把模型塞进一个工业系统里,让它和其他模块协同工作。相机采集、图像处理、推理引擎、接口设计、线程调度,任何一环掉链子,最终效果都归零。Yolov5本身已经足够成熟,TensorRT的性能也足够好,多数项目失败不是因为模型选得不好,而是因为在环境配置、数据格式转换、DLL接口设计这些琐碎又关键的环节上踩了坑没人带。

如果你们也在做类似的视觉检测项目,我建议把每一步的验证做扎实。导出的模型先用onnxruntime验证一遍,TensorRT的engine先跑测试图片验证输出,DLL先写一个简单的C++控制台程序调用,最后再接C#上位机。每增加一层调用关系,就多一层出错的可能,逐层验证能帮你把问题迅速定位到具体模块。

最后再分享一个小技巧:DLL工程在Visual Studio里编译时,把调试信息和符号文件(.pdb)保留好,同时开启/Zo选项增强优化调试。当上位机那边出了诡异崩溃,你把这套PDB文件给到对方,用VS附加到进程调试,十次里有八次能直接定位到问题出在DLL哪一行。这一手在工业现场排障时,比看半天日志高效太多了。

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

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

立即咨询