☰
C#实战YOLOv9螺栓漏装检测:零中间层部署与PLC联动方案
2026/10/22 8:40:28 网站建设 项目流程

1. 项目背景与方案选型

1.1 螺栓漏装检测为什么难

在汽车装配产线上,螺栓漏装属于那种“看着简单、做起来要命”的工位。一个轮毂上可能有5颗、8颗甚至更多的螺栓,装配工人在节拍压力下少拧一颗,靠人眼复检很难百分之百拦住。产线停下来返工是小事,要是不合格品流出去了,那就是质量事故。

我之前接过一个类似的工位改造需求:轮毂螺栓装配检测,要求实时性高、误报低、能直接对接产线PLC。最初方案想得挺简单——用普通工业相机拍一张图,算一下高亮区域数量就行。但实际上螺栓头是金属反光面,光照角度一变,亮斑就乱七八糟;再加上轮毂本身也有圆形孔洞和纹理,传统图像处理的分割阈值完全扛不住复杂背景。于是视觉部分基本锁定深度学习目标检测,能够直接定位每一个螺栓的位置、判断有没有装,同时还能检测螺栓是否安装到位(漏装、未拧紧、错装)。

模型选型上,当时对比了几个目标检测框架,最终用了YOLOv9。原因也比较实际:它比YOLOv5/v8在特征提取上更强,对小目标和密集目标(比如一排螺栓挨得很近)的召回率更好,而且部署资料多、导出ONNX顺畅,踩坑成本低。

1.2 为什么能省掉Python/C++中间层

很多团队做这类项目,习惯的架构是“Python跑模型+C++/C#做界面+串口/以太网跟PLC通信”。这套组合成熟,但是有个麻烦:Python推理服务进程一旦崩了,上位机还傻等,两个进程之间的通信调度、异常同步都要单独写,甚至要再加一层Redis或者HTTP服务。对工厂环境来说,多一个进程就多一个故障点,而且每次现场调试,你都得在两个进程的日志里来回翻,效率很低。

我这里说的“零中间层”,意思是整个视觉处理和上位机控制逻辑全部跑在C#进程里。用ONNX Runtime的C# API直接加载YOLOv9导出的模型做推理,推理结果留在进程内存里直接用,不需要序列化、不需要走Socket、不需要写JSON。PLC通信、相机采集、UI刷新、检测逻辑都在同一个进程内协作,写起来像在写单机程序,定位问题也很直接。

当时不少人劝我用C++做推理层,理由是C++性能更快。但那都是老观念了。ONNX Runtime在C#和C++下用的是同一个底层推理引擎,实际跑640x640输入的推理,单张耗时差距通常在2-5毫秒以内,对一个节拍3-5秒的检测工位来说完全无感。而C#在写界面、写通信、调试效率上的优势,是实打实的。

1.3 整体架构与工作流程

这套系统从物理上分为三大部分:

  • 工业相机:采集轮毂工位照片,通过GigE或者USB3连接到工控机。
  • 工控机(Windows):运行C#上位机程序,内置YOLOv9 ONNX推理引擎,负责图像采集、螺栓定位识别、结果判定。
  • 可编程控制器PLC:控制产线运动、电磁阀、传感器。相机触发信号由PLC输出给工控机,检测完成后工控机把OK/NG结果写回PLC指定数据块,PLC根据结果决定放行还是拦截。

整个流程看起来像一条流水线:

  1. 轮毂到达检测工位,传感器到位,PLC发送“拍照触发”信号(比如把数据块里某个位写1)。
  2. C#上位机轮询到这个信号,立即从相机采集一张当前工位的图像。
  3. 图像缩放填充到640x640,转为Tensor输入ONNX模型,推理得到每个螺栓的边界框和类别。
  4. 上位机完成规则判断:该工位应当安装的螺栓是否全部存在且类别正确,是否存在漏装/错装区域。
  5. 把OK或NG标志写回PLC的一个数据位。
  6. PLC根据结果控制下一个动作:OK则放行,NG则让轮毂进入返修通道,同时上位机保存当前NG图像,方便后续追溯。

