十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C++与OpenCV工业气泡检测实战:成像、分割与特征筛选

C++与OpenCV工业气泡检测实战:成像、分割与特征筛选 简介C结合OpenCV实现气泡检测的完整工程包面向计算机视觉初学者与工业检测开发者适用于液体分析、质量检测等场景可帮助理解从图像预处理到气泡区域识别的完整流程。资源基于Visual Studio搭建内含可运行源码、测试图片与全局阈值法实现覆盖灰度化、直方图均衡化、高斯滤波、Canny边缘检测、形态学腐蚀膨胀、连通区域标记及面积周长特征提取等关键环节便于对照代码逐段学习。压缩包共36个文件以cpp源码、sln工程文件、jpg测试图、pdb调试文件及tlog构建日志为主另有x64 Release可执行程序整体约59.82MB可直接打开工程复现效果。目前已有473人学习下载可结合博客文章快速上手通过调整阈值与形态学参数优化不同液体环境下的气泡识别结果为后续缺陷检测或自动化分析提供基础。 我最早接触气泡检测是在一条输液器生产线上。药液灌装时只要混入几个气泡灌装量就会不准严重的直接整批报废。人工目检盯久了必然疲劳漏检率根本压不住。后来我改用C配合OpenCV做了一套气泡检测模块放到工控机上跑把背光成像、阈值分割、连通域分析和特征筛选串成完整链路NG拦截稳定产线也一直没出过大的幺蛾子。这篇就把这套方案从头到尾拆开讲涵盖成像打光、图像预处理、核心检测算法、特征筛选和工程落地的各种坑。不管你是做医疗器械、制药灌装、微流控芯片还是检测玻璃/亚克力内部的透明气泡缺陷这套思路都可以直接借鉴。1. 气泡检测到底是什么场景的需求1.1 从输液器到微流控哪些产线需要这个功能气泡检测在工业视觉里非常常见但它不是单指某一种固定的检测对象而是一类透明基体内部/表面出现气相缺陷的问题。我梳理了一下大概能分成这么几类应用场景医疗器械输液器、输液袋、血液透析管路、注射器灌装。气泡混入直接影响剂量精度属于安全底线问题。制药行业西林瓶、安瓿瓶的液体灌装灌装完成后要检测液面附近有没有气泡或者药液内部有没有微小气泡残留。微流控芯片芯片通道里的气泡会导致液体流动中断、反应失败检测通常要在灌注后立刻完成。食品饮料透明瓶装液体的灌装、固体果酱或蜂蜜里混入的空气泡影响外观和净含量。材料行业玻璃、亚克力、透明塑料件内部的气泡缺陷影响结构强度和外观品级。这些场景的共同特点是检测目标在透明或半透明基体里直径从零点几毫米到几毫米不等产线节拍从每秒几个到每分钟上千个。气泡本身不像金属划痕那样形态固定它会流动、变形、聚集所以检测方案必须足够鲁棒。1.2 透明目标检测的三大难点第一个难点是对比度低。透明物体对光的吸收很小气泡和液体/玻璃之间的折射率差异也没有想象中那么大如果成像方案选得不对气泡在图像里几乎是隐形的再牛的算法也无从下手。第二个难点是形态变化大。静止的气泡接近圆形但一旦液体流动或者受到压力变化气泡就会被拉成椭圆、长条甚至聚集成泡沫群。这就意味着不能用固定的模板匹配也不能只判断圆不圆。第三个难点是干扰因素多。产线震动、环境光波动、容器表面的划痕和手印、管路侧壁的弧度都会在图像里形成类似气泡的暗色区域一不小心就会被误判成气泡。1.3 为什么用C和OpenCV而不是深度学习很多人现在一提到视觉检测就想到深度学习但对于气泡检测这类场景传统图像处理方案有它不可替代的优势。气泡在背光条件下是非常明确的暗色目标跟背景的灰度差异很大用阈值分割加连通域分析就能解决得很好完全不需要收集几千张缺陷样本去训练模型。选C是因为部署方便。产线工控机通常配置有限C编译出来的程序直接跑不需要依赖复杂的运行环境内存和CPU的占用也可控。OpenCV作为图像处理库已经非常成熟threshold、findContours这些基础函数经过了大量项目验证稳定性和可排查性都远好于自己造轮子。真到了线上出问题的时候打开一张中间结果图就能定位到是哪一步处理出了问题这对产线维护来说极其重要。2. 成像打光是检测成败的第一步2.1 背光照明下气泡呈现的灰度特征气泡检测的成像方案我首推背光照明。所谓背光就是把光源放在被测物体的后方相机从前方拍摄透射光图像。当平行光穿过液体时气泡内的气体折射率和周围液体不一样光线在气泡边界发生偏折最终在成像面上形成一个暗色斑块。背景是明亮的气泡是暗的对比度可以做到非常高后续算法就会非常简单。如果不用背光而是用环形光或者条形光从前方打光情况就完全不一样了。透明容器表面会形成强烈的镜面反射光源的反光直接把内部气泡淹没掉你看到的就是一片高光提取特征的难度会成倍增加。所以现场如果条件允许尽量优先考虑背光。光源颜色上红色或白色都可以。红色光波长大、穿透力稍强工业相机对红光响应也普遍好一些还能减少一部分环境杂散光的影响。白色光源则更利于现场人员肉眼观察。光源的尺寸要略大于视野范围否则视野边缘的亮度会明显衰减。2.2 相机参数与分辨率估算分辨率怎么定这一步必须根据最小气泡直径来倒推。假设我们要检出的最小气泡直径是0.3mm考虑算法可靠性希望它在图像里至少占5个像素那像素精度就是0.06mm/pixel左右。如果视野宽度是50mm那么相机分辨率至少要在830像素以上实际用的时候我会留两倍余量选1600像素左右的相机。曝光和增益的设置也很关键。背光场景下光源是稳定的我建议用手动曝光和手动增益不要开自动曝光。自动曝光会根据画面内容不断调整参数气泡一旦出现画面平均亮度变化曝光参数就会跟着漂导致图像亮度不稳定算法的阈值也就没法稳定。增益尽量调低增益过高会把传感器的噪声一并放大微小的气泡直接淹没在噪点里。2.3 成像环节的几个隐蔽坑相机必须正对被测目标。倾斜安装会让圆形气泡在图像里变成椭圆小气泡的成像面积也会被压缩。光源如果支持频闪要确认频率不会和相机的帧率打架否则会出现明暗交替的条纹。容器表面的结露、指纹、污渍在背光下都会表现为暗色区域很容易被当成气泡。检测节拍允许的话可以在检测前加一道吹气或擦拭工序。3. 预处理与阈值分割让气泡从背景里浮出来3.1 灰度转换与中值滤波的取舍拿到相机图像后第一步是转灰度。OpenCV里直接用cvtColor处理Mat gray; cvtColor(src, gray, COLOR_BGR2GRAY);然后是滤波。这一步的目的是去除噪声同时尽量保留气泡边缘的锐利度。我习惯用中值滤波而不是高斯滤波原因很直接背光图像里的噪声以椒盐类噪声和传感器固定噪声为主中值滤波对这类噪声的抑制效果极好而且它对边缘的模糊程度远小于高斯滤波。高斯滤波平滑噪声的同时会把小气泡的边缘一起磨掉本来就只有几个像素的小气泡磨一下直接消失了。Mat blurred; medianBlur(gray, blurred, 5);核大小要谨慎。3x3和5x5是最常用的如果气泡本身就很小建议用3x3只有画面里杂质点比较多时才考虑5x5。核太大小气泡就变成了背景的一部分这一步是很多新手容易忽略的。3.2 Otsu自适应阈值分割及其失效场景滤波完成之后我们要把气泡从背景里分离出来。背光图像经过上面处理后灰度直方图通常是典型的双峰分布高灰度峰对应明亮的背景低灰度峰对应气泡和暗色边缘。这种情况下用Otsu大津法可以自动找到一个分割阈值不用人工去调。Otsu的原理是遍历所有可能的灰度阈值找到让前景和背景两类之间的类间方差最大的那个值。这个值就是最佳分割点。OpenCV里的用法很简洁Mat binary; threshold(blurred, binary, 0, 255, THRESH_BINARY_INV | THRESH_OTSU);这里一定要加THRESH_BINARY_INV。因为背光下气泡是暗色的不加INV的话分割出来的前景是背景气泡反而成了黑色后续findContours处理的方向就反了。Otsu虽然自适应但也会失效。最常见的失效场景是ROI设置不合理导致画面里气泡目标占的面积比例过大或过小。比如视野里几乎全被大气泡占满背景只剩很小一块Otsu找出来的阈值可能就不准了。解决办法是合理设置检测区域让气泡在ROI中的面积占比保持在一个合理区间一般10%到60%之间会稳定得多。对于光照不均的情况可以先对整幅图做灰度拉伸或亮度归一化再做Otsu。4. 连通域提取与特征筛选从像素到这是一个气泡4.1 findContours的参数选择阈值分割之后binary图像里白色区域就是候选目标。接下来用findContours提取连通域轮廓。这里有两个参数需要认真选。第一个是轮廓提取模式mode。气泡作为独立目标用RETR_EXTERNAL就够了它只提取最外层轮廓不会把气泡内部嵌套结构也当成新的目标。如果遇到双层气泡这种特殊情况气泡内部还包着气泡才需要RETR_TREE这样的层级模式然后结合hierarchy分析内外层关系。第二个是轮廓近似方法method。推荐用CHAIN_APPROX_SIMPLE它会把水平、垂直和对角方向上的冗余点压缩掉只保留端点这样轮廓的数据量小处理速度快。注意OpenCV 4.x以后findContours的接口签名有变化不再返回轮廓而是直接修改传入的contours参数。正确写法是这样vectorvectorPoint contours; findContours(binary, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);4.2 面积、圆度、最小外接圆三重判断轮廓提取出来之后里面既可能有真气泡也可能有灰尘点、划痕和噪声块。单靠一个条件是筛不干净的我一般用三重判断联合筛选。首先是面积。contourArea可以计算轮廓的像素面积。设定最小面积阈值把那些只有几个像素的噪声点直接滤掉再设定最大面积阈值把超出气泡合理范围的大块暗区排除掉。其次是圆度。气泡在静止状态接近圆形但流动状态下会变形。用圆度公式circularity 4 * PI * area / (perimeter * perimeter)圆形目标的圆度接近1越不规则越接近0。真实气泡的圆度通常在0.6以上杂质和划痕的形状不规则圆度往往很低。以0.5到0.6作为阈值可以比较好地区分。第三是最小外接圆。用minEnclosingCircle获取轮廓的最小外接圆之后可以计算轮廓面积与外接圆面积的比值。如果轮廓是一个椭圆形气泡这个比值和圆度指标可以互相补充进一步确认目标是否接近气泡形态。4.3 核心检测代码实现把上面这些综合起来一个完整的检测函数大致长这样#include opencv2/opencv.hpp #include vector #include iostream using namespace cv; using namespace std; struct Bubble { Point2f center; // 气泡中心 float radius; // 最小外接圆半径 double pixelArea; // 像素面积 double realArea; // 换算后的实际面积单位mm^2 double circularity; // 圆度 }; vectorBubble detectBubbles(const Mat src, double minAreaPixel, double maxAreaPixel, double minCircularity, double pixelToMmSquare) { Mat gray, blurred, binary; cvtColor(src, gray, COLOR_BGR2GRAY); medianBlur(gray, blurred, 5); // 背光下气泡是暗色用INV把气泡变成白色前景 threshold(blurred, binary, 0, 255, THRESH_BINARY_INV | THRESH_OTSU); vectorvectorPoint contours; findContours(binary, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE); vectorBubble result; for (const auto contour : contours) { double area contourArea(contour); if (area minAreaPixel || area maxAreaPixel) { continue; } double perimeter arcLength(contour, true); if (perimeter 0) { continue; } double circularity 4 * CV_PI * area / (perimeter * perimeter); if (circularity minCircularity) { continue; } Point2f center; float radius; minEnclosingCircle(contour, center, radius); Bubble b; b.center center; b.radius radius; b.pixelArea area; b.realArea area * pixelToMmSquare; b.circularity circularity; result.push_back(b); } return result; }pixelToMmSquare的换算方法是pixelToMmSquare (realWidthMm / imageWidthPixel) ^ 2也就是单个像素对应的实际物理尺寸的平方。这个值在相机标定或者用标准尺测量视野宽度时可以确定。5. 工程落地实时性、参数自适配与防误报5.1 ROI裁剪与分辨率权衡算法在实验室跑通只是第一步真正拿到产线上要考虑的问题完全不同。第一个必须做的就是ROI裁剪。相机安装好之后气泡只可能出现在固定的管路或者容器区域剩下的画面都是背景。我们先把检测区域框出来处理时只在ROI内操作计算量大幅下降同时还能避开很多与检测无关的干扰。分辨率也是一样不是越高越好。分辨率越高处理时间几乎成倍增长而气泡检测并不需要特别精细的纹理信息。在满足最小气泡尺寸精度要求的前提下尽量降低分辨率能换来更快的处理速度和更低的硬件成本。如果处理时间仍然紧张可以把采集和处理拆到不同线程采集线程负责抓图处理线程负责算法显示线程负责绘制结果。产线上实测下来这种流水线式的并行结构往往比单纯优化算法更有效。5.2 光照波动与参数自适配产线环境的灯光会衰减光源用久了亮度会下降相机增益也可能漂移。这些都会导致图像整体灰度改变阈值如果写死过一段时间就会频繁误检。我的处理思路是光学层面用固定曝光和固定增益从源头控制变量算法层面在每一帧计算ROI区域的平均灰度一旦和基准值偏差超过一定范围就触发光照异常告警提示现场人员停机检查而不是让系统在异常成像条件下继续闷头跑。如果产品本身的透光性差异很大可以做一个简单的动态亮度归一化先算出ROI平均灰度再用一个期望灰度值除以平均灰度得到增益系数对整帧图像做乘法拉伸这样不同产品在算法入口处的灰度水平就能基本保持一致。5.3 防误报与结果可视化产线上震动、灰尘、偶发噪声都会造成单帧误检。如果检测到一帧有气泡就立即报警现场几乎没法正常生产。我通常会让系统连续检测多帧比如连续3帧都判定有气泡才真正触发NG信号单帧的偶发异常直接忽略。这个策略对产线环境非常有效代价只是多了两帧的响应时间对大多数灌装产线来说完全可接受。结果可视化同样是工程落地的关键一环。检测到气泡后用drawContours或circle在原图上把结果标出来同时imwrite保存对应的NG图像。这样现场工程师一眼就能看到系统的判定依据出了误报也能直接对着图片排查而不是对着一个抽象的报警灯干瞪眼。写一下可视化部分的参考代码Mat display src.clone(); for (const auto b : detectedBubbles) { circle(display, b.center, (int)b.radius, Scalar(0, 0, 255), 2); } imwrite(ng_ to_string(frameIndex) .jpg, display);气泡检测这件事算法本身并不复杂真正的难点在于把每一个环节都做扎实成像要稳定、分割要合理、筛选条件要贴合实际场景。用findContours加特征筛选的方式又稳又快这比我最初尝试HoughCircles霍夫圆检测的效果好很多——霍夫圆对参数极其敏感气泡一变形就容易出问题。做这类项目我最大的体会是不要把问题想复杂先把成像打好再用最朴素的特征把目标筛出来最后把防御误报的机制做足这套组合拳在产线上远比花哨的算法可靠。本文还有配套的精品资源点击获取
返回列表