在这里插入图片描述
上个月接了个汽配厂的活,他们的发动机密封垫产线之前全靠人工目检,一天下来工人眼睛花,漏检率还高。老板要求搞个“眼睛+大脑+手脚”的系统:用相机当眼睛,YOLO当大脑,PLC当手脚,检测到缺陷直接分拣。花了三周时间调通,今天把这套C#上位机+YOLO+PLC的联动方案拆解开,从架构到代码,全是能直接落地的干货。

一、系统整体架构:稳定压倒一切

先给大家看这套系统的核心架构,没有花里胡哨的设计,全是工业现场验证过的可靠组合:

GigE Vision

ONNX Runtime

Modbus TCP

数字量输出

工业相机
(Basler acA2040)

C#上位机
(.NET 8 + OpenCVSharp)

YOLOv8n模型
(缺陷检测)

PLC
(西门子S7-1214C)

执行机构
(分拣气缸+传送带)

为什么这么选?给大家算笔账:

  • C#上位机:厂里老系统都是WinForms,技术栈统一,维护成本低;
  • YOLOv8n:速度快(640x640分辨率推理<50ms),精度够,导出ONNX后能直接在C#里跑,不用装Python环境;
  • Modbus TCP:不用额外装OPC UA服务器,PLC侧编程简单,连车间电工都能看懂;
  • 西门子S7-1200:稳定,抗干扰,汽配厂车间里的电磁干扰根本不是事。

二、核心模块实现:每一行代码都为7x24小时运行设计

1. YOLO视觉检测:从模型到C#调用的全流程

第一步:模型准备
我用LabelImg标注了5000张密封垫缺陷图(划痕、变形、气泡各占1/3),训练YOLOv8n,重点是导出ONNX格式时要加上--opset 12参数,否则C#里的ONNX Runtime会报错。

第二步:C#调用ONNX Runtime
这里有两个坑必须提醒大家:一是图像预处理必须和训练时完全一致(Resize到640x640、RGB通道、归一化到0-1);二是一定要用using语句释放资源,否则产线跑一晚上内存就爆了。

using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;
using OpenCvSharp;

public class YoloDetector
{
    private readonly InferenceSession _session;
    private readonly string[] _classNames = { "划痕", "变形", "气泡" };

    public YoloDetector(string modelPath)
    {
        var sessionOptions = new SessionOptions();
        sessionOptions.AppendExecutionProvider_CPU(0); // 用CPU推理,工控机没显卡也能跑
        _session = new InferenceSession(modelPath, sessionOptions);
    }

    public List<DetectionResult> Detect(Mat image)
    {
        // 1. 图像预处理:Resize、转RGB、归一化
        var resized = image.Resize(new Size(640, 640));
        var rgb = new Mat();
        Cv2.CvtColor(resized, rgb, ColorConversionCodes.BGR2RGB);
        var inputTensor = ConvertMatToTensor(rgb);

        // 2. 推理
        var inputs = new List<NamedOnnxValue>
        {
            NamedOnnxValue.CreateFromTensor("images", inputTensor)
        };
        using var results = _session.Run(inputs);

        // 3. 后处理:NMS去重、解析检测框
        return PostProcess(results, 0.5f, 0.4f); // 置信度0.5,NMS阈值0.4
    }

    private DenseTensor<float> ConvertMatToTensor(Mat mat)
    {
        var tensor = new DenseTensor<float>(new[] { 1, 3, 640, 640 });
        var data = mat.GetData<byte>();
        for (int i = 0; i < 640; i++)
        {
            for (int j = 0; j < 640; j++)
            {
                tensor[0, 0, i, j] = data[(i * 640 + j) * 3 + 0] / 255f; // R
                tensor[0, 1, i, j] = data[(i * 640 + j) * 3 + 1] / 255f; // G
                tensor[0, 2, i, j] = data[(i * 640 + j) * 3 + 2] / 255f; // B
            }
        }
        return tensor;
    }

    // PostProcess方法省略,核心是NMS和坐标转换
}

2. C#与PLC通信:加心跳、重连,一个都不能少

通信我选了工业界用烂的HslCommunication库,稳定、文档全。但工业现场最容易出问题的就是通信,所以我加了两个关键机制:心跳包自动重连