这套架构最大的好处:现场调试时,一台笔记本电脑加一根网线就能把整条检测逻辑跑通。相机、PLC、模型推理全在上位机进程里,日志也是同一个文件,排查问题很快。

2. 部署模型:从YOLOv9训练到ONNX导出

2.1 数据集的采集与标注经验

数据是整个检测项目的地基。螺栓检测的样本和普通公开数据集不太一样,特别注意三点:

  • 光照多样性:白天、晚上、车间灯光、侧面补光、无补光,每个场景都要拍。我们要求现场采集至少覆盖两种光照模式,最好是把生产线上不同班次、不同天气的图都拿上。最终数据集中大约有2万张真实产线图像,加上各类增强,凑到3万张左右。

  • 负样本和难例:很多团队只看“有螺栓”的数据,结果上线后发现大量的误检,因为没有让模型见过“没有螺栓时的样子”。轮毂上明明没有螺栓,模型却因为孔的纹理误判为螺栓。所以,负样本必须单独标注一个“missing”类别?还是让模型不输出?我这里采用的是显式类别:除了螺栓(bolt),还标了一个类别“missing_screw”,专门表示“这个位置应该有螺栓但没装”的空洞。这样模型能学会区分“正常孔”和“漏装孔”,后期判断逻辑也会简单很多。

  • 标注框一定要贴紧:螺栓目标小,如果标注框上下左右留太多空白,训练出来的框偏移会很严重,直接影响后面的坐标规则判断。我们用标注工具统一调整为贴合螺栓边缘。对于部分遮挡的螺栓,只要还能分辨,就标上;完全被遮挡的,不标,避免给模型发送错误信号。

2.2 模型训练与权重选择

YOLOv9主要有两种结构:T(tiny)和C(compact),再往上还有E(extended)。工位检测的硬件大多是工控机,CPU可能一般,GPU也没有独立的显卡,所以我一开始就没考虑YOLOv9-C这种大模型,直接选了YOLOv9-T,在识别精度下降可控的前提下,把推理速度放在第一位。

训练时,输入尺寸640x640,类别设为2类:bolt和missing_hole。批量大小在单卡上设为16,初始学习率0.01,使用余弦退火调度。简单说下训练过程的直观感受:大约100轮以内,验证集mAP就能爬到0.9以上,到200轮基本收敛稳定。主要卡时间的是数据读取和增强,如果训练机器磁盘慢,可以先把数据全部读进内存。这个项目用的标签格式是YOLO的txt格式,归一化坐标,没有特殊处理。

训练完成后,权重保存在best.pt中,接下来就是导出。

2.3 模型导出ONNX的操作与验证

导出这一步是整个项目的关键。导出前先确认一版环境:

pip install ultralytics onnx onnxruntime-gpu onnxsim python export.py --weights best.pt --include onnx --opset 12 --simplify

如果你用的是官方YOLOv9仓库,导出完成后会在同一目录生成best.onnx。这里有几个非常容易踩的坑:

  • 导出前关闭模型训练模式,确保权重已经推理模式。官方脚本一般会自动处理,但如果改了源码,注意eval。
  • Opset版本建议不低于12,太低的话某些算子不支持。我实测opset 12和opset 17差别不大,但别用opset 11,个别结构在ONNX Runtime新版上会报错。
  • 导出后一定要用onnxsim做简化,能消掉一部分多余的恒等算子,减少模型大小。螺栓检测模型从40MB降到35MB左右,推理时间差不了太多,但简化掉一些微量开销总是好的。

导出完成后,用Python快速验证一下ONNX的输出和PyTorch原始权重输出是否一致:

import onnxruntime as ort import numpy as np import cv2 img = cv2.imread("test_bolt.jpg") img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = cv2.resize(img, (640, 640)) img = img.astype(np.float32) / 255.0 img = np.transpose(img, (2, 0, 1)) img = np.expand_dims(img, axis=0) sess = ort.InferenceSession("best.onnx", providers=["CPUExecutionProvider"]) outputs = sess.run(None, {"images": img}) print(outputs[0].shape)

