在这里插入图片描述

干工控视觉这行快11年了,见过太多项目从一开始的信心满满到最后一地鸡毛。上个月刚交付了一个汽车焊装线的缺陷检测项目,合同额120万,前后折腾了8个月,中间差点黄了。今天把整个过程写下来,希望能给正在做类似项目的兄弟们避避坑。

先说说这个项目的背景。客户是国内一家主流的汽车主机厂,他们的焊装车间有一条自动化生产线,主要生产SUV的车身侧围。之前一直靠人工肉眼检测焊点质量,一个工位配3个工人,三班倒,不仅成本高,而且漏检率高达15%以上。经常有带缺陷的车身流到总装车间,造成返工,严重的时候甚至会影响整车下线。

客户的需求很明确:用机器视觉替代人工,实现焊点缺陷的自动检测。要求检测速度不低于30帧/秒,准确率不低于99.5%,漏检率为0,系统要能7x24小时连续运行,并且要和现有的PLC系统无缝对接。

技术选型:为什么最终选了C# + YOLOv12

一开始我们团队内部有很大的分歧。有人主张用Python+OpenCV,说开发快,生态好;有人主张用C++,说性能高,适合工业环境;还有人说直接买商用的视觉软件,比如Halcon或者VisionPro,省事。

我当时力排众议,坚持用C# + YOLOv12的方案。现在回头看,这个选择是对的,但也让我们踩了不少坑。

为什么不用Python?很简单,工业环境下Python的稳定性太差了。内存泄漏、GIL锁的问题,在实验室里可能看不出来,但在7x24小时运行的生产线上,就是定时炸弹。而且Python和PLC、工业相机的通信也不如C#方便。

为什么不用C++?开发效率太低了。我们团队只有3个人,工期只有6个月。用C++写的话,光是界面和通信部分就能把我们累死。而且后期维护也麻烦,客户那边的技术人员大多只会C#,出了问题他们自己都没法排查。

为什么不用商用软件?太贵了。Halcon一个运行时授权就要好几万,我们这个项目有6个检测工位,光授权费就要30多万,客户根本接受不了。而且商用软件的灵活性太差,很多定制化的需求实现起来非常困难。

至于为什么选YOLOv12而不是其他版本,主要是因为YOLOv12在小目标检测上的表现提升非常明显。焊点本身就是很小的目标,直径只有几毫米,而且在复杂的背景下很容易被忽略。我们对比了YOLOv8、YOLOv11和YOLOv12,在同样的数据集上,YOLOv12的准确率比YOLOv11高了3个百分点,推理速度还快了15%。

系统整体架构设计

整个系统分为三层:感知层、处理层和执行层。

工业相机

图像采集卡

C#主程序

PLC

YOLOv12推理引擎

数据库

HMI界面

报警灯

剔除装置

  • 感知层:由6台海康威视的工业相机和对应的图像采集卡组成。每个工位安装一台相机,负责采集不同位置的焊点图像。
  • 处理层:核心是C#编写的主程序和YOLOv12推理引擎。主程序负责图像采集、预处理、调用YOLO模型进行推理、结果分析和逻辑判断。
  • 执行层:包括PLC、报警灯和剔除装置。主程序将检测结果发送给PLC,PLC控制报警灯和剔除装置动作。

这里有个很重要的设计决策:我们没有把YOLO推理放在单独的进程里,而是直接集成到了C#主程序中。很多人会说这样不好,耦合度太高。但在工业项目里,稳定性和实时性才是第一位的。跨进程通信会带来额外的开销和不确定性,一旦通信出问题,整个系统就会瘫痪。

核心实现细节

1. C#调用YOLOv12的正确姿势

这是整个项目最核心的部分,也是我们踩坑最多的地方。

一开始我们用的是ML.NET来加载ONNX模型。结果发现ML.NET的推理速度慢得离谱,一张640x640的图片,推理时间居然要200多毫秒,远远达不到30帧/秒的要求。而且ML.NET对YOLOv12的支持也不好,输出结果的解析非常麻烦。

后来我们换成了ONNX Runtime C# API,速度一下子就上来了。同样的图片,推理时间降到了30毫秒左右。但新的问题又来了:ONNX Runtime的内存泄漏问题非常严重。程序运行几个小时后,内存占用就会从几百MB涨到几个GB,最后直接崩溃。