using HslCommunication.ModBus;
using System.Threading;

public class PlcController
{
    private readonly ModbusTcpNet _plc;
    private Thread _heartBeatThread;
    private volatile bool _isRunning;

    public PlcController(string ip, int port = 502)
    {
        _plc = new ModbusTcpNet(ip, port);
        _plc.ConnectTimeOut = 2000; // 连接超时2秒
    }

    public bool Connect()
    {
        var result = _plc.ConnectServer();
        if (!result.IsSuccess)
        {
            Console.WriteLine($"PLC连接失败:{result.Message}");
            return false;
        }

        // 启动心跳线程
        _isRunning = true;
        _heartBeatThread = new Thread(HeartBeatLoop) { IsBackground = true };
        _heartBeatThread.Start();
        return true;
    }

    private void HeartBeatLoop()
    {
        while (_isRunning)
        {
            Thread.Sleep(5000); // 每5秒心跳一次
            var result = _plc.ReadInt16("M100"); // 读一个无关的寄存器做心跳
            if (!result.IsSuccess)
            {
                Console.WriteLine("PLC心跳失败,尝试重连...");
                _plc.ConnectServer();
            }
        }
    }

    public bool SendDefectSignal(bool hasDefect)
    {
        // 写入M100:1=有缺陷,0=正常,重试3次
        for (int i = 0; i < 3; i++)
        {
            var result = _plc.Write("M100", (short)(hasDefect ? 1 : 0));
            if (result.IsSuccess) return true;
            Thread.Sleep(100);
        }
        Console.WriteLine("写入PLC失败,已重试3次");
        return false;
    }

    public void Disconnect()
    {
        _isRunning = false;
        _heartBeatThread?.Join();
        _plc.ConnectClose();
    }
}

3. PLC控制逻辑:简单到电工都能维护

PLC侧我用西门子TIA Portal写的,逻辑非常简单:当M100为1时,Q0.0输出触发分拣气缸,延迟2秒后复位。给大家看时序图,一目了然:

分拣气缸 西门子PLC C 工业相机 分拣气缸 西门子PLC C 工业相机 alt [检测到缺陷] [正常产品] 硬件触发拍照(PLC输出触发) YOLO推理(<50ms) 写入M100=1 Q0.0输出,气缸伸出 气缸到位反馈(I0.0) 延迟2秒,Q0.0复位 写入M100=0 无动作,产品流向下一站

三、实战效果:三个月收回成本,老板笑开了花

这套系统上线后,我每周都去汽配厂跟进数据,给大家看真实的效果:

  • 人工成本:从6个目检工减到1个(只负责补料和设备巡检),每月省3万;
  • 漏检率:从5%降到0.1%,客户投诉几乎为零;
  • 产线节拍:从每分钟30件提到40件,产能提升33%。

唯一的小插曲是车间里的电磁干扰,一开始相机偶尔丢帧,后来给相机的GigE线加了两个磁环,问题就解决了。

四、常见问题与优化建议

1. 推理速度不够快怎么办?

如果产线节拍要求更高(比如每分钟60件),可以试试这两个方法:

  • 模型量化:用ONNX Runtime把FP32模型转成INT8,速度能提30%,精度损失不到1%;
  • OpenVINO加速:如果工控机是Intel的CPU,用OpenVINO执行Provider,推理速度能再提20%。

2. 通信丢包怎么解决?

除了我代码里的心跳和重连,还可以加两个机制:

  • 请求确认:PLC收到信号后,写一个寄存器给上位机确认,没收到确认就重发;
  • 报警机制:连续5次通信失败,上位机弹出报警窗口,同时触发PLC的声光报警。

3. 相机触发时机怎么选?

尽量用硬件触发(比如PLC输出一个24V信号给相机),不要用软件定时触发。硬件触发能保证每次拍照都在产品的同一位置,检测精度更高。

最后说两句

工业现场的多设备联动,从来不是拼技术有多高深,而是拼稳定、可靠、易维护。C#上位机+YOLO+PLC这套组合,既能利用YOLO的强大视觉能力,又能发挥PLC的控制稳定性,而且对传统工控人非常友好——不用学Python,不用搞复杂的框架,就能把视觉检测落地到产线。

如果大家在实际项目中遇到类似问题,欢迎一起探讨。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