这一步很重要:如果输出张量的shape和预期一致(比如1, 6, 8400,其中6是xywh、conf、label的组合),说明导出成功。如果模型有多个输出节点,建议写个小的归一化脚本,统一后处理逻辑。

提示:如果导出时加了nms或end2end选项,输出格式会变成1x300x6这种固定框数量格式。这个项目里我选择不经NMS的原始输出,把后处理完全交给C#实现,灵活度更高,调参也不用重新导出模型。

3. C#端推理引擎搭建

3.1 项目结构与本方案的技术栈

C#上位机项目使用的核心依赖:

  • ONNX Runtime(Microsoft.ML.OnnxRuntime.Gpu)——加载和执行YOLOv9 ONNX模型。如果工控机没有NVIDIA显卡,就用CPU版本,实际检测速度也够。
  • OpenCvSharp4.Windows或Emgu CV——负责图像采集之后的封装、颜色转换、尺寸缩放、画框。这类库在NuGet上直接安装,集成非常方便,不需要额外配置C++编译环境。
  • 开源S7通信库——用于和PLC建立以太网通信,直接读写数据块。

整个上位机主体是一个WPF或者WinForms应用。我们这版用了WPF,因为界面需要实时显示相机画面和检测框,WPF的绑定和渲染比WinForms更顺手。当然WinForms也可以,逻辑上没有区别。

3.2 C#端图像预处理实现

先定义检测结构体。我们最终YOLOv9模型输出是[1, 6, 8400]这种格式(取决于导出的decoupled输出),这里以常见的输出格式做示例。

预处理的核心是LetterBox缩放——把原始图像等比缩放到640x640,周围用灰边填充,避免截断目标。很多新手直接Resize,结果螺栓变形、长宽比失真,模型精度掉得厉害。这一步必须保留原始比例。

public class YoloV9Detector { private readonly InferenceSession _session; private readonly int _inputSize; private readonly float _confTh; private readonly float _nmsTh; private readonly float _scale; private readonly int _padW; private readonly int _padH; public YoloV9Detector(string modelPath, int inputSize = 640, float confTh = 0.25f, float nmsTh = 0.45f) { _session = new InferenceSession(modelPath); _inputSize = inputSize; _confTh = confTh; _nmsTh = nmsTh; } public List<Detection> Detect(Mat originalImage) { var (letterBoxed, _scale, _padW, _padH) = LetterBoxResize(originalImage, _inputSize); var tensor = MatToTensor(letterBoxed); var inputs = new List<NamedOnnxValue> { NamedOnnxValue.CreateFromTensor("images", tensor) }; using var results = _session.Run(inputs); var outputTensor = results.First().AsTensor<float>(); var rawDetections = DecodeOutput(outputTensor); return NonMaxSuppression(rawDetections); } }

LetterBoxResize的实现:

private (Mat resized, float scale, int padW, int padH) LetterBoxResize(Mat src, int size) { int h = src.Rows; int w = src.Cols; float scale = Math.Min((float)size / w, (float)size / h); int newW = (int)(w * scale); int newH = (int)(h * scale); var resized = new Mat(); Cv2.Resize(src, resized, new Size(newW, newH), 0, 0, InterpolationFlags.Linear); int padW = (size - newW) / 2; int padH = (size - newH) / 2; var canvas = new Mat(size, size, MatType.CV_8UC3, new Scalar(114, 114, 114)); resized.CopyTo(canvas[new Rect(padW, padH, newW, newH)]); return (canvas, scale, padW, padH); }

把图像转成Tensor:

