写在前面
很多算法工程师都有个误区:觉得模型mAP刷到99%项目就成功了。
直到去年在一家汽车零部件厂驻场三个月,被产线老师傅指着鼻子骂“你这电脑一卡,我这一整批货都得报废”,我才明白:在工业界,稳定性 > 准确性,实时性 > 先进性。

这篇文章不聊复杂的Transformer架构,也不堆砌数学公式。我想复盘一个真实落地的项目:如何用C#把YOLOv8塞进工控机,通过Modbus TCP跟十年前的西门子S7-1200 PLC“对话”,最终实现毫秒级缺陷剔除。

如果你正卡在“算法能跑,但没法控制设备”这一步,或者想从纯算法转型做全栈落地,希望这篇踩坑实录能帮你省下两周的调试时间。


一、为什么是 C#?为什么不是 Python?

在项目选型会上,我也曾坚持用Python + Flask + WebSocket的方案。理由很充分:生态好、库多、开发快。
但现场电气主管只问了我三个问题:

  1. “工控机重启后,你的Python环境能不能自动起来?”
  2. “如果相机丢帧了,你的GIL锁会不会卡死整个线程?”
  3. “我们要跟现有的SCADA系统集成,你们谁写过C#的DLL调用?”

那一刻我意识到,工业现场的“通用语言”是C# (.NET)

  • 多线程优势:.NET的多线程模型在处理“采集、推理、通信、UI”四路并发时,比Python的GIL友好太多。
  • 硬件亲和性:海康、巴斯勒的相机SDK,西门子、三菱的PLC库,原生支持都是.NET。
  • 部署简单:打包成一个exe,丢进启动项,不用配conda环境,不用管pip依赖冲突。

所以,架构定调:C# WPF (UI+逻辑) + ONNX Runtime (推理) + NModbus (通信)


二、核心难点:如何打破“算法”与“控制”的墙?

1. 时序同步的陷阱

最容易翻车的地方在于时序
新手常做的逻辑是:
相机拍照 -> YOLO推理(50ms) -> 发现缺陷 -> 发送Modbus信号 -> PLC动作

致命问题:传送带速度是0.5米/秒。当你花50ms推理完再发信号,产品已经向前跑了2.5厘米。如果剔除机构在相机后方30厘米处,这2.5厘米的误差可能导致气缸推空,或者把良品当次品打了。

工业级解法
视觉只负责“判”,PLC负责“断”和“行”。

  • 视觉上位机:检测到缺陷,立刻记录当前的编码器位置时间戳,并瞬间给PLC发一个“有缺陷”的标志位(Flag)。
  • PLC:收到Flag后,不立即动作,而是持续读取产线编码器的数值。当 当前编码器值 - 拍照时编码器值 == 预设偏移量 时,才驱动电磁阀。

这样,无论上位机推理慢了10ms还是100ms,只要在产品到达剔除位之前把信号送到,PLC都能精准执行。把时间敏感的逻辑交给实时性更强的PLC,是工业控制的铁律。

2. 通信协议的“握手”设计

直接用Modbus写一个寄存器触发?太危险了。网络抖动一下,信号丢了怎么办?PLC没读到怎么办?

我们设计了一套**“三态握手”机制**(寄存器地址规划见文末附录):

  1. Request (上位机写):置位 Trigger_Flag = 1,同时写入 Defect_Type
  2. Acknowledge (PLC读):PLC扫描到 Trigger_Flag 为1,将其内部状态机置为“待处理”,并主动将 Trigger_Flag 清零(告诉上位机:我收到了,你可以发下一个了)。
  3. Busy Check (上位机读):上位机在发新信号前,先读PLC的 System_Status 寄存器。如果PLC处于“忙”或“故障”状态,暂停发送,防止指令堆积。

这套逻辑虽然多写了十几行代码,但在现场连续跑72小时压力测试时,一次信号丢失都没发生。


三、实战代码:剥离框架看本质

为了让大家能直接复用,我剔除了项目中繁琐的UI绑定,只保留最核心的推理引擎通信服务

1. 零拷贝的YOLO推理封装

很多人用OpenCvSharp处理完图片,转成byte[]再喂给ONNX,这在高频检测中是性能杀手。
我们采用内存指针直传,避免GC(垃圾回收)造成的卡顿。

// 核心片段:YoloEngine.cs
public class YoloEngine : IDisposable
{
    private readonly InferenceSession _session;
    private readonly float[] _inputBuffer; 
    private readonly GCHandle _bufferHandle; // 关键:锁定内存,防止GC移动

    public YoloEngine(string modelPath)
    {
        var options = new SessionOptions();
        options.InterOpNumThreads = 1;
        options.IntraOpNumThreads = Environment.ProcessorCount;
        // 如果有显卡,这里追加 CUDA Provider
        // options.AppendExecutionProvider_CUDA(0); 
        
        _session = new InferenceSession(modelPath, options);

        // 预分配640x640x3的缓冲区,全程复用
        _inputBuffer = new float[3 * 640 * 640];
        _bufferHandle = GCHandle.Alloc(_inputBuffer, GCHandleType.Pinned);
    }

