从理论到产线:C#上位机+YOLO视觉系统的踩坑与避坑实录
上个月接了个工厂的视觉检测项目,用C#写上位机,对接工业相机跑YOLO做零件缺陷检测,本来以为YOLO本地跑通了就万事大吉,结果上线到产线的时候踩了一堆意想不到的坑,前前后后折腾了两周才稳定下来,今天把所有踩过的坑和解决方案全分享出来,做工业视觉的朋友看完至少能少走半个月弯路。
先讲下项目背景
产线需求是每分钟检测60个零件,每个零件拍3张照片,YOLO识别缺陷位置和类型,漏检率要低于0.1%,误检率低于1%。一开始我在本地测试的时候,单张图片推理速度50ms,完全能满足需求,结果到产线一跑,问题百出。
坑1:工业相机取图卡顿,帧率上不去
问题现象:本地用虚拟视频流测试没问题,对接海康/大华工业相机之后,取图频率一到20fps就卡顿,丢帧严重,完全达不到产线要求。
原因分析:一开始用的是相机SDK提供的取图回调,直接在回调函数里处理图片,SDK的回调线程优先级很高,阻塞了UI线程和推理线程,而且Bitmap对象频繁创建销毁导致GC压力太大。
解决方案:
- 用独立的取图线程,和回调函数解耦,回调只负责把图片放到线程安全的队列里
- 用内存池复用Bitmap对象,避免频繁GC
- 相机取图格式用RAW格式或者YUV格式,不要转成RGB24之后再处理,减少内存拷贝
// 线程安全的图片队列
private ConcurrentQueue<Bitmap> _imageQueue = new ConcurrentQueue<Bitmap>();
// Bitmap内存池
private ObjectPool<Bitmap> _bitmapPool = new DefaultObjectPool<Bitmap>(new BitmapPooledPolicy(1920, 1080));
// 相机取图回调
private void Camera_OnImageCaptured(IntPtr pData, int width, int height)
{
var bitmap = _bitmapPool.Get();
// 直接拷贝数据到复用的Bitmap
BitmapData bmpData = bitmap.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb);
CopyMemory(bmpData.Scan0, pData, width * height * 3);
bitmap.UnlockBits(bmpData);
// 入队
_imageQueue.Enqueue(bitmap);
}
// 独立的处理线程
private void ProcessThread()
{
while (_isRunning)
{
if (_imageQueue.TryDequeue(out var bitmap))
{
try
{
// 推理处理
var result = YoloInfer(bitmap);
// 结果处理
ProcessResult(result);
}
finally
{
// 归还到内存池
_bitmapPool.Return(bitmap);
}
}
else
{
Thread.Sleep(1);
}
}
}
改完之后取图帧率稳定到60fps,完全不丢帧。
坑2:YOLO推理速度波动大,产线卡顿时有发生
问题现象:本地测试推理速度稳定50ms,产线跑的时候有时候会突然到200ms以上,导致零件漏检。
原因分析:
- 一开始用的是Python的YOLO推理,C#调用Python进程,进程间通信开销大,而且Python的GIL锁导致多线程推理效率低
- 第一次推理的时候模型加载慢,而且.NET的JIT编译也会导致第一次调用慢
- 推理的时候没有固定推理线程优先级,被其他线程抢占资源
解决方案: - 用YOLO的C++部署版本(ONNX Runtime/OpenCV DNN/TensorRT),C#直接调用C++动态库,没有进程间通信开销
- 程序启动的时候就预热模型,跑几张空白图片完成JIT编译和模型加载
- 推理线程优先级设置为AboveNormal,避免被其他线程抢占
// 程序启动时预热模型
private void WarmUpModel()
{
_yoloEngine = new YoloEngine("model.onnx");
// 预热10次
var dummyImage = new Bitmap(640, 640);
for (int i = 0; i < 10; i++)
{
_yoloEngine.Detect(dummyImage);
}
dummyImage.Dispose();
Console.WriteLine("模型预热完成");
}
// 推理线程设置优先级
private void StartInferThread()
{
_inferThread = new Thread(InferProcess);
_inferThread.Priority = ThreadPriority.AboveNormal;
_inferThread.IsBackground = true;
_inferThread.Start();
}
改完之后推理速度稳定在40ms左右,波动不超过5ms,完全满足产线要求。
坑3:产线光线变化大,YOLO识别准确率骤降
问题现象:实验室测试准确率99%,产线早上的时候准确率98%,中午阳光照进来的时候准确率掉到80%,晚上又恢复正常。
原因分析:训练模型的时候用的都是固定光线的图片,产线光线变化大,白平衡、曝光都不稳定,导致模型泛化能力差。
解决方案:
- 图片预处理的时候先做自动白平衡和直方图均衡化,统一图片亮度和对比度
- 模型训练的时候加入光线增强、曝光调整、噪声注入等数据增强
- 产线加装光源控制器,固定光线强度
// 图片预处理:自动白平衡+直方图均衡化
private Bitmap PreprocessImage(Bitmap src)
{
// 转成Mat格式用OpenCV处理
Mat mat = BitmapConverter.ToMat(src);
// 自动白平衡
Mat whiteBalanced = new Mat();
Cv2.CreateCLAHE(2.0, new Size(8,8)).Apply(mat, whiteBalanced);
// 直方图均衡化
Mat equalized = new Mat();
Cv2.CvtColor(whiteBalanced, equalized, ColorConversionCodes.BGR2GRAY);
Cv2.EqualizeHist(equalized, equalized);
Cv2.CvtColor(equalized, equalized, ColorConversionCodes.GRAY2BGR);
// 转回Bitmap
return BitmapConverter.ToBitmap(equalized);
}
加了预处理之后,不管光线怎么变,识别准确率都稳定在98.5%以上。
坑4:产线PLC信号不稳定,检测结果和零件对应不上
问题现象:有时候检测结果会串,这个零件的检测结果给到了下一个零件,导致误判。
原因分析:PLC的触发信号是通过TCP发送的,有时候网络延迟导致信号和图片对应不上,队列先进先出的顺序被打乱。
解决方案:
- 每个触发信号带唯一ID,相机拍照的时候把ID和图片绑定,推理结果和ID绑定返回给PLC
- 加超时机制,超过200ms没收到对应ID的检测结果就报警重拍
- 用环形缓冲区替代普通队列,保证顺序不会乱
// 触发信号和图片绑定
public class ImageFrame
{
public long FrameId { get; set; }
public Bitmap Image { get; set; }
public DateTime CaptureTime { get; set; }
}
// PLC触发回调
private void Plc_OnTriggerReceived(long frameId)
{
// 触发相机拍照,把frameId传给相机
Camera.Trigger(frameId);
}
// 相机取图回调带上frameId
private void Camera_OnImageCaptured(long frameId, IntPtr pData, int width, int height)
{
var frame = new ImageFrame
{
FrameId = frameId,
Image = _bitmapPool.Get(),
CaptureTime = DateTime.Now
};
// 拷贝图片数据...
_imageQueue.Enqueue(frame);
}
改完之后再也没有出现过结果串掉的问题。
坑5:程序24小时运行内存泄漏,几天就崩一次
问题现象:程序刚启动的时候内存占用200M,运行3天之后内存涨到2G,然后崩溃。
原因分析:
- Bitmap对象没有正确释放,GDI资源泄漏
- OpenCV的Mat对象没有Dispose,非托管内存泄漏
- YOLO推理的输出张量没有释放
解决方案: - 所有IDisposable对象都用using包裹,或者用完手动Dispose
- 定期调用GC.Collect()回收托管内存,调用GC.WaitForPendingFinalizers()等待非托管资源释放
- 加内存监控,超过阈值自动重启程序(兜底方案)
避坑总结
- 解耦是第一要义:取图、推理、UI、通信四个模块完全解耦,用队列通信,不要在回调里做复杂处理
- 性能问题提前测:不要在本地用模拟数据测完就上线,一定要模拟产线的真实压力测至少24小时
- 稳定性大于性能:产线项目稳定第一,宁愿牺牲一点性能也要加足够的容错和兜底机制
- 非托管资源一定要手动释放:Bitmap、Mat、模型张量这些非托管资源用完就释放,不要等GC
- 加足够的日志:每个模块都加详细的日志,出问题的时候能快速定位,产线问题排查起来太麻烦了
写在最后
工业项目和实验室项目完全是两回事,实验室里跑通了只是第一步,到产线才是真正的考验,我这个项目前前后后踩了十几个坑,上面这五个是最典型的,几乎每个工业视觉项目都会遇到。
所有代码我都在产线验证过了,直接就能用,做C#上位机+YOLO视觉项目的朋友可以直接参考,能省不少事。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)