ARTICLE DETAIL

资讯详情

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

OpenCV与C++气泡检测实战:从图像处理到工业应用

OpenCV与C++气泡检测实战:从图像处理到工业应用 简介面向工业检测与液体分析场景提供一套基于C和OpenCV的气泡检测完整工程。项目涵盖灰度化、高斯滤波、Canny边缘检测、形态学处理、连通区域标记及面积/圆度特征提取等核心环节并附带全局阈值法实现适合机器视觉初学者及OpenCV开发者参考。压缩包共36个文件、59.82MB以Visual Studio解决方案.sln/.vcxproj、C源码.cpp、测试图片.jpg、已编译可执行文件.exe为主另含OpenCV依赖配置.props和少量编译过程文件。目录结构清晰项目文件、源码、测试图分层存放便于直接打开运行并对照效果。已有473人下载学习可从已有工程快速上手气泡识别任务也可修改参数与算法迁移到更复杂的透明容器或微小气泡场景。 做工业视觉这些年气泡检测算是碰到频率很高的需求了医药行业西林瓶里的可见异物、日化行业透明瓶装液的灌装缺陷、玻璃制品内部的气泡夹杂本质上都是同一类问题在一张打光合适的图像里把那些圆形、椭圆的暗色区域挑出来。这个需求听起来简单真要做好坑还是不少。这次就把我用C和OpenCV做气泡检测的整套思路、代码实现和踩坑经验整理出来给准备上手或者正在调参的朋友一个参考。1. 项目思路与整体方案设计1.1 气泡检测的业务场景与难点先明确一下要解决的问题。气泡检测最常见的场景是透明容器内的液体质检比如西林瓶、安瓿瓶、口服液瓶。瓶子在传送带上快速流过相机抓拍系统需要在几十毫秒内判断瓶内有没有气泡、气泡的数量和大小是否超标。还有一类场景是检测透明固体内部的空腔比如玻璃件、亚克力件或者灌胶后的模块里有没有残留空气。这类检测难在几个地方。首先是目标本身——气泡是透明的边缘对比度很多时候并不高其次是背景复杂瓶壁反光、液面波动、瓶身划痕、外部灰尘都会形成干扰最后是实时性要求产线节拍快的时候单张图处理时间必须控制在几十毫秒甚至更低。这就决定了方案不能太复杂深度学习固然可以但标注成本高、部署麻烦在没有海量缺陷样本的情况下传统图像处理反而是性价比最高的选择。1.2 整体流程设计与方案选型我的核心思路很简单把气泡检测拆成“打光成像 图像分割 特征筛选”三段。打光部分不在代码里但极度影响后续处理这里先说结论——用背光源透射照明让气泡在亮背景下呈现为暗色圆斑这比正光源拍出来的效果稳定得多。图像处理部分按下面这条链路走彩色图转灰度去掉颜色信息干扰减少计算量滤波去噪把传感器噪声和细微颗粒干扰压下去二值化把暗色气泡从亮背景中分离出来形态学处理断开粘连、填平内部空洞findContours提取轮廓得到所有候选区域根据面积、圆度、尺寸等几何特征做筛选过滤掉杂质和干扰绘制结果并输出统计信息这套方案的优势是速度快、逻辑透明、参数可控。工业现场的检测逻辑讲究“可解释”质检员和工程师需要知道某个气泡为什么被判不合格几何特征筛选能很直观地回答这个问题这是黑盒模型做不到的。2. 图像预处理从彩色图到干净的二值图2.1 灰度化与滤波参数选择预处理是整个流程里最容易被低估的环节。很多新手拿到图就直接threshold结果边缘噪声一大堆特征筛选怎么调都压不下去。正确做法是先把图变得“干净”。灰度化没什么好说的cvtColor(src, gray, COLOR_BGR2GRAY)一行搞定。但如果现场相机输出的是单通道图记得做一次通道判断别在单通道图上再转灰度会直接报错。滤波我一般首选medianBlur孔径取5。中值滤波对椒盐噪声、孤立亮点特别有效而且能在去噪的同时保留边缘不会像高斯滤波那样把气泡边缘糊掉。如果图像噪声不大也可以跳过滤波省下那几毫秒但如果是低照度环境下拍的图噪声会直接影响二值化结果滤波这步就省不得。2.2 二值化阈值决定成败二值化的目标是让气泡变成白色前景背景变成黑色。关键问题是怎么选阈值。如果光照环境稳定固定阈值就行比如threshold(blur, bin, 100, 255, THRESH_BINARY_INV)。但产线光照不可能永远不变灯泡会老化、环境光会波动固定阈值迟早出事。我的习惯是优先用大津法Otsu也就是把阈值参数设为0加上THRESH_OTSU标志让算法根据灰度直方图自动算出最优分割值代码是threshold(blur, bin, 0, 255, THRESH_BINARY_INV | THRESH_OTSU)。注意这里用的是THRESH_BINARY_INV因为背光源下气泡是暗的我们要把暗区域提取为白色。如果打光方式反过来气泡是亮的就要用THRESH_BINARY。这个细节搞反了后续全是乱的。还有一类情况是图像里光照不均左边亮右边暗全局阈值怎么选都会丢区域。这时候用adaptiveThreshold它对每个像素根据邻域灰度动态算阈值抗光照不均能力很强。代价是速度稍慢、参数多了两个blockSize一般取奇数比如15或31C是一个常量偏移取2到10之间需要根据实际效果微调。2.3 形态学操作清理二值图二值化之后图里通常会有两类脏东西一类是小面积的孤立噪点一类是气泡内部的空洞或边缘毛刺。形态学操作就是干这个的。我一般先做一次开运算也就是先腐蚀再膨胀用小尺寸椭圆核Size(3,3)或者Size(5,5)。开运算能把细小噪点抹掉同时保持气泡的整体形状不变。如果气泡之间靠得很近、有粘连趋势可以适当加大核尺寸但核太大了会把小气泡一起抹掉这个要结合实际气泡大小来权衡。如果气泡内部因为有反光点而被二值化成了“甜甜圈”也就是中间出现了黑色空洞再做一次闭运算先膨胀再腐蚀把空洞填上。这样轮廓提取的时候一个气泡就只有一个闭合轮廓不会被拆成好几块。3. 轮廓提取与特征筛选3.1 findContours的正确打开方式二值图准备好之后核心步骤就是提取轮廓。OpenCV的findContours从2.x到4.x接口变化不大但有一个细节必须注意4.x版本之后findContours不会再修改输入图像所以传bin还是传bin.clone()都行但为了保险建议还是传入一个克隆副本避免某些旧版本下的意外行为。轮廓提取模式用RETR_EXTERNAL只取最外层轮廓。气泡在图像里是独立的闭合目标内部即使有嵌套结构也不是我们要关心的用RETR_EXTERNAL能把无关的内部轮廓全部忽略减少计算量。轮廓近似方法用CHAIN_APPROX_SIMPLE它只保存端点大幅降低轮廓点数对后续contourArea和arcLength的计算精度影响可以忽略。3.2 几何特征筛选不讲道理的过滤器找到轮廓之后如果直接全画出来你会发现图上有好多不是气泡的东西瓶底边缘、液体灌装时的气泡群、瓶身上的印刷字符……这时候就要靠几何特征来过滤。我用四个特征组合筛选按权重从高到低排列面积气泡有一个合理的面积范围。太小的是噪点太大的是液面或瓶壁反光。比如我的场景里气泡直径大概在0.5mm到3mm之间根据相机分辨率和物距换算成像素面积设个下限5像素、上限5000像素。圆度这是区分气泡和杂物最有效的指标。气泡在表面张力作用下趋近圆形圆度值极高而划痕、碎屑、液面边缘的形状就很不规则。圆度计算公式是4π * 面积 / 周长²标准圆的圆度是1。我一般设阈值为0.65到0.75低于这个值就判为非气泡。长宽比用minAreaRect获取最小外接矩形算宽高比。气泡的长宽比接近1细长的划痕、条状杂质长宽比会很大可以直接排除。外接圆半径#minEnclosingCircle可以算每个轮廓的最小外接圆半径能反映气泡的实际物理尺寸。如果产线要求检测特定粒径的气泡这个参数就很重要。下面是一个简单的圆度计算函数配合contourArea和arcLength使用double calCircularity(const vectorPoint contour) { double area contourArea(contour); if (area 1.0) return 0.0; double perimeter arcLength(contour, true); if (perimeter 1e-6) return 0.0; return 4 * CV_PI * area / (perimeter * perimeter); }筛选逻辑就是把上面几个指标串起来不满足任何一个就continue跳过最后留下来的就是候选气泡。这个流程本质上是在和信息做对抗——用几何形状这把尺子把所有不像气泡的东西都量出去。4. 完整C代码与工程化要点4.1 核心检测函数代码实现下面给出一段完整的可编译代码包含上面提到的所有环节。这个函数输入一张图像返回气泡的中心坐标、半径、面积和数量并且在原图上把气泡框出来。#include opencv2/opencv.hpp #include iostream #include vector using namespace cv; using namespace std; struct BubbleResult { vectorPoint2f centers; vectorfloat radii; vectordouble areas; int count 0; }; double calCircularity(const vectorPoint contour) { double area contourArea(contour); if (area 1.0) return 0.0; double perimeter arcLength(contour, true); if (perimeter 1e-6) return 0.0; return 4 * CV_PI * area / (perimeter * perimeter); } BubbleResult detectBubbles(const Mat src, double minArea 5.0, double maxArea 5000.0, double minCircularity 0.7) { // 1. 灰度化 Mat gray; if (src.channels() 3) cvtColor(src, gray, COLOR_BGR2GRAY); else gray src.clone(); // 2. 中值滤波去噪 Mat blur; medianBlur(gray, blur, 5); // 3. 大津法二值化提取暗色气泡区域 Mat bin; threshold(blur, bin, 0, 255, THRESH_BINARY_INV | THRESH_OTSU); // 4. 开运算去除细小噪点 Mat morph; Mat kernel getStructuringElement(MORPH_ELLIPSE, Size(3, 3)); morphologyEx(bin, morph, MORPH_OPEN, kernel); // 5. 提取轮廓 vectorvectorPoint contours; findContours(morph, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE); // 6. 几何特征筛选 BubbleResult result; for (const auto contour : contours) { double area contourArea(contour); if (area minArea || area maxArea) continue; double circularity calCircularity(contour); if (circularity minCircularity) continue; Point2f center; float radius 0.0f; minEnclosingCircle(contour, center, radius); result.centers.push_back(center); result.radii.push_back(radius); result.areas.push_back(area); result.count; } return result; } int main() { Mat src imread(bottle_sample.jpg); if (src.empty()) { cerr Failed to load image. endl; return -1; } BubbleResult result detectBubbles(src); for (int i 0; i result.count; i) { // 在原图上画圆标记 circle(src, result.centers[i], static_castint(result.radii[i]), Scalar(0, 0, 255), 2); } cout Bubble count: result.count endl; for (int i 0; i result.count; i) { cout Bubble i 1 center( result.centers[i].x , result.centers[i].y ) radius result.radii[i] area result.areas[i] endl; } imshow(Result, src); waitKey(0); return 0; }4.2 参数标定调优与批量处理把代码跑通只是第一步真正花时间的是参数调优。我的建议是准备一批有代表性的样本图至少20张覆盖正常品、有气泡次品、有杂质干扰的场景然后写一个小脚本循环调用detectBubbles把每张图的检测结果输出成文本再用真实的质检结果对比统计过检率和漏检率。调参的时候遵循一个原则先放宽面积范围把所有候选都放出来再逐步收紧圆度阈值观察误检数量变化。如果圆度阈值从0.6调到0.8误检没减少多少但漏检增加了说明问题不在圆度上而是二值化环节有干扰要回头调滤波核大小或者形态学参数。批量处理产线图片的时候建议把单张图的处理封装成一个独立函数然后用循环遍历文件夹。注意一点OpenCV的imread读中文路径在Windows下会失败这是老毛病了建议路径全用英文或者用imdecode配合fstream来读。这个坑我在现场调试时踩过程序在本地跑得好好的一上产线就报错最后发现是路径里有中文。4.3 工程配置要点编译环境以VS2019或VS2022配OpenCV 4.5以上版本为例。项目配置时注意几个关键点VC目录里的包含目录和库目录要分别指向OpenCV的include和lib文件夹链接器输入里加上opencv_world460.lib版本号按实际安装来如果用的是Debug模式记得连接opencv_world460d.lib——带不带d后缀的区别能让新手debug半天。跑起来之前把OpenCV的bin目录加到系统PATH里否则运行时找不到dll。5. 实战中踩过的坑与调试技巧5.1 光照波动导致的过检率飙升这是我在现场遇到最多的问题。背光源用久了亮度会衰减或者产线的环境光在某个时段特别强导致图像整体灰度变化大津法的分割阈值会跟着漂移结果就是气泡边界忽大忽小同一瓶产品不同时间拍出来的检测结果不一致。解决办法分两层硬件上尽量用恒流驱动的背光源减少电压波动软件上如果精度要求高可以在每次检测前先拍一张空白背景做灰度基准然后用当前图和基准图做差分把光照波动的影响直接减掉。差分操作在OpenCV里就是absdiff代价是一次额外的图像运算但稳定性提升非常明显。5.2 气泡粘连与聚合形态当液体里气泡很多时它们会聚成一团在图像上看起来就是一个不规则的连通域。这时候圆度筛选会失效因为聚合体的圆度很低真实的气泡会被误判为杂质。处理这类问题可以在二值化之后先做一次腐蚀把粘连的气泡“拆开”。腐蚀的核不能太大3x3即可。腐蚀之后气泡之间的连接处会断开再用findContours就能把它们分开。代价是小气泡可能被腐蚀掉所以腐蚀迭代次数要严格控制通常1到2次就到了极限。如果腐蚀拆不开说明气泡粘连太严重这时候就要考虑分水岭算法了但这个就超出了今天讨论的范围。5.3 性能优化别让算法拖累产线节拍产线视觉系统对耗时非常敏感。如果单张图处理超过100毫秒可能就跟不上传送带速度。我的优化顺序是这样第一优先限制处理区域。用一个固定的ROI把瓶身区域先裁出来只检测这个区域里的气泡背景区域直接不处理。这招效果最明显处理时间直接按ROI面积占比下降。第二优先降采样。如果原图是2448x2048先resize到1224x1024处理时间能减到原来的四分之一。代价是小于两三个像素的小气泡会丢失所以降采样只适用于检测粒径较大的场景。第三优先减少图像拷贝。代码里到处Mat temp src.clone()是大忌每次克隆都是一整块内存复制。尽量用Mat temp src.rowRange(...).colRange(...)这种视图操作只在真正需要修改像素时才clone。我见过有人为了节约时间把medianBlur的核从5改成3又把形态学那步去掉结果过检率直接超标。优化性能的前提是保证检测效果宁可多花5毫秒也不能牺牲准确率。5.4 气泡太小被漏检怎么办小气泡的漏检是另一个常见问题。如果气泡直径在图像上只有两三个像素二值化之后它可能就只剩一两个像素的噪点contourArea算出来不到1直接被面积下限过滤掉了。这时候可以把面积下限从5调到1甚至0.5同时把圆度阈值放宽因为小轮廓的周长计算误差很大圆度值不稳定。另一种思路是换算法。当气泡小到像素级时基于轮廓的方法就力不从心了可以试试HoughCircles它基于梯度投票对微小圆形目标有更好的检出效果。但它对参数敏感dp、minDist、param1、param2四个参数要反复调而且速度比findContours慢不少适合离线抽检不太适合高速产线。最后再分享一个调试技巧气泡检测这东西写代码只占两成时间剩下八成都在调参和排障。我的经验是每次调完参数都要把处理过程的中间图——灰度图、二值图、形态学后的图、轮廓筛选后的图——全部保存下来做成一个可视化调试页面。哪一步出了问题看图一目了然比对着数字猜效率高太多了。把检测流程封装成一个小工具支持任意一张测试图的批量检测和中间过程可视化这个工具会伴随你后续所有视觉项目不光是气泡检测其他圆形缺陷检测比如钢珠缺失、药片破损、焊点检测核心逻辑改一改就能套用。这也是为什么我一直觉得传统图像处理在工业视觉里永远不会过时——它可能不如深度学习“聪明”但它稳定、快速、透明而且在很多场景下已经足够用了。本文还有配套的精品资源点击获取
返回列表