1. 项目整体思路为什么把YOLO放到边缘端跑1.1 场景痛点传统上位机方案卡在哪去年年中接到一条产线的视觉检测改造需求我当时最开始的想法是沿用过去的套路工业相机拍照图像传到工控机的上位机软件里上位机调用一个算法模块做检测再把结果通过IO卡丢给PLC。这套方案听着顺理成章但实际跑起来有很现实的问题。产线上相机是2000万像素的全局曝光相机一张图算上ROI裁剪也有差不多20MB数据量千兆网传上来大概需要160毫秒以上。本来相机的拍摄节拍是2秒一张加个传输延迟问题不大但我们又要求图像上传后还要做预处理、推理、结果判定、数据打包回传PLC整个链路跑完单张图的检测周期直接逼近1.8秒。这只是单相机的情况产线是双工位交替拍照上位机要同时处理两路图像CPU占用动不动飙到85%以上。更尴尬的是检测算法如果后续要升级换模型就得重新发布整套上位机程序产线停线陪着改代码客户那边很难接受。后来我们换了个思路把检测这块从上位机里彻底剥离出来下沉到一台边缘计算设备上上位机只负责业务逻辑、流程控制和数据展示。这就是这个项目最终选定的架构——边缘端负责图像采集、预处理和YOLO推理C#上位机负责工位调度、结果接收和与PLC的握手通信。1.2 架构设计边缘端加上位机的分工逻辑整套系统最终的拓扑是这样相机直接接到边缘计算盒子就是一台带独立GPU的迷你工控机通过相机厂家SDK取流本地完成ROI裁剪、畸变校正、缩放、归一化这些预处理。边缘端跑YOLO推理产出检测框、类别、置信度然后把结构化结果通过gRPC推给上位机不再传整张图片。C#上位机部署在普通工控机上负责界面展示、工位逻辑、与PLC的Modbus TCP通信、结果判定和数据库落盘。原始图像只在边缘端本地保存一份方便后续追溯和误检分析不占上位机带宽。这个分工的核心逻辑是尽量让图像数据在本地闭环跨设备只传结果不传原始图。一张20MB的原始图经过检测之后压缩成几百个字节的JSON结果整个传输延迟从几百毫秒降到几毫秒上位机的负载也大幅下降。1.3 技术选型复盘C#凭什么接这活有人会问你边缘端都用YOLO了干嘛不直接用Python一条龙写完非要拉上C#说实话推理端用Python写确实很顺畅但在工业上位机这个场景里Python不是最优答案。工业上位机要对PLC、要对数据库、要对MES系统还要面对客户现场五花八门的Windows环境和老旧的.NET Framework版本——在这些地方C#才是真正的舒适区。而且C#现在跨平台做得很好.NET 6以上的版本在Linux边缘设备上跑gRPC服务端完全没问题。边缘端和上位机可以用同一种语言同时开发代码风格统一出了问题排查链路过短一个人能顶两个端。但这个项目里边缘端还是保留了部分Python组件——主要是数据标注和训练脚本。实际部署的推理服务是C#写的模型训练和调参用Python中间通过ONNX模型文件桥接。2. 模型训练与边缘端部署准备2.1 数据来源与标注规范这个项目的检测对象是铸造件表面的气孔、砂眼和裂纹都属于典型的Small Object Detection缺陷在2000万像素原图里往往只占几十到几百个像素。我们总共收集了大概12000张真实产线的缺陷图像其中缺陷样本大概有8700张正常样本3300张。标注这块走了不少弯路。最初用通用的标注工具画矩形框但小目标缺陷的框稍微偏一点IoU差异就很明显直接拉低mAP。后来我们把标注规范细化了气孔要求框贴合缺陷最小外接矩形砂眼要求框中心对准缺陷重心裂纹这种细长目标允许用多边形标注后再转旋转矩形训练时统一转成外接正矩形。这里有个小技巧——训练阶段不要直接用原始尺寸的标注框按网络下采样倍数对齐一下比如网络是640x640输入且下采样32倍标注框的坐标最好取整到32的倍数附近能减少标签与特征图对齐的误差。数据增强方面我没有一上来就上Mosaic、MixUp这种重增强——检测对象是小缺陷过度裁剪和拼接会导致背景上下文丢失模型容易把正常纹理误检为缺陷。最终采用的增强组合是轻微旋转正负15度以内、随机亮度和对比度扰动模拟不同光照、随机裁剪后缩放保持缺陷相对尺寸不变以及少量高斯噪声。2.2 训练配置与精度调优我们边缘端的GPU是入门级的显存只有6GB所以模型不能选太大的。YOLO系列的选型对比过v8n和v8s还有YOLO11n最后定下来用v8n做底子原因是它的参数量只有3.2M640x640输入下推理延迟可以压到20毫秒以内TensorRT FP16精度我们通过后续的针对性优化补了回来。训练时输入分辨率没有直接拉到640而是先按1280跑了一个大分辨率模型当老师再用教师模型对640输入的学生模型做蒸馏。此外还加了一个很有效的小技巧把缺陷类别做成“二阶段识别”也就是第一遍先跑一个粗糙的YOLO找候选区域第二遍在候选区域放大后跑细分类。这个在推理阶段会稍麻烦一点我们后面用C#实现时做了两级推理管线的支持。最终在验证集上的指标是mAP500.957mAP50-950.762小目标能做到这个数已经很能打了气孔类别的召回率0.961裂纹类别因为形状细长误检率偏高后续专门做了针对性优化。2.3 模型导出与边缘端适配这一步是整个项目里最容易翻车的地方。PyTorch训练的模型导出到ONNX再转TensorRT中间只要有一个算子不兼容整个流程就要卡住。我们用到的关键导出命令大致是import torch model.model.eval() dummy_input torch.randn(1, 3, 640, 640) torch.onnx.export( model.model, dummy_input, best_fp32.onnx, opset_version17, input_names[images], output_names[output0], dynamic_axes{images: {0: batch}} )导出时有几个关键点opset_version不能太低避免新算子不支持dynamic_axes尽量只保留batch维度宽高固定成640这样TensorRT构建engine时能拿到更精确的优化信息。ONNX转TensorRT用的是trtexec工具FP16精度构建/opt/tensorrt/bin/trtexec \ --onnxbest_fp32.onnx \ --saveEnginebest_fp16.engine \ --fp16 \ --minShapesimages:1x3x640x640 \ --optShapesimages:1x3x640x640 \ --maxShapesimages:4x3x640x640构建engine过程中我发现一个坑v8n模型里有些上采样算子会触发TensorRT的某次崩溃排查到原因是插件版本不匹配后来换了TensorRT 8.6的容器重新构建才通过。这块建议直接用NVIDIA官方NGC容器来做转换省掉很多环境依赖问题。3. C#边缘端推理工程化3.1 推理方案选型我用过的三种方案从一开始想用的方案到最终落地C#这边的推理路线换过三轮。第一轮直接用ONNX Runtime的C# API简单省心模型加载就能跑CPU和GPU都支持。问题是延迟偏高640x640输入在6GB的入门级GPU上单次推理大约35毫秒配合预处理和后处理整体在50毫秒上下。做单路够用双路并发就有点紧。第二轮换成OpenVINOIntel的推理套件但我们的GPU是NVIDIA的OpenVINO跑GPU得走插件模式性能并不理想CPU模式又明显不如直接用ONNX Runtime。这轮很快就放弃了。第三轮就是最终的方案TorchSharp加载PyTorch导出的TorchScript模型还是在GPU上跑CUDA——这个方案性能很不错但部署依赖重。后来因为要集成到客户的边缘盒子上环境不可控又回头选型发现NVIDIA官方其实一直有人在维护一个叫Yolov8.Onnx.Runtime的封装模式但说实话那套方案在输入输出处理上还是太绕。最后狠狠心把ONNX Runtime和TensorRT通过P/Invoke封装成了统一的C#推理接口。模型推理走TensorRT前处理、后处理用C#手写这一套组合拳打下来单张图端到端延迟压到了22毫秒。3.2 ONNX Runtime集成实战其实对于大多数场景ONNX Runtime已经够用我在这里给出最终项目里保留下来的一个通用封装作为备用推理管线。using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; public class YoloOnnxInference : IDisposable { private readonly InferenceSession _session; private readonly int _inputSize 640; public YoloOnnxInference(string modelPath) { var options new SessionOptions { GraphOptimizationLevel GraphOptimizationLevel.ORT_ENABLE_ALL, IntraOpNumThreads 4 }; _session new InferenceSession(modelPath, options); } public ListYoloDetection Detect(Mat image) { var resized Cv2.Resize(image, new Size(_inputSize, _inputSize)); var input Preprocess(resized); // BGR - RGB, 归一化, HWC - CHW var inputTensor new DenseTensorfloat(input, new[] { 1, 3, _inputSize, _inputSize }); var inputs new ListNamedOnnxValue { NamedOnnxValue.CreateFromTensor(images, inputTensor) }; using var results _session.Run(inputs); var output results.First().AsTensorfloat(); return Postprocess(output); } }这里面需要特别注意的是输入Tensor的维度顺序和数据类型。OpenCV读进来的是HWC的BGR图像要转成CHW的RGB数据类型必须是float且归一化到0到1之间。很多新手在这里踩坑出来的检测框全乱套其实就差两步转换。后处理核心就是解析输出TensorYOLOv8的输出是[1, 84, 8400]84 4个框坐标 80个类别概率COCO但不知道你的模型是不是COCO类。我们训练的是自定义缺陷类别所以最后一维是类别数 4。这里要写一个通用的维度解析逻辑直接用形状信息推断var shape output.Shape; // [1, 84, 8400] int numClasses shape[1] - 4; int numCandidates shape[2];然后逐列做置信度筛选和NMS。3.3 性能实测数据与硬件适配边缘端最终选的是某品牌迷你工控机CPU是八核低功耗版GPU是6GB显存的入门级专业卡功耗墙限制在45W。整机大概一本书大小DIN导轨安装适合塞进电气柜。实际跑出来的数据大致是环节耗时相机SDK取流1200万像素58毫秒ROI裁剪缩放归一化16毫秒TensorRT FP16推理640x64014毫秒后处理含NMS6毫秒gRPC结果传输2毫秒端到端总计约96毫秒这个延迟意味着单张图从快门触发到结果回传PLC可以控制在100毫秒以内产线节拍2秒一拍的情况下边缘端还剩余大量的算力余量。我们甚至用剩下的算力额外跑了一个简单的异常监控模型专门检测传送带上的卡料——等于一个边缘盒子干了两个检测活。4. 上位机与边缘端通信集成4.1 通信协议选型为什么选gRPC上位机要跟边缘端通信方案其实有几个HTTP JSON简单粗暴但实时性一般MQTT适合物联网分布式但在这条产线的局域网场景下有点重TCP 自定义协议性能最好但每次改协议都要同步改两端的解析代码最终选了gRPC理由有几个gRPC基于HTTP/2客户端和服务端之间可以维持长连接不用每次请求都重新握手延迟低带宽占用小。接口定义用Protocol Buffers改了消息结构能自动生成两端的强类型代码不会出现两边字段名对不上这种低级问题。自带流式传输后续如果要加视频流推流可以直接在一个连接上搞定不用另起一路协议。这个项目里我们定义了三个主要的gRPC服务service DefectInspection { rpc DetectImage(DetectRequest) returns (DetectReply); rpc StreamDetectResult(stream DetectRequest) returns (stream DetectReply); rpc GetHealthStatus(Empty) returns (HealthStatus); } message DetectRequest { string camera_id 1; string image_path 2; int64 timestamp 3; } message DetectReply { string request_id 1; bool has_defect 2; repeated DefectBox defects 3; float inference_ms 4; int64 timestamp 5; }image_path就是边缘端本地保存的原始图路径上位机如果需要追溯直接拿这个路径去边缘端拉图就行正常情况下只传检测结果。4.2 数据帧设计与心跳机制虽然gRPC已经把消息帧封装好了但工业上位机跟PLC握手的数据帧反而要自己设计。上位机从PLC拿到的是触发信号也就是一个bool位的上升沿PLC通过Modbus寄存器传过来。上位机把触发事件转成检测请求发到边缘端边缘端返回结果上位机再写回PLC的寄存器。整个闭环的关键参数是超时控制我们最初的版本里设了500毫秒超时但是PLC扫描周期如果是50毫秒500毫秒有可能在极端情况下漏掉一个上升沿。最后调成PLC每100毫秒同步一次触发状态上位机做边沿检测后立即发请求超时设为2秒——2秒内没返回结果就判定超时PLC那边走超时物件剔除流程。另外边缘端和上位机之间维持一个心跳机制边缘端每5秒上报一次运行状态包括GPU利用率、显存占用、模型推理平均耗时、最近的错误码。上位机界面上做一个小图标绿色正常、黄色性能劣化、红色离线。4.3 上位机实时显示与结果落库C#上位机这部分用WinForms实现的客户现场对界面要求不高重要的是信息密度和操作效率。主界面分成三块左侧是实时检测结果列表中间是当前检测图像缩略图和缺陷框叠加底部是产线统计面板。图像显示这里有个性能关键点——不要把边缘端的结果JSON转成图像再传过来那样等于把原始的带宽压力又请回来了。正确做法是上位机显示缩略图直接用边缘端保存的原始图路径通过gRPC单独开一个缩略图下载接口用低分辨率图来显示如果客户要点开大图看细节再走另一个接口拉原始图。结果落库用的是SQLite按天分表一张表存检测请求ID、相机ID、时间戳、缺陷数量、缺陷类别和坐标、检测耗时另一张表存原始图像路径和缩略图路径。数据库的写入频率大概是每2秒一条SQLite完全扛得住。实测连续跑72小时数据库文件大约膨胀到1.2GB我们每天凌晨做一次自动归档把前一天的数据导出成CSV并压缩再把原表清空。5. 落地过程中的问题与排查实录5.1 推理卡顿与帧率不稳项目联调阶段遇到过一个很诡异的问题单独压测边缘端推理服务的时候性能很正常但和上位机连起来跑整个链路推理延迟从正常的14毫秒飙升到80多毫秒而且是一阵一阵的。排查过程比较曲折。先怀疑是网络问题抓包看gRPC请求和响应的时间差发现瓶颈确实发生在边缘端内部。又怀疑是GPU被某个进程占了一看并没有。最后在边缘端部署了性能计数器发现CPU使用率有规律性的尖峰而且和推理延迟的尖峰时间是吻合的——问题出现在C#侧的后处理代码里。后处理中用到的NMS实现按照类别循环每个类别内部做IoU排序这个逻辑在类别少的时候没问题。但我们的缺陷类别有6个每张图的候选框有8400个6个类别的排序操作叠加起来最坏情况下要处理数万次IoU比较。我最初实现时直接用了List的Sort每帧都触发一次内存分配GC压力飙升导致整个推理管线周期性抖动。解决办法是把NMS改成预分配内存的朴素实现避免每帧分配新列表。同时把候选框的预筛选阈值从0.25提高到0.5在进入NMS之前就已经筛掉了90%以上的低置信度候选框。优化之后推理延迟在双路并发下稳定在20毫秒左右。5.2 误检漏检的实际调优记录产线试运行阶段最大的麻烦是误检——正常铸件表面有一些机加工纹路在某些光照角度下很容易被模型当成裂纹。我们尝试了三个方向的优化。第一个方向是数据层面把正常纹理的图片加入训练集同时用硬负挖掘Hard Negative Mining的策略把之前模型误检的样图人工标注后重新加入训练大约补了1800张负样本。第二个方向是后处理层面对置信度阈值做类别差异化裂纹类的阈值从0.25提升到0.55虽然召回率掉了一点但误检率降低了七成。第三个方向是业务逻辑层面上位机加了一个“复检机制”——单张图被判为有缺陷时边缘端会自动对缺陷区域做一次局部放大推理只有两次结果一致的才判定为真阳性。这三个方向合在一起现场最终达到的指标是漏检率小于0.2%误检率小于1.5%客户验收通过。5.3 工业现场断网与重启恢复产线环境最怕的是设备掉线后恢复不了得人工过去重启。我们在整个系统里做了三层防护。第一层边缘端和上位机之间的gRPC连接断了之后客户端会自动做指数退避重连初始1秒最大30秒。重连成功后边缘端会把断线期间积压的检测结果按照时间戳顺序补发给上位机保证数据不丢。第二层上位机与PLC通信的Modbus TCP如果断开上位机会持续重试并且把通信状态写入本地日志和界面状态栏PLC那边也会检测到通信超时自动把产线进入安全暂停模式排出半成品后等待上位机恢复。第三层边缘端整机断电重启后系统服务采用systemd托管开机自启并且通过一个持久化的本地队列保存未上报的结果服务起来之后自动把队列里的数据推送给上位机。这一步很重要否则边缘端重启的几十秒时间里产线上拍的照片就全丢了。这三层机制目前稳定运行了几个月断网重连测试做过十几次最久一次是边缘端断电5分钟后再上电恢复后1分钟内全部数据补发完成PLC那边自动复位。6. 个人复盘与后续扩展方向项目上线之后我自己也复盘过几轮有几点体会比较深。第一大坑是启动时贪多求全一开始把工位管理、设备管理、报表系统、用户权限全塞进上位机里结果开发周期一拖再拖。后来狠心砍掉了将近一半的非核心需求用“先跑通最小闭环再逐步叠加功能”的方式推进最终才在预算周期内交付。这个思路在工业项目里尤其重要——产线不会等你把界面做到完美再开始干活。第二点是从第一天起就该有日志和性能监控而不是出了问题再补。我们边缘端服务的prometheus指标是项目进行到一半才加上去的导致前期几次性能抖动全靠人工猜浪费时间。如果一开始就把推理耗时、检测率、误检率、GPU利用率全部可视化排查问题的效率至少翻一倍。第三点是模型迭代要跟业务剥离。现在边缘端加载模型支持热切换上位机下发新模型文件后边缘端用新的模型文件启动一个新的推理实例推理完成后自动切换流量不需要重启服务。这让后续模型优化不会影响产线生产把“模型升级”变成了一个运营动作而不是改造工程。当前这套系统还有几个想继续扩展的方向。一是把几个边缘节点的检测结果汇聚到一台中心服务器上做全局产线质量趋势分析用统计方法提前预警模具磨损或者工艺参数漂移。二是把上位机的WinForms界面逐步迁移到跨平台框架上这样客户将来要改成Linux工控机或者加一套Web端远程监控也更方便。三是尝试在边缘端跑更多的轻量模型不只是缺陷检测——比如OCR识别批次号、零件姿态估计、装配步骤校验这些都是工业现场常见的物料管理场景。做工业视觉项目这几年最大的感受是技术本身往往不是瓶颈如何把模型、算力、上位机、PLC和现场业务逻辑拧成一个可靠的系统才是真正考功夫的地方。希望这篇分享能给你一些参考少走几步弯路。