我们花了整整两个星期才解决这个问题。原来ONNX Runtime的InferenceSession对象是非常昂贵的,不能每次推理都创建一个新的。必须在程序启动时创建一个单例的InferenceSession,然后重复使用。而且每次推理后,必须手动释放Tensor的内存,不能依赖GC。

// 错误的写法:每次推理都创建新的InferenceSession
public DetectionResult Detect(byte[] imageData)
{
    var session = new InferenceSession("yolov12.onnx");
    // ... 推理代码 ...
}

// 正确的写法:使用单例模式
public sealed class YoloInference
{
    private static readonly Lazy<YoloInference> _instance = new Lazy<YoloInference>(() => new YoloInference());
    private readonly InferenceSession _session;

    private YoloInference()
    {
        var options = new SessionOptions
        {
            GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_ALL,
            ExecutionMode = ExecutionMode.ORT_PARALLEL
        };
        options.AppendExecutionProvider_CUDA(0); // 使用GPU加速
        _session = new InferenceSession("yolov12.onnx", options);
    }

    public static YoloInference Instance => _instance.Value;

    public DetectionResult Detect(Mat image)
    {
        // ... 图像预处理 ...
        
        using (var inputTensor = DenseTensor<float>.FromArray(inputData, new[] { 1, 3, 640, 640 }))
        using (var outputs = _session.Run(new List<NamedOnnxValue> { NamedOnnxValue.CreateFromTensor("images", inputTensor) }))
        {
            var outputTensor = outputs.First().AsTensor<float>();
            // ... 结果解析 ...
            
            // 手动释放内存
            outputTensor.Dispose();
        }
        
        return result;
    }
}

2. 图像预处理的坑

工业环境下的图像质量和实验室里完全不是一个概念。焊装车间里光线复杂,有强光反射,有灰尘,还有焊渣飞溅。如果直接把原始图像喂给YOLO模型,准确率会低得吓人。

我们尝试了很多预处理方法,最后总结出了一套效果最好的流程:

原始图像

灰度化

高斯模糊去噪

自适应直方图均衡化

边缘检测

ROI提取

输入YOLO模型

这里有个小技巧:不要对整张图片进行预处理,只对焊点所在的ROI区域进行处理。这样不仅能提高处理速度,还能减少背景干扰。我们的ROI区域是通过PLC发送的车身位置信息动态确定的,这样即使车身有轻微的偏移,也能准确找到焊点。

3. 多线程并发处理

6个相机同时采集图像,每个相机每秒30帧,这意味着我们的程序每秒要处理180张图片。单线程肯定是处理不过来的,必须用多线程。

但多线程在工业项目里是把双刃剑。用不好的话,会出现各种奇怪的问题,比如死锁、竞态条件、数据丢失等。

我们的解决方案是使用生产者-消费者模式。每个相机对应一个生产者线程,负责采集图像并放入队列。然后有一个线程池,里面有多个消费者线程,负责从队列中取出图像进行处理。

private readonly BlockingCollection<ImageFrame> _imageQueue = new BlockingCollection<ImageFrame>(new ConcurrentQueue<ImageFrame>(), 100);

// 生产者线程
private void CameraCaptureThread(Camera camera)
{
    while (!_isStopped)
    {
        var frame = camera.Capture();
        if (frame != null)
        {
            // 如果队列满了,丢弃最旧的帧
            if (_imageQueue.Count >= 100)
            {
                _imageQueue.Take();
            }
            _imageQueue.Add(frame);
        }
    }
}

// 消费者线程
private void ProcessImageThread()
{
    while (!_isStopped)
    {
        if (_imageQueue.TryTake(out var frame, 100))
        {
            try
            {
                var result = YoloInference.Instance.Detect(frame.Image);
                ProcessResult(frame, result);
            }
            catch (Exception ex)
            {
                // 记录日志,不要让异常导致线程崩溃
                _logger.Error($"图像处理失败: {ex.Message}", ex);
            }
            finally
            {
                frame.Image.Dispose();
            }
        }
    }
}

这里一定要注意:队列的大小必须有限制。如果不限制队列大小,一旦处理速度跟不上采集速度,内存就会被迅速耗尽。我们设置的队列大小是100,超过这个大小就丢弃最旧的帧。在工业项目里,偶尔丢几帧是可以接受的,总比系统崩溃强。