    public List<Detection> Detect(Mat frame)
    {
        // 1. 预处理 (Resize + Normalize + HWC2CHW) 直接写入 _inputBuffer
        Preprocess(frame, _inputBuffer); 

        // 2. 创建Tensor,直接使用 pinned 内存地址,无拷贝
        var inputShape = new long[] { 1, 3, 640, 640 };
        using var inputOrtValue = OrtValue.CreateTensorValueFromMemory(
            _bufferHandle.AddrOfPinnedObject(), 
            inputShape, 
            TensorElementType.Float
        );

        // 3. 推理
        var inputs = new NamedOnnxValue[] { NamedOnnxValue.CreateFromTensor("images", inputOrtValue) };
        using var results = _session.Run(inputs);
        
        // 4. 后处理 (NMS...)
        return Postprocess(results);
    }

    public void Dispose()
    {
        _bufferHandle.Free(); // 必须释放
        _session?.Dispose();
    }
}

经验之谈:在产线全速运行时,GC的一次Full Collection可能导致界面卡顿200ms,对于高速产线这就是致命的。预分配内存+Pinned Handle是必选项。

2. 健壮的Modbus通信服务

不要相信网络永远稳定。我们需要一个带心跳检测自动重连的后台服务。

// 核心片段:PlcService.cs
public class PlcService
{
    private IModbusMaster _master;
    private CancellationTokenSource _cts;
    private bool _isConnected = false;

    // 启动心跳守护线程
    public void Start(string ip)
    {
        _cts = new CancellationTokenSource();
        Task.Run(() => HeartbeatLoop(ip), _cts.Token);
    }

    private async Task HeartbeatLoop(string ip)
    {
        int heartbeatVal = 0;
        while (!_cts.IsCancellationRequested)
        {
            try
            {
                if (!_isConnected) await ConnectAsync(ip);

                // 每秒写入心跳寄存器,PLC侧监控这个值,不变则报警
                await _master.WriteSingleRegisterAsync(1, 40005, (ushort)heartbeatVal++);
                
                // 顺便读取PLC状态,判断是否急停
                var status = await _master.ReadHoldingRegistersAsync(1, 40001, 1);
                HandlePlcStatus(status[0]);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"[Warn] PLC通信中断: {ex.Message}");
                _isConnected = false;
                // 触发UI报警,甚至联动声光塔灯
                OnPlcDisconnected?.Invoke(); 
            }
            
            await Task.Delay(1000);
        }
    }

    // 发送缺陷信号的原子操作
    public async Task SendDefectSignal(int type)
    {
        if (!_isConnected) return;
        
        // 一次性写入类型和触发标志,减少TCP交互次数
        ushort[] data = { (ushort)type, 1 }; 
        await _master.WriteMultipleRegistersAsync(1, 40003, data);
    }
}

四、那些只有现场才知道的“坑”

1. 光照是玄学,也是科学

在实验室调好的模型,到了车间早上能跑,下午就废。因为车间顶棚的天窗会随着太阳角度变化,导致背景亮度剧烈波动。
解决方案

  • 硬件上:必须上光源控制器,用频闪光源配合相机硬触发,彻底屏蔽环境光。
  • 算法上:训练集必须包含不同时间段、不同光照条件下的图片。如果条件允许,加入简单的传统图像处理(如直方图均衡化)作为预处理前置步骤。

2. “误杀”比“漏杀”更可怕?

理论上漏杀(Bad Pass)是质量事故,误杀(False Reject)是成本浪费。
但在实际博弈中,如果误杀率超过3%,产线工人会因为频繁停机复检而直接关掉检测系统。
策略

  • 设置双阈值。置信度 > 0.8 直接剔除;0.5 ~ 0.8 之间的,标记为“可疑”,推送到人工复检台,不直接停机。
  • 引入时间滤波。如果一个缺陷只在单帧出现,相邻帧没有,大概率是噪点或反光,忽略它。只有连续2帧以上检测到,才确认为真缺陷。

3. 日志就是黑匣子

系统出问题时,电气说是软件问题,软件说是相机问题,相机说是光线问题。
一定要做详尽的日志

  • 记录每一张图的拍摄时间、推理耗时、结果。
  • 记录每一次Modbus收发的具体报文。
  • 关键点:当检测到缺陷时,自动保存原图和标注图到本地磁盘(按日期/班次分类)。这是后续优化模型和扯皮的最有力证据。

五、结语:从“做题家”到“工程师”

在学校里,我们追求模型的SOTA(State of the Art),追求mAP提升0.1个点。
在工厂里,客户只关心:能不能24小时不宕机?坏了能不能一键重启?误报会不会影响产量?

这套 C#+YOLO+PLC 的方案,虽然没有用到最新的Vision Transformer,也没有复杂的分布式架构,但它胜在简单、可控、易维护。它打通了AI算法与物理世界的“最后一公里”,让代码真正变成了生产力。

如果你也想投身工业AI,建议先从理解业务开始,再去钻研算法。毕竟,能解决实际问题的代码,才是好代码。


附录:寄存器映射表(参考)

地址 名称 读写 说明
40001 System_Status R/W Bit0:运行, Bit1:故障, Bit2:忙
40002 Control_Cmd R/W Bit0:复位, Bit1:启动, Bit2:急停
40003 Defect_Type R/W 缺陷类型代码 (1:划痕, 2:裂纹…)
40004 Trigger_Flag R/W 1:触发剔除 (PLC读后清零)
40005 Heartbeat R/W 心跳计数,每秒+1

(注:具体地址需根据现场PLC型号调整,西门子S7-1200需注意V区偏移)

Logo

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

更多推荐