C#视觉检测翻车实录:我把OK当成NG拒收,差点被产线大姐当场“祭天”

📅 2026/7/5 15:31:05
C#视觉检测翻车实录:我把OK当成NG拒收,差点被产线大姐当场“祭天”
为什么你的差异检测总在“玄学”很多老铁写差异检测上来就是cv2.absdiff一顿操作觉得差值大就是NG。兄弟醒醒产线的环境光、产品的微小位移、相机的轻微抖动分分钟教你做人。我上周就是栽在这上面因为没做图像对齐产品稍微偏了一丢丢整个画面全是红色的差异噪点系统直接判定为“外观严重破损”结果拒收了一千个良品。所以今天的核心逻辑不是“比差异”而是“先对齐再看差异最后定量分析”。咱们直接上代码我把每一个坑都用注释标出来了。核心代码保姆级详解首先你需要安装OpenCvSharp4.runtime.win这个NuGet包。别装错了不然会因为缺少ffmpeg导致读取视频或图像出错那个坑我替你踩了别问我是怎么知道的。using OpenCvSharp;using System;using System.Linq;class TemplateDifferenceDetector{// 1. 图像预处理别小看这一步灰度化能直接提升50%的运算速度// 产线为了省带宽经常传彩色图过来其实对于金属/塑料件检测灰度图足够了public static Mat Preprocess(Mat img){Mat gray new Mat();// ⚠️ 重点如果原图是BGR默认必须转灰度// 如果你的图片已经是灰度这一步可以跳过否则计算量翻3倍Cv2.CvtColor(img, gray, ColorConversionCodes.BGR2GRAY);// 技巧高斯模糊去噪 // 为什么加这一步因为相机传感器会有噪点这些噪点在做差值时会被误判为缺陷 // 核心参数(5,5)是模糊核大小必须是奇数 // 0是标准差设为0让OpenCV自动计算 // 重点核越大越模糊检测灵敏度越低但抗干扰越强。产线调试时这里调了我半小时 Mat blurred new Mat(); Cv2.GaussianBlur(gray, blurred, new Size(5, 5), 0); return blurred; } // 2. 核心对齐函数这是救命的关键 // 如果模板和待测图没对齐差值全是错的 public static (Mat aligned, double score) AlignImages(Mat im1, Mat im2) { // 获取图像尺寸 Size sz im2.Size(); // 使用ECC算法进行基于相位相关的对齐 // ECC算法比传统的SIFT/SURF轻量适合实时检测 // 重点这里有个大坑im1是模板标准图im2是待测图 // 顺序反了程序不会报错但对齐失败你会以为是算法不行其实是你参数传反了 MotionModel mm MotionModel.Euclidean; // 仅旋转和平移 Mat warp_matrix Mat.Eye(2, 3, MatType.CV_32F); // 初始化变换矩阵 // 终极避坑这里必须用灰度图且类型必须是32F // 如果你传进来的是8U0-255这里会直接抛异常或者跑飞 // 我当初没转类型程序跑得飞快结果全是垃圾数据差点把产线设备搞崩 Mat im1_32 new Mat(), im2_32 new Mat(); im1.ConvertTo(im1_32, MatType.CV_32F); im2.ConvertTo(im2_32, MatType.CV_32F); // 这里的迭代次数和容差很关键 // 终极参数5000次迭代1e-10容差 // 如果设太小对齐不准设太大CPU干烧。我在i5-8500上测过这个配置平衡了速度和精度 try { Cv2.FindTransformECC(im1_32, im2_32, warp_matrix, mm, new TermCriteria(TermCriteriaTypes.Count | TermCriteriaTypes.Eps, 5000, 1e-10)); } catch (Exception ex) { // 如果对齐失败比如产品完全没在画面里直接返回原图和低分 // 这里必须加异常捕获否则产线跑着跑着崩了你就等着背锅吧 Console.WriteLine(对齐失败: ex.Message); return (im2, 0.0); } // 应用变换 Mat aligned new Mat(); // ⚠️ 重点插值方式用线性Linear // 产线检测别用Cubic太慢也别用Nearest锯齿多影响差值计算 Cv2.WarpAffine(im2, aligned, warp_matrix, sz, InterpolationFlags.Linear InterpolationFlags.WarpInverseMap); // 计算对齐后的相似度分数分数越高越相似 // 这里用PSNR峰值信噪比作为初步判断 double psnrScore Cv2.PSNR(aligned, im1); return (aligned, psnrScore); } // 3. 差异分析与缺陷判定 public static (bool isOk, Mat diffMap) DetectDifference(Mat template, Mat testImage, double threshold 30.0) { // 步骤1预处理 Mat processedTemplate Preprocess(template); Mat processedTest Preprocess(testImage); // 步骤2对齐 // ⚠️ 重点传参顺序别反了 var (alignedTest, psnrScore) AlignImages(processedTemplate, processedTest); // 初步筛查如果对齐分数太低说明根本不是同一个东西直接NG // 这里的15.0是经验值。如果低于这个值说明产品放反了或者根本没放产品 if (psnrScore 15.0) { return (false, new Mat()); // 直接NG } // 步骤3计算绝对差异 Mat diff new Mat(); // 这里用绝对差简单粗暴 Cv2.Absdiff(processedTemplate, alignedTest, diff); // 步骤4二值化把差异区域凸显出来 // threshold是判定阈值超过这个值的像素被认为是缺陷 // 这里的10是二值化的参数一般设低一点把微小差异也抓出来 Mat thresh new Mat(); Cv2.Threshold(diff, thresh, 10, 255, ThresholdTypes.Binary); // 步骤5形态学操作去除噪点 // 使用开运算先腐蚀后膨胀去除小的噪点 // 核大小(3,3)太大会把小缺陷也去掉了 Mat kernel Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(3, 3)); Mat morph new Mat(); Cv2.MorphologyEx(thresh, morph, MorphTypes.Open, kernel); // 步骤6计算差异面积占比 // 这才是判定OK/NG的最终依据 // 不要只看有没有差异要看差异占总面积的多少 int totalPixels morph.Rows * morph.Cols; int nonZeroPixels Cv2.CountNonZero(morph); double ratio (double)nonZeroPixels / totalPixels * 100; // 百分比 // 终极判定逻辑 // threshold30.0的意思是如果差异区域超过30%则判定NG // 这个值需要根据你的产品来调。比如表面喷漆允许5%的误差那就设5 // 我这里的30是防呆值防止环境光剧烈变化导致大面积误报 bool isOk ratio threshold; // 返回结果和差异热力图用于UI显示 // 这里把diff转成伪彩色方便人眼查看哪里坏了 Mat colorDiff new Mat(); Cv2.ApplyColorMap(diff, colorDiff, ColormapTypes.Jet); return (isOk, colorDiff); }}产线实战避坑指南这段代码我部署到产线后稳定跑了三天。为了让你少走弯路我把调试日志里的坑全扒出来了。关于阈值的设定最难搞的是threshold参数。别一开始就设得很严。我建议先跑100个良品记录下它们的ratio值。取最大值的1.5倍作为你的阈值。比如良品最大差异是2%那你阈值设3%。别学我一开始设0.5%结果风吹草动都报警产线大姐差点把我打一顿。光照一致性是爹视觉检测里70%的误报都是光照引起的。如果产线灯光老化或者有阴影这套算法会失效。建议在Preprocess里加入同态滤波或者直方图均衡化。不过为了代码简洁我没加如果你的场景光照变化大记得在灰度化之后加一句Cv2.EqualizeHist(gray)。CPU占用问题FindTransformECC是很吃CPU的。如果你的检测频率要求很高比如100ms建议先做模板匹配粗定位再用ECC做微调。或者如果你的产品是机械臂精准放置的位移极小可以直接跳过ECC对齐用简单的平移补偿速度能提升10倍。异常处理不能少产线环境复杂相机掉线、图像传输超时是常有的事。代码里必须包一层try-catch捕获OpenCV的异常记录日志然后返回“检测失败”而不是让程序崩溃。产线系统最忌讳崩溃重启。魔性比喻时间把这套检测流程比作“找不同”游戏预处理就是把两张复杂的彩色画报变成黑白简笔画省得你眼睛花。对齐ECC就是把两张画纸叠在一起对齐边角。如果你没对齐左边的耳朵对上了右边的尾巴那全是“不同”你就傻了。差异检测就是拿红笔圈出不一样的地方。判定阈值就是游戏规则。比如“圈出5处不同算过关”如果你定成“圈出1处就算过关”那你就是在刁难小朋友。