那些差点让项目黄了的坑

1. 光线变化导致准确率骤降

这是我们遇到的第一个大问题。在实验室里调试的时候,准确率能达到99.8%。但一到现场,准确率直接掉到了80%以下。

原因很简单:车间里的光线是变化的。白天有阳光照进来,晚上只有灯光。而且不同时间段的光线强度也不一样。我们的模型是在实验室的固定光线下训练的,到了现场自然就不行了。

解决方案:

  • 安装了专门的环形光源,并且用遮光罩把相机和光源罩起来,隔绝外界光线的干扰
  • 采集了不同时间段、不同光线条件下的图像,扩充了数据集
  • 在预处理阶段增加了自动亮度和对比度调整

2. 相机抖动导致的误检

焊装车间里有很多机器人和传送带,振动非常大。相机安装在支架上,时间长了就会出现轻微的抖动。这会导致图像模糊,进而产生误检。

一开始我们想通过机械方式解决,比如加固支架,使用减震垫。但效果都不理想。后来我们想到了一个软件层面的解决方案:在图像采集后增加一个图像锐化的步骤,并且使用模板匹配来校正图像的偏移。

3. 模型过拟合

我们的模型在测试集上表现很好,但在实际生产中却经常出现误检。后来发现是数据集的问题。我们一开始只采集了几百张有缺陷的焊点图像,而且大部分都是比较明显的缺陷。但实际生产中,很多缺陷都是非常细微的,比如虚焊、漏焊、焊偏等。

解决方案:

  • 花了整整一个月的时间,在现场采集了超过10000张图像
  • 对数据集进行了增强,包括旋转、缩放、翻转、加噪声等
  • 使用了早停策略,防止模型过拟合

4. 7x24小时运行的稳定性问题

这是工业项目和实验室项目最大的区别。实验室里的程序跑几个小时没问题,但在生产线上要连续跑几个月甚至几年。

我们遇到的稳定性问题包括:

  • 内存泄漏:前面已经说过了,通过单例模式和手动释放内存解决
  • 相机断连:工业相机经常会因为各种原因断连,我们实现了自动重连机制
  • 程序崩溃:增加了看门狗程序,一旦主程序崩溃,自动重启
  • 硬盘满了:自动删除7天前的日志和图像文件

性能优化成果

经过一系列的优化,我们的系统最终达到了以下指标:

指标 要求 实际达到
检测速度 ≥30帧/秒 45帧/秒
准确率 ≥99.5% 99.92%
漏检率 0 0
平均无故障时间 ≥720小时 1500小时

客户非常满意,不仅按时支付了尾款,还和我们签订了后续的维护合同,并且把另外两条生产线的视觉检测项目也交给了我们。

总结和经验教训

这次项目让我深刻体会到,工业视觉项目和普通的软件项目完全不一样。普通的软件项目只要功能实现了就差不多了,但工业视觉项目,功能实现只是第一步,后面还有无数的坑等着你去踩。

给正在做或者准备做工业视觉项目的兄弟们几点建议:

  1. 不要迷信算法:算法只是整个系统的一部分,很多时候,工程实现比算法更重要。一个好的工程实现,能让一个一般的算法发挥出很好的效果;反之,一个差的工程实现,再好的算法也没用。

  2. 一定要去现场:不要在实验室里闭门造车。工业环境的复杂性是你在实验室里想象不到的。多去现场看看,多和一线工人交流,你会发现很多问题。

  3. 稳定性大于一切:在工业项目里,稳定性是第一位的。哪怕你的系统准确率再高,速度再快,如果经常崩溃,那就是一个失败的系统。

  4. 做好数据备份:一定要定期备份模型和数据。万一系统出了问题,能快速恢复。

  5. 不要过度承诺:客户的需求永远是无止境的。在签合同的时候,一定要把需求写清楚,不要为了拿项目而过度承诺。

最后说一句,干工控这行真的不容易。经常要熬夜加班,经常要去现场吃灰。但每当看到自己做的系统在生产线上稳定运行,替代了工人的重复劳动,那种成就感是无法用语言形容的。

Logo

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

更多推荐