ARTICLE DETAIL

资讯详情

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

OpenCV轮廓提取实战:findContours参数详解与避坑指南

OpenCV轮廓提取实战:findContours参数详解与避坑指南 简介这是一份OpenCV轮廓提取功能的学习资料以PDF格式呈现文件大小95KB适合正在学习图像处理、希望掌握findContours与drawContours用法的初学者。内容从轮廓的基本概念讲起说明轮廓可理解为有序点集并重点解析findContours函数中image、contours、hierarchy、mode、method、offset等参数的含义同时介绍四种检索模式和四种近似方法的区别。资料还结合drawContours的绘制参数解释如何将检测到的轮廓显示到图像中并配有基于滑动条调节阈值的Canny边缘检测与轮廓提取示例代码便于读者在实际环境中运行和观察效果。目前已有2255人学习下载对快速理解OpenCV轮廓提取流程、理清常见参数用法具有较强参考价值。1. Opencv轮廓提取一组有序点集背后的工程价值做图像处理项目时轮廓提取几乎是绕不开的第一步。比如我要数一张图片里有几个零件、定位仪表盘上的刻度线、把ROI区域的边缘框出来第一反应不是上深度学习而是先用OpenCV的findContours把轮廓拿到手。轮廓本质上是一系列像素点组成的有序集合它保存的是目标的形状信息而不是整张图。这篇笔记我围绕Opencv轮廓提取这条主线把findContours和drawContours的参数、检索模式、近似方法以及实际跑代码时遇到的坑完整过一遍。读完你能照着代码把轮廓提取跑通也能知道什么时候该用RETR_EXTERNAL、什么时候该用RETR_TREE更能在轮廓数量异常、绘制结果发懵的时候有排查思路。适合正在做图像预处理、目标检测前处理或者被OpenCV轮廓参数绕晕的开发者。2. 认识findContours核心参数与四种检索模式的取舍2.1 输入图像为什么必须处理成二值图Canny和threshold的分工findContours函数的第一个参数是单通道图像矩阵官方文档明确要求输入是二值图。但实际项目中直接喂二值图的情况很少大多数场景是先对灰度图做Canny边缘检测得到一张只有边缘像素为白色、其他区域为黑色的图再交给findContours。有人会问为什么不能直接把灰度图丢进去答案在于轮廓的定义轮廓是灰度跳变形成的闭合边界。灰度图每个像素值都在0到255之间连续变化算法很难判断哪里是真正的轮廓边界。二值图把问题简化了白色像素就是边缘候选点黑色就是背景findContours只需要在白色像素之间寻找连通关系再把这些点按顺序串起来。在使用Canny时双阈值的选择影响很大。Canny的两个阈值参数分别控制强边缘和弱边缘的判定低阈值以下的像素直接丢弃高阈值以上的像素确定为边缘介于两者之间的像素只有在与强边缘相连时才会被保留。我一般会把低阈值设在50左右、高阈值设在100左右然后根据实际图像微调。阈值太低噪声会被当成边缘轮廓数量暴增阈值太高目标的轮廓断成好几截findContours就找不到完整的闭合轮廓。cvtColor(src, src, CV_BGR2GRAY); Canny(src, canny_img, value, 2 * value);这段代码把彩色图转成灰度图再做Canny边缘检测。value是滑动条的当前值Canny的第二个参数是低阈值第三个参数是2倍的低阈值。用2倍关系是Canny最常用的经验配比能有效过滤梯度变化比较弱的噪声同时保住真正的物体边界。如果想进一步收敛轮廓数量可以在Canny之前加一个高斯模糊但注意模糊半径不能太大否则细小的轮廓细节会被抹掉。2.2 mode检索模式从RETR_EXTERNAL到RETR_TREE怎么选findContours的第四个参数mode决定轮廓的检索模式这也是初学者最容易混淆的地方。OpenCV提供了四种模式RETR_EXTERNAL、RETR_LIST、RETR_CCOMP、RETR_TREE。RETR_EXTERNAL只检测最外层的轮廓如果图像里有一个圆环它只返回外圈的轮廓内圈的轮廓被忽略。适合只关心物体外边界、不需要内部结构的场景比如统计画面里有多少个独立物体。RETR_LIST检测所有轮廓但轮廓之间不建立等级关系。所有轮廓都是平级的hierarchy向量里每个元素的后两个分量会被置为-1。这个模式适合只需要把轮廓全部列出来、不关心嵌套关系的场景。RETR_CCOMP把轮廓组织成两层结构外层是顶层轮廓内层是第二层。如果内层轮廓里还有更深的嵌套统一归到第二层不再往下分级。这个模式在查找“孔洞”时比较方便比如齿轮的齿孔。RETR_TREE建立完整的等级树结构每个轮廓都记录了后一个、前一个、父轮廓、内嵌轮廓的索引。这个模式信息最全但耗时也相对高一些。我在做零件筛选时通常用RETR_TREE因为后续可以通过hierarchy拿到父子关系快速判断一个轮廓是独立物体还是另一个轮廓内部的孔。模式检测范围层级关系典型场景RETR_EXTERNAL只检测最外层无内部层级数物体个数RETR_LIST检测全部无层级全部平级只需全部轮廓RETR_CCOMP检测全部两层结构提取孔洞RETR_TREE检测全部完整树状层级需要父子关系2.3 method近似方法SIMPLE与NONE对轮廓点数量的影响method参数决定轮廓的近似方式直接影响contours里点的数量。CV_CHAIN_APPROX_NONE保存边界上所有连续的轮廓点一条直线上的每个像素都会进contours点数量非常多内存占用高后续计算耗时也大。CV_CHAIN_APPROX_SIMPLE只保存拐点信息比如一个矩形的轮廓它只保存四个顶点直线段中间的点全部丢弃。这个模式在绝大多数场景下都够用而且数据量小、运算快。我默认就用SIMPLE只有在需要精确还原边界形状、或者要逐点分析曲线走势时才用NONE。还有两种基于Teh-Chin链的近似算法CV_CHAIN_APPROX_TC89_L1和CV_CHAIN_APPROX_TC89_KCOS。这两种算法会自动压缩轮廓点压缩程度介于NONE和SIMPLE之间实际项目中用得不多。理解method的关键点是它只影响轮廓点的稠密程度不影响轮廓的位置和数量。轮廓数量由mode和Canny阈值决定轮廓外形由SIMPLE拐点保真度决定这两者的职责不要混。findContours(canny_img, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0));这行代码用了RETR_TREE和CHAIN_APPROX_SIMPLE组合。contours是双重向量外层每个元素对应一个轮廓内层是该轮廓的所有点hierarchy和contours一一对应每个hierarchy元素保存4个整型索引分别指向后一个轮廓、前一个轮廓、父轮廓、内嵌轮廓。如果对应位置不存在值为-1。Point(0,0)是偏移量表示不偏移如果轮廓是从ROI区域提取的需要把ROI在原始图像中的位置加进来。3. 画轮廓drawContours参数与轮廓索引的配合3.1 核心参数拆解contourIdx、thickness、maxLevel怎么配合drawContours函数把findContours算出来的结果可视化到目标图像上。第一个参数是目标图第二个参数是轮廓向量第三个参数contourIdx指定画第几个轮廓。contourIdx的取值有讲究传入-1表示绘制所有轮廓传入具体索引则只画那一个。画全部轮廓时通常把thickness设为2或3线条太细看不清楚太粗会把相邻轮廓黏在一起。颜色用Scalar类型在8UC3图像上按BGR顺序排列。maxLevel参数控制绘制深度很多人忽略它。默认值是INT_MAX效果是绘制所有层级的轮廓。如果设成0只绘制指定索引的那一层轮廓设成1绘制该轮廓及其后一个轮廓值为负数时不画同级轮廓按层级升序绘制直到abs(max_level)-1的子轮廓。做调试时我喜欢用maxLevel0配合单个索引只高亮感兴趣的目标其余轮廓不显示画面干净很多。注意drawContours的绘制顺序后画的轮廓会覆盖先画的。如果所有轮廓用同一种颜色嵌套轮廓的渲染结果看不出层次建议对每个轮廓用不同颜色。这也是示例代码里用随机颜色的原因。3.2 只画指定轮廓通过hierarchy索引定位目标轮廓hierarchy在找轮廓时是输出参数在画轮廓时就变成了查找关系的数据源。比如我用RETR_TREE模式得到一棵树想只画第5个轮廓内部的孔洞轮廓需要先拿到第5个轮廓在hierarchy中的记录找到其内嵌轮廓的索引再传给drawContours。hierarchy[i][2]表示第i个轮廓的父轮廓索引hierarchy[i][3]表示内嵌轮廓索引。定位目标轮廓的常见代码逻辑是遍历hierarchy找出父轮廓索引等于目标索引的所有子轮廓。for (int i 0; i contours.size(); i) { if (hierarchy[i][3] targetIndex) { drawContours(dst, contours, i, Scalar(0, 255, 0), 2, 8, hierarchy, 0, Point(0, 0)); } }这段代码检查每个轮廓的内嵌索引是否等于目标索引等于说明这个轮廓是目标轮廓的子轮廓把它画成绿色。maxLevel传0只画这一层不拖出更深层的轮廓。这个写法在提取零件内部特征时很实用。3.3 绘制调试技巧随机颜色、线宽与offset的作用调试轮廓提取时随机颜色是快速定位问题的好办法。示例代码里用RNG生成随机Scalar每个轮廓一个颜色这样一眼就能看出轮廓是否断裂、是否错连。如果多个轮廓被画成同一颜色错连的轮廓很难被发现。offset参数在ROI场景下特别重要。比如我先在图像上半部分截取一个ROI做轮廓提取ROI内部的坐标是相对于ROI左上角的直接画回原图就会位置错乱。这时候把ROI的偏移量传给findContours或drawContours轮廓点坐标会自动加上偏移画出来的位置就和原图对上了。更常见的做法是提取时不加偏移绘制时用offset修正两个函数都支持这个参数用哪个取决于你后续要不要用原始坐标做计算。4. 完整示例把Canny阈值、findContours、drawContours串起来跑通4.1 代码骨架滑动条调阈值回调函数里完成整条链路这里给出一份可以直接编译运行的完整代码通过滑动条动态调整Canny阈值每次调整都会触发回调函数在回调里完成轮廓提取和绘制。代码基于示例工程整理补上了图像读取失败的保护判断。#include opencv2/opencv.hpp using namespace cv; using namespace std; int value 50; Mat src, dst, canny_img; void callback(int, void*); int main(int argc, char** argv) { src imread(2.jpg); if (src.empty()) { printf(image load failed\n); return -1; } namedWindow(src, CV_WINDOW_AUTOSIZE); imshow(src, src); cvtColor(src, src, CV_BGR2GRAY); namedWindow(output, CV_WINDOW_AUTOSIZE); createTrackbar(threshold, output, value, 255, callback); callback(0, 0); waitKey(0); return 0; } void callback(int, void*) { Canny(src, canny_img, value, 2 * value); imshow(canny, canny_img); vectorvectorPoint contours; vectorVec4i hierarchy; findContours(canny_img, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE, Point(0, 0)); dst Mat::zeros(src.size(), CV_8UC3); RNG rng(1); for (int i 0; i contours.size(); i) { Scalar color Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)); drawContours(dst, contours, i, color, 2, 8, hierarchy, 0, Point(0, 0)); } imshow(output, dst); }主函数前半段完成图像读取和窗口初始化imshow(src, src)显示彩色原图紧接着cvtColor把src原地转换成灰度图。注意这个顺序一旦转换完成src变量本身就不再是彩色图了。createTrackbar在output窗口上创建名为threshold的滑动条范围0到255初值50回调函数是callback。callback(0,0)手动调一次保证滑动条还没被拖动时画面就有输出。回调函数是整套流程的核心。先用当前value值做Canny双阈值检测value是低阈值2*value是高阈值再把边缘图传给findContoursmode用RETR_EXTERNALmethod用CHAIN_APPROX_SIMPLE然后新建一张全黑的8UC3三通道画布dst用RNG生成随机颜色逐个绘制轮廓最后在output窗口显示结果。4.2 滑动条参数的作用value和2*value的联动逻辑滑动条的value值直接影响边缘检测的敏感度。value越小Canny的低阈值和高阈值都越低图像里更多细微的梯度变化会被标记为边缘轮廓数量变多噪声轮廓也会出现。value越大阈值越高边缘越少轮廓可能断裂或消失。2*value这个倍数关系不是OpenCV硬性规定的是经验值。有些项目会用value和value/2搭配或者分别用两个滑动条控制低阈值和高阈值。这个示例选2倍是为了减少滑动条数量操作时只调一个参数就能同时改变两个阈值。实际项目中如果目标边缘和背景对比度差异大可以降低倍数让高阈值不那么高保留更多边缘如果图像噪声严重提高倍数能有效抑制弱边缘。4.3 三个窗口对照观察canny窗口和output窗口的对应关系运行后会出现三个窗口src显示原始灰度图canny显示Canny边缘检测结果output显示最终绘制的轮廓。调滑动条时canny窗口和output窗口同时刷新方便对比边缘图和轮廓图的关系。canny窗口中白色像素就是findContours的输入output窗口中的彩色线条就是findContours的输出。如果output里某个轮廓明显比canny的白色边缘少说明SIMPLE近似把中间点压缩掉了属正常现象如果output里出现了canny窗口不存在的闭合圈说明Canny边缘在某处闭合了原本断开的边缘之间被低阈值连上了。观察这两个窗口的差异能快速判断是阈值问题还是mode选择问题。dst Mat::zeros(src.size(), CV_8UC3);这行创建与图像同尺寸的黑色画布CV_8UC3表示8位三通道用来显示彩色轮廓。这里有一个容易忽略的细节dst的尺寸必须和原图一致否则轮廓坐标会超出画布范围绘制时被自动裁剪看起来像是轮廓丢失了。5. 轮廓提取避坑与常见问题排查5.1 现象findContours报错或返回空轮廓代码运行直接崩溃提示source image is empty或者返回的contours容量为0。原因是输入图像路径不对、读取失败或者传给findContours的图像不是单通道二值图。很多人在灰度图上直接调用findContoursOpenCV虽然不报错但提取结果完全是乱的。解决方法是先判断src.empty()再确认图像经过cvtColor转到灰度、经过threshold或Canny转成二值图最后检查Canny输出窗口中是否有连续白色区域。没有白色区域就没有轮廓先排查边缘检测这一步。5.2 现象轮廓画出来是一坨实心色块output窗口里轮廓不是线条而是一个个填充块或者轮廓内部被大面积涂色。原因是Canny的低阈值设得太低Canny把大量非边缘像素判定为边缘白色区域连成大片findContours把整片白色区域的外边界当成轮廓drawContours用细线描绘时密集的轮廓点互相黏连看起来就像实心。解决方法是调高value值观察canny窗口直到边缘呈细线状而不是面状。另一个常见原因是drawContours的thickness设置成负数负数表示填充轮廓内部把它改成正整数即可。5.3 现象轮廓数量比预期多得多数出来有几十个轮廓但图像里明显只有三五个物体。主要原因有三个。一是Canny阈值过低图像纹理和噪声被当成边缘每个噪声点都形成微型轮廓二是mode选成了RETR_LIST或RETR_TREE把所有内部轮廓也计算进来了比如一个物体上有文字、条纹每个元素都是独立轮廓三是SIMPLE模式下应该用RETR_EXTERNAL只取最外层。排查时先把mode改成RETR_EXTERNAL再看canny窗口确认边缘的干净程度最后用第6章提到的contourArea过滤面积过小的轮廓。5.4 现象轮廓位置和物体对不上整体偏移轮廓形状是对的但画在原图上时位置往右下方偏了一段距离。原因是findContours的输入图像是ROI裁剪出来的轮廓坐标是相对于ROI的绘制时没有把ROI在原图中的坐标补偿回来。解决方法是给findContours或drawContours传offset参数比如ROI左上角在原图中的坐标是(100, 50)则传Point(100, 50)。我一般习惯在findContours阶段就传offset这样contours里保存的就是绝对坐标后续做面积计算、外接矩形都不需要再手动加偏移省去很多麻烦。5.5 现象同一个目标检测出内外两层轮廓重复计数统计物体数量时一个圆环形零件被数成两个。原因是使用了RETR_TREE模式圆环的外轮廓和内孔轮廓都被算进来了。如果目标统计只关心最外层mode改成RETR_EXTERNAL就能解决。如果确实需要内孔轮廓做后续处理就在循环绘制或计数时判断hierarchy[i][3]也就是父轮廓索引是否为-1只有父轮廓为-1的才是顶级轮廓用这个条件过滤后再计数。5.6 现象src变量被转成灰度后原图不见了代码跑完src窗口一开始显示彩色图后面再想用src做别的处理发现它已经变成灰度图。原因是cvtColor的第三个参数是CV_BGR2GRAY时如果输入输出用同一个Mat变量它会原地覆盖。示例代码里先imshow再cvtColor所以第一次显示没问题但之后src就一直是灰度图。如果代码中还需要彩色原图做其他处理用一个独立的Mat变量保存灰度结果比如Mat gray; cvtColor(src, gray, CV_BGR2GRAY);。这是一个很容易被忽略的细节尤其在你把示例代码改成自己的项目时很容易因为这个踩坑导致后续彩色信息拿不到。6. 进阶用面积过滤和层级关系把轮廓变成目标数据6.1 用轮廓面积过滤掉小噪声Canny阈值再调也有漏网之鱼画面角落的噪点、细小的纹理都会生成不想要的轮廓。最常见的做法是用contourArea计算每个轮廓的面积把面积小于阈值的轮廓直接跳过。for (int i 0; i contours.size(); i) { double area contourArea(contours[i]); if (area 500) continue; drawContours(dst, contours, i, Scalar(0, 255, 0), 2); }contourArea计算的是轮廓包围区域的像素面积。面积阈值设多少取决于图像分辨率和目标大小。我的经验是先随便设一个值运行后打印所有轮廓的面积观察分布再确定阈值而不是上来就拍脑袋定500。打印轮廓面积用printf(area: %.2f\n, contourArea(contours[i]))一目了然。6.2 用外接矩形快速框出目标轮廓点太密直接拿去画矩形会很不规则。实际项目里更常用boundingRect拿到轮廓的轴对齐外接矩形用这个矩形做后续的裁剪或标注。对于倾斜的目标用minAreaRect拿最小外接矩形结果带旋转角度更适合做方向判断。Rect rect boundingRect(contours[i]); rectangle(dst, rect, Scalar(0, 0, 255), 2);boundingRect返回一个Rect包含x、y、width、height四个值可以直接传给rectangle绘制。做目标检测前处理时我会先用轮廓面积过滤噪声再用boundingRect提取每个目标的矩形区域最后把这些矩形区域从原图裁剪出来做分类整个过程只需要几行代码。6.3 用hierarchy提取父轮廓下的子轮廓RETR_TREE模式下hierarchy记录了完整的嵌套关系。统计一个容器内的零件数量可以先找到容器的最外轮廓再遍历hierarchy找出所有父轮廓索引等于该外轮廓的子轮廓这些子轮廓就是容器内的目标。从那以后我每次写轮廓提取代码都会强制走一遍完整流程先确认输入是干净的二值图再根据是否需要嵌套关系决定mode最后用面积过滤一遍轮廓列表。顺序对了轮廓提取这件事就很少翻车。希望帮到你。本文还有配套的精品资源点击获取
返回列表