☰
C++与OpenCV工业气泡检测实战:成像、分割与特征筛选
2026/10/28 15:02:22 网站建设 项目流程

简介: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参数。正确写法是这样:

vector<vector<Point>> 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; // 圆度 }; vector<Bubble> 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); vector<vector<Point>> contours; findContours(binary, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE); vector<Bubble> 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霍夫圆检测的效果好很多——霍夫圆对参数极其敏感,气泡一变形就容易出问题。做这类项目,我最大的体会是不要把问题想复杂,先把成像打好,再用最朴素的特征把目标筛出来,最后把防御误报的机制做足,这套组合拳在产线上远比花哨的算法可靠。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询