private DenseTensor<float> MatToTensor(Mat img) { // YOLOv9输入是 CHW 顺序,BGR通道翻转成RGB,再归一化到0-1 var tensor = new DenseTensor<float>(new[] { 1, 3, _inputSize, _inputSize }); for (int y = 0; y < _inputSize; y++) { for (int x = 0; x < _inputSize; x++) { var pixel = img.At<Vec3b>(y, x); tensor[0, 0, y, x] = pixel[2] / 255f; tensor[0, 1, y, x] = pixel[1] / 255f; tensor[0, 2, y, x] = pixel[0] / 255f; } } return tensor; }

这段代码虽然看起来像素级操作很慢,但实际上在Release模式下JIT会做很多优化,640x640输入单张预处理大约10毫秒以内,完全不影响整体节拍。如果你追求极限性能,可以用OpenCvSharp的Split把BGR通道拆开再分通道赋值,或者直接使用Tensor的缓冲区指针用unsafe方式操作,但那些都属于进阶优化,前期不必折腾。

3.3 后处理与NMS实现

YOLOv9的原始输出是若干特征图合并之后的结果,每一行对应一个预测框。假设输出shape是(1, 6, 8400),其中6 = 4个坐标 + 1个置信度 + 1个类别编号,这里我按这个格式解码。

private List<Detection> DecodeOutput(Tensor<float> output) { int rows = output.Dimensions[2]; // 8400 int cols = output.Dimensions[1]; // 6 或 numClass+5,取决于导出配置 var list = new List<Detection>(); for (int i = 0; i < rows; i++) { float xc = output[0, 0, i]; float yc = output[0, 1, i]; float w = output[0, 2, i]; float h = output[0, 3, i]; float objConf = output[0, 4, i]; // 如果模型已经提前解码类别,第5个元素是类别ID // 如果是旧式84输出,还要遍历后面的80类取最大值 if (objConf < _confTh) continue; int classId = (int)output[0, 5, i]; float score = objConf; // 映射回原图坐标 float x1 = (xc - w / 2 - _padW) / _scale; float y1 = (yc - h / 2 - _padH) / _scale; float x2 = (xc + w / 2 - _padW) / _scale; float y2 = (yc + h / 2 - _padH) / _scale; list.Add(new Detection( new Rect((int)x1, (int)y1, (int)(x2 - x1), (int)(y2 - y1)), score, classId )); } return list; }

注意一个细节:如果你导出的是带类别分支那种格式,即每个anchor有多个类别分数,那需要在循环里遍历类别维,取最大值作为最终置信度。我导出的模型开启了类别分支,所以上述代码直接取第六个通道。

NMS实现:

private List<Detection> NonMaxSuppression(List<Detection> detections) { var candidates = detections .Where(d => d.Score >= _confTh) .OrderByDescending(d => d.Score) .ToList(); var result = new List<Detection>(); while (candidates.Count > 0) { var best = candidates[0]; result.Add(best); candidates.RemoveAt(0); candidates = candidates .Where(d => IoU(best.Box, d.Box) <= _nmsTh) .ToList(); } return result; } private float IoU(Rect a, Rect b) { int interX1 = Math.Max(a.X, b.X); int interY1 = Math.Max(a.Y, b.Y); int interX2 = Math.Min(a.X + a.Width, b.X + b.Width); int interY2 = Math.Min(a.Y + a.Height, b.Y + b.Height); int interW = Math.Max(0, interX2 - interX1); int interH = Math.Max(0, interY2 - interY1); float interArea = interW * interH; float unionArea = a.Width * a.Height + b.Width * b.Height - interArea; return unionArea > 0 ? interArea / unionArea : 0; }

这个NMS是比较朴素的实现,因为螺栓数量少、目标密度高但不至于重叠严重,排序情况下性能足够。如果后续要扩展到几百个目标的场景,可以考虑使用集合式NMS或者直接调用OpenCvSharp的Dnn.NMSBoxes。

4. PLC联动与产线控制逻辑

4.1 与PLC的以太网通讯模块

工业环境中PLC品牌五花八门,我这次用的是S7协议,因为设备端用的就是S7-1200系列,支持以太网直连。C#里用开源的S7通讯库封装连接与管理即可。

连接代码:

public class PlcGateway : IDisposable { private Plc _plc; private readonly object _locker = new object(); public bool Connect(string ip, int rack = 0, int slot = 1) { try { _plc = new Plc(CpuType.S71200, ip, rack, slot); _plc.Open(); return _plc.IsConnected; } catch (Exception ex) { Log.Error($"PLC连接失败: {ex.Message}"); return false; } } public void Reconnect() { if (_plc != null && _plc.IsConnected) return; // 自动重连逻辑,带5次退避 for (int i = 1; i <= 5; i++) { if (Connect(_plcIp)) return; Thread.Sleep(i * 1000); } throw new PlcException("多次重连失败"); } public void WriteResult(bool ok) { lock (_locker) { // 这里定义DB10.DBX0.1为OK结果标志,DB10.DBX0.2为NG标志 if (!_plc.IsConnected) Reconnect(); _plc.Write("DB10.DBX0.1", ok); _plc.Write("DB10.DBX0.2", !ok); } } public bool ReadTrigger() { lock (_locker) { if (!_plc.IsConnected) Reconnect(); return _plc.Read("DB10.DBX0.0"); } } public void ClearResultFlags() { lock (_locker) { _plc.Write("DB10.DBX0.1", false); _plc.Write("DB10.DBX0.2", false); } } public void Dispose() { _plc?.Close(); } }

关于PLC通信有两个容易出事的地方。

  • 多线程访问同一个PLC连接对象,很容易产生“管道已断开”之类的异常。务必给读写操作加锁。
  • PLC的DB号和偏移地址必须和电气工程师核对清楚。我曾经就吃过亏——电气那边说的是DB10.DBX0.1,实际他手里还有一份旧图纸是DB12.DBX0.1,结果检测结果一直写不进去。所以现场施工之前一定要拿着拓扑图,逐点确认地址表。

4.2 触发-检测-反馈的完整闭环

整个检测状态机不复杂,但要注意不要把视觉处理和PLC通信混在一个线程里。推荐用三个线程协作:

  • 主线程(UI线程):显示图像、显示当前状态、处理人工操作。
  • PLC轮询线程:每10毫秒读一次触发信号,一旦检测到信号,调用相机采集并启动推理。
  • 相机采集线程:独立处理相机帧,防止因为相机SDK的回调阻塞导致画面卡死。

我简化后的主循环长这样:

public void ProductionLoop(CancellationToken ct) { while (!ct.IsCancellationRequested) { try { if (!_plcGateway.ReadTrigger()) { Thread.Sleep(10); continue; } // 清掉触发信号,防止重复处理同一轮 _plcGateway.ClearTriggerFlag(); var sw = Stopwatch.StartNew(); // 1.采集相机图像 using var frame = _camera.CaptureFrame(); // 2.推理 var detections = _detector.Detect(frame); // 3.业务规则判断 bool resultOk = EvaluateRule(detections, _expectBoltCount); // 4.显示和保存 var annotated = DrawDetections(frame, detections); _uiDispatcher.Invoke(() => UpdateScreen(annotated, resultOk)); if (!resultOk) { SaveNgImage(frame, detections); } // 5.写回PLC _plcGateway.WriteResult(resultOk); sw.Stop(); Log.Info($"本轮检测耗时: {sw.ElapsedMilliseconds} ms"); } catch (Exception ex) { Log.Error(ex.ToString()); // 某些异常需要将工位置为NG,等待人工处理 _plcGateway.WriteResult(false); } } }

注意ClearTriggerFlag这一步很关键,否则PLC如果忘记自动清位,上位机会同一帧图像重复检测很多次,产线动作乱套。

4.3 业务规则判定

螺栓漏装检测不能只看“模型输出了几个目标”,还要结合工位结构做规则:

  • 标准数量校验:这个工位应该装8颗螺栓,检测出来的“bolt”类别数量必须大于等于8。少于8,说明漏装。
  • 漏装区域校验:如果模型在某个应装位置输出了“missing_hole”,不管螺栓数量是否够数,都视为NG。这个设计是为了防“一颗螺栓没装但旁边多装了一颗”这种情况。
  • 类别校验:如果检测结果里出现“missing_hole”,直接NG。
private bool EvaluateRule(List<Detection> detections, int expectedCount) { var bolts = detections.Where(d => d.ClassId == (int)YoloClasses.Bolt).ToList(); var missing = detections .Where(d => d.ClassId == (int)YoloClasses.MissingHole) .ToList(); if (missing.Count > 0) return false; return bolts.Count >= expectedCount; }

如果某个工位有多排螺栓,比如前排4个、后排4个,可以额外加一个“按区域计数”的规则,把检测框按x坐标或者y坐标分区,分别统计数量。这里可以根据工位自由扩展,不必把所有逻辑塞进模型里——能用规则做的事,就不要用模型做,模型只负责“看见目标”,规则负责“判断对错”。

5. 实测效果与调优之路

5.1 从97%到99.99%的关键调整

首版系统在实验室测试时,准确率只有97%左右,主要体现在两类问题:一类是漏检,螺栓表面反光太严重导致没有检出;另一类是误检,把轮毂上的定位孔当成了螺栓。

经过几轮调整,准确率才逐步爬到99.99%这个量级。这几个调整是至关重要的:

  • 加入现场真实光照数据:实验室图片打光均匀,但现场车间顶部荧光灯、侧窗自然光、检测工位补光灯混合在一起,反差极大。采集来的真实数据重新标注并加入训练集后,召回率立刻上去不少。

  • 调整置信度阈值:首版用0.45的置信度,漏检很多。调到0.25后,召回率上来了,但误检也增加了一些。这里不能在训练参数里死磕,而是在后处理规则上区分:螺栓这种目标需要高精度定位,我把它单独设置0.35阈值;missing_hole这种需要高召回,单独设0.2。模型输出阶段用一个统一阈值过滤低质量框,之后按类别再做一次阈值筛选。

  • 增加目标框最小尺寸过滤:有些误检框特别小,大约是正常螺栓框的三分之一,这类框大概率是孔洞边缘误识别。在后处理时补充一个最小宽高限制,直接把太小的框丢掉。

5.2 误检和漏检的平衡

99.99%这个数字听起来吓人,但实际说的是“在固定测试集上的表现”,而且业务上我们更看重“漏检率”而不是“误检率”。

漏检意味着螺栓真的没装但系统没报警,这是生产事故,必须压到最低。误检意味着明明装好了但系统报警,这会额外增加人工复查工作量,影响节拍但在可控范围。

所以我的阈值选择策略是宁误勿漏:把missing_hole类的置信度阈值拉低,尽量让疑似漏装的目标都报出来;bolt类阈值略微拉高,避免乱七八糟的误报。正常情况下10000次检测中会出现几次误报,但螺栓漏装漏报的次数是零。这种取舍在工厂里是合理的,因为误报让工人看一眼就好,漏报会出大问题。

5.3 性能实测数据

在实际工控机上(无独显,i7-8700级别CPU),单张640x640图像的模型推理时间大约100-130毫秒,加上预处理、后处理、PLC读写和相机采集,整体流水线耗时在300毫秒左右。按3秒一个节拍算,余量非常大。

后来给工控机加了一张入门的NVIDIA显卡(类似GTX 1650级别),推理时间降到20-30毫秒,整条流水线耗时不到80毫秒。其实对这类项目来说,CPU跑100毫秒已经够用,不一定非要上显卡。要不要上GPU,取决于产线节拍有多紧,以及工控机里还有没有其他任务在抢CPU。

在项目落地两个多月后,统计结果是:累计检测超过3万件产品,漏装漏报0起;误报警出现过几次,全部是光照突变导致(比如有人站在相机前短暂遮挡,或者一个螺栓表面带着油污),经人工确认后放行。

6. 现场踩坑与排错实录

6.1 C#推理结果和Python完全不一样

这个坑遇到的人绝对不少。模型在Python里跑得好好的,导出的ONNX在C#里跑,框的位置偏移很大,或者干脆结果全是空。

我排查下来最常见的原因有两个:

  • 输入Tensor的通道顺序不是模型期望的顺序。YOLOv9训练时用CHW,而OpenCvSharp读图得到的是HWC,必须在赋值前转成CHW并把BGR转成RGB。上面代码里已经做了处理。
  • 归一化方式不一致。如果训练时使用了均值和方差归一化,导出模型后C#端不记得这件事,直接把像素除以255喂进去,结果自然错。YOLOv9本身用的就是0-1归一化,但要确认自己的训练配置有没有覆盖默认设置。

排查方法也比较简单:在C#预处理完成后,把Tensor里第一个通道的前几行数据打印出来,和Python代码里处理后的数据对比。如果数值对不上,就一条条追。

6.2 相机采集偶尔超时导致卡死

相机SDK采集图像有时候会因为网络冲突或者相机掉线而阻塞。刚开始我用单线程直接采集,一旦相机卡住,整个程序全部卡死,连界面都没反应。

后来改成独立相机线程,并给采集操作加了超时控制:

var task = Task.Run(() => _camera.CaptureFrame()); if (!task.Wait(TimeSpan.FromMilliseconds(1500))) { Log.Warning("相机采集超时"); _camera.Reconnect(); // 本轮不检测,直接认为系统故障,置NG _plcGateway.WriteResult(false); return; }

虽然每次采集外包一个Task有点土,但胜在简单、稳定。工程现场最重要的是程序不会因为一个超时把整个工位锁死。

6.3 PLC通讯偶发异常与断线恢复

S7协议通信在稳定网络下没什么问题,但工厂车间电磁干扰大,偶发连接超时或者“目标机器积极拒绝”也很常见。我在PLC封装里加了重连机制,检测到连接失败后先尝试关闭再重新打开,退避时间为1秒、2秒、3秒、5秒,最多重试5次。如果重连也不成功,就上位机直接弹窗报警,并保持输出NG信号,让产线无法放行,这是最安全的状态。

另外,PLC侧也要配合设置:上位机写回DB区数据后,PLC应该在一个扫描周期内读取并复位,并且最好增加握手信号——上位机写结果后,PLC把结果取走再回写一个“已读取”标志。这样就不会出现上位机和PLC因为时序差而对同一个标志位反复读写的情况。

6.4 模型更新时需要停机吗

之前每次更新模型权重,都需要重启上位机进程。后来我做了一个小优化:在上位机里加载模型时,把模型路径放到配置文件里,增加一个“热更新”按钮。点击后程序重新创建InferenceSession并替换当前引用,不需要退出程序。虽然ONNX Runtime初始化新模型对象需要几秒钟,但放在检测间隙执行完全可行。这个功能对现场调试特别有用,因为你可以现场标注新数据、重新训练、导出新ONNX,然后直接让产线程序热加载,不用反复开关机。

最后的实操体会

这套C#直接集成YOLOv9的方案,我在两个类似项目里用过,整体评价是:省掉了中间层,现场调试效率至少提升一倍。不需要在两个进程的日志里找问题,不需要担心Python解释器崩溃导致整个视觉服务挂掉,所有状态、缓存、业务规则都放在一个进程里,逻辑非常清晰。

如果你也想做类似项目,我建议先别急着买昂贵的GPU工控机。先把相机、PLC地址表、模型精度这三件事坐下来和机械、电气工程师对齐,尤其是PLC的数据块地址。很多项目延误都是出在这种“鸡毛蒜皮”的对接上,而不是算法跑不动。

另外一个经验是:不要过分追求模型指标好看,把漏装检测做成100%。生产线上真正要的是稳定、可解释、可追溯。只要每一轮检测都有日志、有图像保存、有PLC反馈记录,即使偶尔误报一次,也能顺着记录快速定位原因,这才是工业项目该有的样子。模型推理只是其中一环,整个系统稳定闭环了,产线才会真正信任这套视觉检测。

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

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

立即咨询