做视觉检测项目这些年,我接过不少需要从零搭建的案子,印象最深的是给一家汽车零部件厂做气门弹簧的外观检测线。甲方原计划用进口整机,一问报价直接劝退,后来找到我,需求很明确:C#上位机 + 工业相机,把人工质检换成自动化检测。从硬件选型到软件落地,前后两个月搞定,节拍从人工的5秒/件压到1.5秒/件,漏检率反而降了一个数量级。今天我就把这套系统的完整实现思路拆开讲,从相机选型、SDK接入、视觉算法到PLC联动,再到那些只有踩过坑才懂的血泪教训,全部倒出来。不管你是在校学生准备找上位机开发的工作,还是工程师打算给自家产线做视觉升级,这篇文章都能给你一条清晰可复现的路径。
1. 方案选型与整体架构设计
很多人一上来就急着写代码,结果代码写了一堆,连相机图像都取不出来,或者取出来了不知道往哪儿送。做视觉检测系统,第一步永远是架构设计,先想清楚数据从哪来、到哪里去、谁来控制谁。
1.1 为什么用C#做视觉检测上位机
选C#而不是C++、Python或LabVIEW,理由其实挺实在的。C++做视觉底层算法确实快,但界面开发效率太低,开发周期动不动翻倍,维护成本也高;LabVIEW上手确实快,但视觉算法库相对封闭,和第三方硬件对接时经常要写一堆DLL来桥接,遇到底层的图像处理需求就捉襟见肘了。Python虽然写算法方便,但打包部署到工控机上容易出环境问题,性能也确实拼不过原生编译。C#站在了平衡点上:WinForm/WPF开发界面效率高,Pylon、MVS这些相机SDK都提供了官方C#接口,OpenCvSharp又能无缝调用海量视觉算法,加上面向对象的特性让整个系统逻辑清晰,维护成本可控。
而且C#的老牌框架.NET Framework 4.7.2或者.NET 6/8都能在工控机上稳定运行,只要不是太老的操作系统,部署基本不折腾。我接触过的上百个项目中,至少七成上位机用的都是C#,这本身就是一种行业惯性,后续招人、维护、扩展都有保障。
1.2 视觉检测系统的四大功能模块
一个完整的视觉检测系统,无论检测对象是什么,拆开看都是四块:
图像采集模块负责通过SDK从相机取流,做触发控制(硬触发或软触发)、图像缓存和格式转换。这层的关键是稳定性和实时性,绝对不能丢帧漏帧。
视觉算法模块是核心,用OpenCvSharp或Halcon对图像做处理,包括预处理、定位、测量、缺陷检测等。算法跑得准不准、快不快,直接决定系统能不能用。
运动控制/PLC通信模块负责和产线交互,接收传感器的触发信号,给PLC反馈检测结果,控制气缸分拣不良品。这层讲究的是时序严谨,信号延迟、误触发这些都必须处理干净。
人机交互模块就是操作界面,显示当前图像、检测数据、报警信息,提供参数配置入口,让产线工人能方便地调整检测参数、查看历史记录。
1.3 系统通信拓扑与数据流设计
我建议的数据流是单向闭环:传感器或PLC发出拍照信号 → 相机硬触发采集 → 图像进入处理队列 → 算法模块计算 → 结果回传PLC → PLC控制执行机构的同时,上位机更新界面和数据存储。
这里有个容易忽略的点:图像处理和UI显示必须是两个独立线程。相机SDK的回调线程拿到图像后,直接丢进并发队列,算法线程从队列取图处理,处理完的结果通过事件或委托传给UI线程刷新。如果直接在回调线程里做算法再更新界面,界面卡顿不说,相机缓冲区还会被堵住,轻则拖慢节拍,重则直接掉线。
2. 工业相机接入与图像采集
图像采集是整个系统的地基,地基建不好,算法再牛也白搭。接入这块涉及相机选型、SDK选择、触发方式、多相机管理几个环节,每个都有讲究。
2.1 工业相机选型关键参数
相机选型是一个项目前期最容易踩坑的环节,选错了轻则检测精度不够,重则整个节拍都跟不上。我一般按下面几个维度来做选择题:
分辨率要根据视野范围和检测精度来定。比如要检测的工件尺寸是10mm x 10mm,需要识别的缺陷最小是0.05mm,那么长方向需要的像素数至少是10 / 0.05 = 200像素。再考虑到算法稳定性、边缘模糊、光照波动等因素,实际要留3~5倍余量,也就是600~1000像素起步,所以我通常直接选500万像素以上的相机,省得后面因为分辨率不够返工。
帧率/曝光时间要卡着生产节拍来算。产线节拍是2秒一件,那你1秒能拍5张以上的相机就够了;但如果节拍是0.5秒一件,普通USB3.0相机就很吃力,得考虑带硬触发的GigE相机甚至Camera Link高速相机。另外曝光时间决定了运动模糊,工件在拍照时是静止的还好,如果运动中拍照,曝光时间必须压到工件移动一个像素以内的时间。
接口类型上,USB3.0适合短距离、高带宽、不用长线的场景,GigE适合20米甚至更长的线缆场景,走普通网线就行。现场电磁环境复杂的话,还得选带光耦隔离IO的相机,避免触发信号被干扰误触发。
品牌选择上,Basler性能稳定、文档齐全,价格也相对高一些;海康威视、大华这些国产品牌性价比高,SDK也比较完善。其实选哪家不重要,关键看SDK的C#接口好不好用,以及后期技术支持响应快不快。我的建议是同一个项目尽量统一品牌,多相机混用会带来SDK版本管理的麻烦。
2.2 三种主流接入方式对比
C#接入工业相机主要有三条路:
官方SDK直连是最可靠的方式,Basler的Pylon SDK、海康的MVS SDK、大华的DLL都提供C#接口,取流稳定、功能全面、硬触发支持好。缺点是换了相机品牌就要换SDK,代码不能直接复用。
DirectShow/UVC通用接口适合USB摄像头、UVC协议的工业相机,用DirectShow或MediaFoundation就能枚举和取流,不需要安装私有SDK。好处是通用性强,坏处是难以使用工业相机的硬触发、异步复位等高级功能,且帧率稳定性受系统调度影响比较大,不适合高精度场景。
第三方库如OpenCvSharp的VideoCapture是快捷通道,VideoCapture类可以直接打开相机索引,代码量非常少,适合快速做原型验证。但坑也明显:底层很多相机品牌的私有协议支持不佳,GigE相机的包丢得厉害,硬触发基本用不了,生产环境慎用。
我的经验是:正式项目一律用官方SDK,原型验证才考虑OpenCvSharp的VideoCapture。尤其如果你还需要Halcon做算法,直接用Halcon的接口去连相机有时候也能省事,但Halcon的采集和SDK取流在性能上还是有差距的,量产系统建议用相机SDK取原始图,再传递给算法模块。
2.3 Basler相机SDK接入实战
Basler是工业相机里的老大哥,文档全、案例多,用它来示范最合适。下面这段是Pylon SDK连续采集的最简模型:
// 引入Pylon SDK命名空间 using Basler.Pylon; // 相机实例化与连接 Camera camera = new Camera(); camera.CameraOpened += Camera_CameraOpened; camera.ConnectionLost += Camera_ConnectionLost; // 注意:掉线检测必须注册 camera.Open(); // 设置相机参数:关闭自动曝光,固定曝光时间,防止产线环境光波动影响 camera.Parameters[PLCamera.ExposureAuto].TrySetValue(PLCamera.ExposureAuto.Off); camera.Parameters[PLCamera.ExposureTimeAbs].TrySetValue(2000.0); // 曝光时间2000us,根据实际运动速度标定 camera.Parameters[PLCamera.TriggerMode].TrySetValue(PLCamera.TriggerMode.Off); // 先关触发,连续采集测试 // 注册图像回调 camera.StreamGrabber.ImageGrabbed += StreamGrabber_ImageGrabbed; camera.StreamGrabber.Start(); // 回调函数中使用独立的图像处理线程,然后自动任务继续 private static void StreamGrabber_ImageGrabbed(object sender, ImageGrabbedEventArgs e) { try { IGrabResult grabResult = e.GrabResult; if (grabResult.GrabSucceeded) { // 此处重点:把图像数据从SDK缓冲区拷贝出来,而不是直接引用 // 因为回调线程返回后SDK会复用缓冲区,直接引用指针会导致数据被覆盖 byte[] buffer = grabResult.PixelDataAsByteArray; int width = grabResult.Width; int height = grabResult.Height; // 将原始数据转换成OpenCV可使用的Mat对象 using (Mat src = new Mat(height, width, MatType.CV_8UC3, buffer)) { // 放入处理队列,算法线程异步处理 Task.Run(() => ProcessImage(src.Clone())); } } } catch (Exception ex) { // 记录日志,但绝对不能在这里弹窗报错,UI线程跨线程弹窗很危险 } }这里有几个细节必须强调。PixelDataAsByteArray拷贝出来后,要立刻把它封装成Mat,然后必须Clone()一份再给算法线程。原理在于SDK的内部缓冲池会复用内存,你直接传引用的话,下一帧到来时这帧数据就被覆盖了,结果画面会出现鬼影一样的错乱。
硬触发模式才是工业检测的标配,改法很简单:
// 软件触发: camera.Parameters[PLCamera.TriggerMode].TrySetValue(PLCamera.TriggerMode.On); camera.Parameters[PLCamera.TriggerSource].TrySetValue(PLCamera.TriggerSource.SoftWare); camera.ExecuteSoftwareTrigger(); // 硬件触发(PLC或传感器直接给IO信号): camera.Parameters[PLCamera.TriggerMode].TrySetValue(PLCamera.TriggerMode.On); camera.Parameters[PLCamera.TriggerSource].TrySetValue(PLCamera.TriggerSource.Line1);选择硬触发还是软触发要看节拍要求。硬触发就是传感器直接给相机的IO口脉冲,不经过上位机,延迟是微秒级的,相位精准;软触发则是上位机收到PLC指令后执行ExecuteSoftwareTrigger(),这样多了一层进程调度,延迟至少几毫秒,而且不稳定的情况下帧和触发信号容易错位。所以产线上带运动控制、需要精确位置拍照的,一律硬触发。
2.4 多相机同时采集的区分与管理
一个工位多相机很常见,比如测一个工件的三个面,或者A面B面分工。多相机管理最容易翻车的是摄像头索引混乱和回调交叉。
如果用DirectShow/UVC这种方式,你需要按DevicePath来区分相机,而不是凭索引号:
// 使用DirectShow枚举设备,筛选出想要的相机 var devices = new FilterInfoCollection(FilterCategory.VideoInputDevice); foreach (FilterInfo device in devices) { // device.Name 是设备名称,可以通过名称或DevicePath来匹配 // 例如包含"USB Camera"、"Basler"等关键词来区分 }用官方SDK时,Basler可以通过相机IP或MAC地址来唯一绑定,海康/大华也有类似的IP配置功能。我的建议是在系统配置界面里加一个相机编号与IP/序列号的映射表,每次启动时校验,发现不匹配直接报警,防止现场工人误插线缆导致相机序号错位。
多相机还有一个坑是多个回调线程同时访问算法模块。如果你让两个相机的回调都往同一个队列里塞图像,队列就变成了竞态条件。建议每个相机配一个独立队列和独立算法线程,或者用带有统一锁的线程安全队列。我用的是BlockingCollection<T>,天然支持多生产者单消费者模式,效果很稳。
3. 视觉算法检测模块实现
采集到图了,接下来就是重头戏——算法处理。视觉检测系统里最常用的三类需求是:定位引导(告诉机器人/运动平台目标在哪)、尺寸测量(量化到物理单位)、缺陷检测(找脏污、划痕、缺料、异物等)。我分别讲下实现思路。
3.1 OpenCvSharp集成与图像处理流程
OpenCvSharp是C#下封装的OpenCV,NuGet直接安装OpenCvSharp4和OpenCvSharp4.runtime.win就能用。处理流程万变不离其宗,都是三步走:预处理 → 提取目标 → 输出结果。
预处理是为了把图像质量提上来。工业现场光照很难绝对匀称,再加上工件本身的反光因素,裸图直接做阈值分割往往一脸糊。
using OpenCvSharp; public Mat Preprocess(Mat src) { Mat gray = new Mat(); Cv2.CvtColor(src, gray, ColorConversionCodes.BGR2GRAY); // 高斯滤波降噪,去除传感器噪声。核大小选5x5,太大的话边缘信息会丢,太小降噪效果差 Mat blur = new Mat(); Cv2.GaussianBlur(gray, blur, new Size(5, 5), 0); // 增强对比度的方式是自适应直方图均衡化(CLAHE),比全局均衡更能保留细节 CLAHE clahe = Cv2.CreateCLAHE(2.0, new Size(8, 8)); Mat enhanced = clahe.Apply(blur); return enhanced; }这里有个关键认知:预处理参数不是随便拍脑袋定的,而是要根据具体打光情况和工件材质来调节。比如高反光的金属件,你对着工件表面打直射光会产生高光溢出,预处理前最好先用带偏振片的光源,再从算法层面抑制高光区域;而漫反射的白色塑料件就简单很多,二值化都能分割得很好。
3.2 边缘检测与工件定位
最常见的定位需求是找工件的中心点和旋转角度,供机器人或运动平台对准。方法很多,我用得最顺手的是:阈值分割 → 轮廓提取 → 最小外接矩形 / 霍夫直线。
public void LocateWorkpiece(Mat enhanced, out Point2f center, out double angle) { // 1. 阈值分割,把工件从背景里提取出来。阈值选择可以先直方图看双峰再定 Mat binary = new Mat(); Cv2.Threshold(enhanced, binary, 120, 255, ThresholdTypes.BinaryInv); // 2. 形态学闭运算,把断开的边缘连接起来,去掉内部小孔洞 Mat kernel = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(15, 15)); Cv2.MorphologyEx(binary, binary, MorphTypes.Close, kernel); // 3. 提取轮廓,只取面积最大的那个,通常是工件主体 Point[][] contours; HierarchyIndex[] hierarchy; Cv2.FindContours(binary, out contours, out hierarchy, RetrievalModes.External, ContourApproximationModes.ApproxNone); double maxArea = 0; Point[] bestContour = null; foreach (Point[] contour in contours) { double area = Cv2.ContourArea(contour); if (area > maxArea) { maxArea = area; bestContour = contour; } } // 4. 对最大轮廓求最小外接矩形,矩形中心即工件中心,矩形角度即工件旋转角 RotatedRect rect = Cv2.MinAreaRect(bestContour); center = rect.Center; angle = rect.Angle; // 注意:OpenCV返回的角度范围是0~90度,可能不满足你的坐标系定义,需要换算 }这个流程看似简单,实际调试中坑点不少。FindContours默认检索模式是External,如果你的工件表面有种复杂图案,就会被当成多个外部轮廓算错中心;这种情况下需要换成RetrievalModes.CCOMP或用更大的形态学核。另外最小外接矩形的角度存在90度歧义,比如矩形边和水平夹角是30度或-60度,同一个矩形返回的角度可能在不同旋转状态下跳变,所以角度换算时要根据矩形边长关系做判断,否则角度结果会时不时跳一下,很影响产线稳定性。
3.3 尺寸测量的标定与像素-物理换算
测尺寸的前提是像素标定,也就是搞明白一个像素等于多少毫米。最土但最实用方法:放一个已知尺寸的标定块或工件在视野里,用MinAreaRect拿到标定块的边长像素数,然后物理长度除以像素数就是标定系数。
// 标定系数示例:已知标定块长度为10.000mm,测出来像素长度是1000像素 double pixelsPerMm = 1000.0 / 10.0; // 得到100像素/mm // 那么被测物体长度为 500像素 / 100 = 5.000mm这里我要提醒一个隐藏问题:镜头畸变。如果你用的镜头视野很大或者边缘要求精度高,图像边缘的标定系数和中间不一样,直接用一个统一系数测量,偏差可能达到0.1mm以上。这种情况下的解决办法有两个:一个是做相机标定用标定板校正畸变,OpenCvSharp里有现成的CalibrateCamera函数可以做;另一个是只使用视野中心区域,留出畸变边缘不参与测量。如果预算允许,远心镜头是最省心的选择,它天生没有近大远小的问题,畸变极小。
3.4 缺陷检测的两种路线
缺陷检测比定位测量要复杂得多,缺点是需要对算法模型有比较强的理解。我把它分成两种路线:
传统图像处理路线适合特征较固定的缺陷。比如表面划痕是高亮的细长条特征,检测策略就是提取灰度比周边高的区域,用Canny边缘提取再加霍夫直线检测看有没有交叉的强直线。又如表面脏污是灰度异常区域,可以做局部二值化(自适应阈值),偏离背景灰度的区域即为可疑缺陷。这套方法速度快、可解释性强、调参可预期,绝大多数现有产线上还是传统方法的天下。
深度学习路线适合缺陷形态复杂、难以用固定规则描述的场景。模型选择难度很高,我自己用量最大的是用**语义分割网络(U-Net的变种)**做像素级缺陷定位,训练数据需要几个月甚至半年的缺陷样本积累,数量至少千张以上。C#下的推理可以通过ONNX Runtime来跑,这样就不用跨C++/Python进程,速度也还过得去。不过我还是要真诚地建议:能用传统方法解决的,别一上来就上深度学习,否则数据、训练周期、现场调优这些成本都能拖死项目。
4. 与PLC及执行机构联动
视觉检测系统不是孤立运行的,它必须嵌进产线的自动化体系。上位机要和PLC通信,接收触发,回传结果;要与光源、气缸、电机这些执行机构协同。这块做不好,视觉做得再准也没用,因为产线根本不听你的。
4.1 工位信号交互与IO控制
IO交互分两大类。传感器直接接相机触发是我最推荐的方式,帧同步精度微秒级,不受上位机调度影响。此时上位机的角色变成被动接收,相机处理完成后通过回调或网络通知上位机。传感器接PLC,PLC再触发上位机是比较常见的产线法兰布局,传感器信号进PLC,PLC发指令给上位机(通过网口或串口),上位机软触发相机拍照。这个链路多了一道环节,时序要拿捏好,否则拍照时机和工件位置对不上。
常见的硬接线方案包括:工件到位传感器 → 同时触发相机和光源;相机拍照完成 → 通过输出IO通知PLC“已拍照”;PLC根据视觉结果控制气缸或回流线。接线上有个经验:相机IO线一定要选带屏蔽的双绞线,且和动力线分槽走线,否则变频器一启动,图像或者触发电平就会被干扰得乱七八糟。
4.2 C#连接PLC的常见做法:Modbus TCP与OPC UA
C#上位机和PLC通信是必修课,不同的PLC有不同的接口协议。我接触的产线里,Modbus TCP和OPC UA是两条主要路线。
Modbus TCP用NModbus库或自写协议栈就能实现。西门子S7-1200/1500、三菱Q/L系列、国产多数PLC都支持Modbus TCP方式。下面的伪代码示例演示了读写寄存器的基本操作:
using EasyModbus; ModbusClient modbus = new ModbusClient("192.168.0.10", 502); modbus.Connect(); if (modbus.Connected) { // 写一个bool量到线圈(比如让PLC知道视觉系统已就绪) modbus.WriteSingleCoil(0, true); // 读PLC的保持寄存器(比如产线工位号、当前产品型号) int[] values = modbus.ReadHoldingRegisters(0, 5); // 写结果寄存器,0=OK, 1=NG modbus.WriteMultipleRegisters(20, new int[] { resultCode, x, y, angle }); }需要注意,Modbus TCP的Word是16位无符号数,如果结果超过65535或者需要小数,就要用到两个字表示32位浮点数的映射转换,否则数据完全对不上。
OPC UA更多用在需要跨系统集成、数据结构复杂的场景。C#可以用OPCFoundation提供的OPCFoundation.NetStandard.Opc.Ua库开发客户端,连接西门子S7-1500,浏览节点、读写变量。OPC UA的优势是自带安全证书、数据语义化、支持复杂数据类型,很多新产线开始采用,但配置和证书管理比Modbus复杂得多,小项目直接用Modbus TCP更快。
4.3 检测结果分流与不良品剔除逻辑
视觉结果最终要落到执行上。典型的分流策略是:检测结果OK → PLC放行;结果NG → PLC启动气缸或喷阀标记,把不良品吹到回收箱。
这里最大的坑在时序竞争——检测结果还没出来,工件就已经流到执行机构位置了。所以做系统设计的时候必须算清楚:从相机拍照位置到执行机构位置的流水线长度,除以传动速度,得到工件到达执行位置所需的时间,这个时间必须大于视觉处理耗时,否则来不及分拣。
如果时间不够,常见的解决方案是提前触发或者在传送带编码器上加装同步脉冲馈入,让相机根据编码器的位置脉冲触发,保证工件在视野内始终能拍到。这个逻辑听起来简单,实则涉及编码器倍频、相机触发分频、PLC计数同步,是整个项目里调试时间最长的地方之一。
5. 界面交互与数据管理
上位机除了是算法容器,还是产线工人每天要操作的工具。界面做得好不好用,直接影响车间接受度,也直接影响项目的后续维护成本。这块我总结为三件事:界面不卡、参数可调、数据可查。
5.1 多线程架构与UI防卡顿
WinForm/WPF默认是STA单线程模型,UI线程一被阻塞,整个窗口就假死。视觉检测里图像处理、SDK回调、PLC通信都是耗时操作,不能放到UI线程。我的标准做法是:UI线程只做界面刷新和用户输入响应;所有耗时操作放进任务线程或独立任务;跨线程更新UI通过BeginInvoke或异步事件投递。
private void UpdateResultLabel(string message) { if (this.labelResult.InvokeRequired) { this.BeginInvoke(new Action(() => { this.labelResult.Text = message; })); } else { this.labelResult.Text = message; } }除了跨线程,还有一个经常被忽视的UI控件过多导致卡顿的问题。有网友反映WinForm控件一多就卡得不行,这往往是因为控件的创建和布局频发,加上AllowDrop、背景重绘等属性没设置好。我的建议是:检测数据列表最多显示最近100行,不要无限累积;图像显示控件用双缓冲,避免图像快速重绘时闪烁和卡顿。
5.2 参数配置与产品换型
产线往往不可能只做一种工件,一套系统要覆盖多个型号。所以上位机必须提供参数配方管理,把检测参数、标定系数、触发延时、光源亮度等一整套配置按产品型号打包。
我用JSON文件来存配方,修改后保存、切换时加载,配合一个简单的产品型号下拉列表,换型只需选择型号,系统自动应用相应参数。这个功能看起来不起眼,但在现场非常实用,省去了每次换型都要工程师重新调参的麻烦。要注意修改配方之前先加密码权限,防止操作工误改。
5.3 检测数据存储与报表追溯
制造业要追溯,所以检测数据必须存储。最轻量的方案是用SQLite,单文件、零安装、读写快,足以支撑单机每天几万条记录。记录至少要有:时间、产品型号、检测结果OK/NG、数值结果、NG类别、相机编号、一次图像路径(存压缩过的图或JPG)。为了追溯方便,还可以把NG的原始图保存下来,按日期和流水号命名,这样出了问题能找到实物证据。
批量插入建议用事务或SQLite的参数化批量插入,不然几万条数据逐条插速度极慢,还可能锁库。我个人常用的方式就是积累一定数量后一次提交,速度能快十几倍。
6. 实战避坑指南与维护技巧
最后这一部分,我按项目现场最常见的坑做个集中复盘,也算是帮大家提前排雷。
6.1 相机掉线与自动重连
工业现场电磁环境恶劣,相机偶尔会因为网络波动或者USB供电不足掉线。掉线处理的第一原则是:不要直接崩溃退出,而是要掉线重连。Pylon SDK里连接丢失事件之后,要尝试重新Open(),重连之前要释放旧相机对象,不然会资源泄露。重连次数要做上限,超过后报警提示人工处理。
6.2 内存泄漏与GDI句柄问题
视觉系统长时间开机运行(产线基本7x24小时),内存泄漏就是隐形杀手。我看过很多上位机运行三天后内存占满的案例,后来又发现是Mat对象没有Dispose。OpenCvSharp的Mat包装了非托管内存,必须用using或在finally里释放,尤其在高帧率下,一个Mat不注意释放,半小时内存就能涨到几个GB。排查时用显式GC.Collect()验证,但生产环境不要频繁调用,会拖慢速度。
GDI句柄泄漏则是另一个坑,典型现象是内存占用不高但界面越来越卡。原因多是重复创建Font、Bitmap、Brush没有释放。正确做法是尽量复用资源对象,能静态定义就静态定义,用完及时Dispose()。快速检查方法是运行一段时间后打开任务管理器或性能计数器,看GDI对象计数是否持续增长。
6.3 图像处理时序与生产节拍
算法速度要卡节拍,处理帧率必须高于产线产能。这里有个容易被忽略的点:OpenCvSharp的图像处理默认是单线程的,如果图像分辨率太大,处理时间就会超预算。解决办法是缩小ROI区域,只处理工件所在区域,或者降低分辨率,或者用GPU推理/并行化。
另一个经典问题我也提过:回调中直接处理图像会导致相机缓冲区拥堵。如果相机的帧缓冲数量设置太小(比如只有一两个帧缓冲),图像处理不及时,后续帧就会丢失或阻塞,严重时相机直接超时。对策是把回调函数里的工作控制在最小,图像立刻Clone出来丢给队列,算法在独立线程做。
6.4 日志系统与现场调试经验
现场调试最怕的问题是无法复现。所以日志系统必须从上位机开发第一天就加上,而不是等项目上线后再补。我的习惯是每个模块都打日志,至少包含:事件时间戳、模块名、事件类型、详细信息。日志文件按大小轮转,比如单文件50MB,保留最近10个文件,避免日志无限增长。用NLog或log4net都能方便地实现,别自己裸写文件,并发写日志会丢失。
现场调试的另外一个技巧是:把“图+信息”一起存。遇到疑难杂症,把当前图像存成PNG,同时把图像对应的相机参数、算法参数、处理结果写进同一个日志序列里,这样离线复现时能快速定位是参数问题还是环境变化问题。这一招帮我省了大量现场跑动时间。
我个人在整个项目中最深的体会是:视觉检测系统的成败,七成在硬件和现场布局,算法只占三成。很多人在相机选型、打光方案上草草了事,指望算法包容一切,最后发现越补越多窟窿。真正稳定可靠的视觉系统,一定是先把硬件基础打牢——光源合适、相机匹配、机构时序精准,算法做的是锦上添花而不是雪中送炭。这套C#上位机+工业相机的方案,从验证到落地已经在我手上跑了十几个项目,成本控制在几万元级别就能替代几十万的进口检测单元,希望这篇文章能帮你在自己的产线上少走一段弯路。