OpenCV—轮廓操作一站式详解:查找/筛选/绘制/形状描述与重心标注(C 版)

视觉/图像重磅干货,第一时间送达

新机器视觉 最前沿的机器视觉与计算机视觉技术206篇原创内容公众号转自:https://blog.csdn.net/iracer/article/details/90260670轮廓是定义或限定形状或对象的边或线,是机器视觉中的常用的概念,多用于目标检测、识别等任务。关于OpenCV轮廓操作,尤其是级别及如何使用轮廓级别进行筛选等问题,相关文章比较少,正好最近用到,因此将其总结成文。本文主要介绍OpenCV的查找轮廓函数findContours()绘制函数drawContours(),及其轮廓级别参数hierarchy,涉及到预处理、轮廓筛选等内容,并提供全部源代码,希望能帮助大家理解基本概念并能借鉴示例代码编写自己的算法。本文代码:C++本文包括如下内容:基本概念1.查找和绘制轮廓函数findContous(),drawContours()2.轮廓参数:轮廓级别、轮廓长度3.轮廓的形状描述子:最小覆盖矩形、圆、多边形逼近、凸包编程实战1.如何筛选轮廓:按轮廓级别和长度筛选2.如何绘制轮廓的外接形状3.如何获取轮廓的重心坐标并标注本文的目标:1.从原始图像中找到2架可回收火箭2.标注目标的位置与重心坐标阅读完成后,将能从原始图像中找到2架火箭,并标注其位置与坐标。如下图所示:

目录OpenCV轮廓操作一站式详解:查找/筛选/绘制/形状描述与标注1.查找、绘制轮廓函数findContours()drawContours()2.预处理3.查找轮廓4.绘制轮廓5.筛选轮廓5.1 hierarchy轮廓级别详解contours与hierarchy的关系什么是层次结构hierarchy?5.2 OpenCV中的层次结构表示NextPreviousFirst_ChildParent5.3 按hierarchy筛选轮廓5.4 按长度筛选轮廓6.联通域分析7.标注轮廓重心1.查找、绘制轮廓函数findContours()void cv::findContours     (InputOutputArray         image,OutputArrayOfArrays   contours,OutputArray                 hierarchy,int                                 mode,int                                 method,Point                             offset = Point())函数参数:image输入:源图像,一个8位单通道图像,注意一定是CV_8UC1的单通道图像,否则报错。非零像素被视为1。零像素保持为0,因此图像被视为二进制。可以使用compare,inRange,threshold,adaptiveThreshold,Canny等来从灰度或彩色图像中创建二进制图像。如果mode为RETR_CCOMP或RETR_FLOODFILL,则输入也可以是标签的32位整数图像(CV_32SC1)。contours输出:检测到的轮廓。每个轮廓都存储为点向量(例如std :: vector <std :: vector <cv :: Point >>)。即由若干个cv::Point类型的点组成了单个轮廓std :: vector <cv :: Point >,再由若干个轮廓组成输入图像中的全部轮廓std::vector<std :: vector <cv :: Point >>hierarchy输出:轮廓级别信息。Hierarchy为可选输出变量,是std::vector<cv::Vec4i>类型的向量(每个元素都是一个4个int值构成的向量)。包含有关图像拓扑的信息。它具有与轮廓数量一样多的元素。例如,第i个轮廓, hierarchy[i][0],hierarchy[i][1],hierarchy[i][2]和hierarchy[i][3]依次为:第i个轮廓的[Next, Previous, First_Child, Parent],即轮廓i相同等级的下一轮廓、前一轮廓,第一个子轮廓和父轮廓(上一级轮廓)的索引号(即contours向量中的轮廓序号)。如果轮廓i没有下一个,前一个,父级或嵌套轮廓,则层次结构[i]的相应元素将为负数。这个参数我们将在下文中重点介绍。mode输入:轮廓检索模式, 详见 RetrievalModesmethod输入:轮廓近似法, 详见ContourApproximationModesoffset输入:每个轮廓点移动的偏移量,可选参数,cv::Point()类型。如果从整幅图像的某个ROI中提取轮廓,然后又在整个图像中分析轮廓(将ROI中的轮廓坐标恢复到整幅图像中的坐标),这个偏移量非常有用,可以免去我们自己写代码转换坐标系的麻烦。mode参数:RETR_EXTERNALPython: cv.RETR_EXTERNAL仅检索极端外轮廓。它为所有轮廓设置hierarchy [i][2] = hierarchy [i][3] = - 1。RETR_LISTPython: cv.RETR_LIST检索所有轮廓而不建立任何层次关系。RETR_CCOMPPython: cv.RETR_CCOMP检索所有轮廓并将它们组织成两级层次结构。在顶层轮廓是外部轮廓。在第二层轮廓是“洞”的轮廓。如果连接组件的洞内有另一个轮廓,它的级别仍然认定为顶层。RETR_TREEPython: cv.RETR_TREE检索所有轮廓并重建嵌套轮廓的完整层次结构。RETR_FLOODFILLPython: cv.RETR_FLOODFILLdrawContours()绘制轮廓轮廓或填充轮廓。void cv::drawContours     (InputOutputArray       image,InputArrayOfArrays   contours,int                               contourIdx,const Scalar &            color,int                               thickness = 1,int                               lineType = LINE_8,InputArray                  hierarchy = noArray(),int                               maxLevel = INT_MAX,Point                           offset = Point())函数参数:image输入:源图像。单通道或3通道图像。contours输入:待绘制的轮廓。std :: vector <std :: vector <cv :: Point >>类型。contourIdx输入:待绘制的轮廓序号。例如:0为绘制第1个轮廓contours[0];1为绘制第2个轮廓contours[1],依次类推;-1为绘制所有轮廓。color输入:轮廓颜色。cv::Scalar变量,例如:cv::Scalar(0,0,255)为红色轮廓,cv::Scalar::all(0)为黑色轮廓thickness输入:轮廓粗细。int型变量,默认为1,值越大越粗lineType输入:绘制轮廓的线型。默认LINE_8,8联通线型(下一个点连接上一个点的边或角)。hierarchy输入:待绘制的轮廓级别。std::vector<cv::Vec4i>类型的向量(每个元素都是一个4个int值构成的向量)。下一轮廓、前一轮廓,第一个子轮廓和父轮廓(上一级轮廓)的索引号。maxLevel输入:待绘制的轮廓最大级别。method输入:轮廓近似法, 详见ContourApproximationModesoffset输入:每个轮廓点移动的偏移量,可选参数。2.预处理预处理目的是为轮廓查找提供高质量的输入源图像。预处理的主要步骤包括:灰度化:使用cv::cvtColor()图像去噪:使用高斯滤波cv::Gaussian()二值化:使用cv::Threshold()形态学处理:cv::morphologyEx()其中灰度化可以将3通道图像转化为单通道图像,以便进行二值化门限分割;去噪可以有效剔除图像中的异常独立噪点;二值化是为轮廓查找函数提供单通道图像;形态学的某些处理通常可以剔除细小轮廓,联通断裂的轮廓。读取图像代码如下:// 1.载入图像cv::Mat image = cv::imread('spaceX2.jpg',1);cv::imshow('original', image);cv::waitKey();// 2.预处理cv::Mat gray, binary, element; // 临时变量cv::cvtColor(image, gray, CV_BGR2GRAY);cv::imshow('gray', gray);GaussianBlur(gray, gray, Size(3, 3), 1);cv::threshold(gray, binary, 80, 255, CV_THRESH_BINARY_INV);cv::imshow('binary', binary);element = getStructuringElement(MORPH_RECT, Size(3, 3));//33全1结构元素cv::morphologyEx(binary, binary, cv::MORPH_CLOSE, element);cv::imshow('morphology', binary);cv::waitKey();

使用findContours函数查找轮廓// 3.查找轮廓std::vector<std::vector<cv::Point>> contours;std::vector<cv::Vec4i> hierarchy;cv::findContours(binary, // 输入二值图contours, // 存储轮廓的向量hierarchy, // 轮廓层次信息RETR_TREE, // 检索所有轮廓并重建嵌套轮廓的完整层次结构CHAIN_APPROX_NONE); // 每个轮廓的全部像素printf('find %d contours', contours.size());4.绘制轮廓为了方便查看轮廓查找结果,使用drawContours()来绘制轮廓并显示。编写函数drawMyContours()函数用于在白色背景中或者原图上查看轮廓。函数参数:窗口名字;原始图像以及轮廓变量白色背景上还是在原图上绘制轮廓的标志位// 4. 绘制轮廓函数void drawMyContours(string winName, Mat &image, std::vector<std::vector<cv::Point>> contours, bool draw_on_blank){cv::Mat temp;if (draw_on_blank) // 在白底上绘制轮廓{temp = cv::Mat(image.size(), CV_8U, cv::Scalar(255));cv::drawContours(temp,contours,-1,//画全部轮廓0, //用黑色画2);//宽度为2}else // 在原图上绘制轮廓{temp = image.clone();cv::drawContours(temp,contours,-1,//画全部轮廓cv::Scalar(0,0,255), //用red画1);//宽度为2}cv::imshow(winName, temp);cv::waitKey();}在main函数中调用drawMyContours()如下:// 4.绘制原始轮廓drawMyContours('contours', image, contours, true);首次查找的轮廓变量contours中有16个向量,即找到16个轮廓。

展开contours变量,可以看到每个元素都是一个由一系列轮廓上的点组成的向量,其size就是每个轮廓的长度。

通过绘制轮廓,可以看到这16个轮廓,除了两个目标之外,还有云、地面背景以其中的“洞”轮廓。

5.筛选轮廓查找到大轮廓显然有许多不符合要求,因此可以通过某些准则进行轮廓筛选。通过观察,发现上图中有许多轮廓包含“洞”,即子轮廓,而这些子轮廓显然也有父级轮廓,因此我们可以使用findContours的hierarchy轮廓级别参数删除那些有子轮廓也有父轮廓的轮廓。5.1 hierarchy轮廓级别详解contours与hierarchy的关系使用findContours()函数将返回contours轮廓向量以及对应的hierarchy轮廓级别向量(可选项)。两者有相同的长度即contours.size() = hierarchy.size(),并且向量的序号表示找到的轮廓索引,且一一对应。什么是层次结构hierarchy?通常我们使用findContours()函数来检测图像中的对象。有时对象位于不同的位置。但在某些情况下,某些形状在其他形状内(类似嵌套)。在这种情况下,我们将外部轮廓称为父级轮廓,将内部轮廓称为子轮廓。这样,图像中的轮廓彼此之间存在某种关系。并且我们可以指定一个轮廓如何相互连接,例如,它是某个其他轮廓的子项,还是父项等。此关系的表示就称为层次结构hierarchy。考虑下面的示例图片:

在上图中,有一些形状,我们从0-5编号这5个形状。图中2和2a表示最外侧矩形的外部和内部轮廓。轮廓0,1,2是最外部轮廓,三者为同一级别。我们可以说,它们在层次结构0中,或者只是它们处于相同的层次结构级别。接下来是轮廓-2a可以被认为是轮廓-2的子轮廓(或者相反,轮廓-2是轮廓-2a的父级轮廓)所以让它在层次结构-1中。类似地,轮廓-3是轮廓-2a的子轮廓,它进入下一层次。最后,轮廓4,5是轮廓-3a的子轮廓,它们位于最后的层次结构级别。从编号框的方式,可以说轮廓-4是轮廓-3a的第一个子轮廓,当然轮廓-5也是轮廓-3a的子轮廓。如果上面的描述看着头晕,不要紧,一开始都这样。5.2 OpenCV中的层次结构表示OpenCV中每个轮廓都有自己的信息,关于它是什么层次结构,谁是它的子轮廓,谁是它的父轮廓等.OpenCV将它表示为四个int值的数组,类型为cv::Vec4i(4个int值):[Next,Previous,First_Child,Parent]NextNext表示同一级别的下一个轮廓索引。例如,在我们的图片中取出轮廓-0。同一水平的下一个轮廓是轮廓-1。所以简单地说Next = 1。类似地,对于轮廓-1,next是轮廓-2。所以Next = 2。轮廓-2的同一级别没有下一个轮廓,所以轮廓-2的Next = -1。轮廓-4呢?它与轮廓-5处于同一水平。所以它的下一个轮廓是轮廓-5,所以轮廓-4的Next = 5。PreviousPrevious表示同一级别的上一个轮廓索引。例如,轮廓-1的上一个轮廓在同一级别中为轮廓-0。类似地,对于轮廓-2,它的上一个轮廓是轮廓-1。而对于轮廓-0,没有先前的,所以把它的Previous = -1。First_ChildFirst_Child表示当前轮廓的第一个子轮廓索引。例如,对于轮廓-2,子轮廓是轮廓-2a。因此轮廓-2的First_Child为轮廓-2a的相应索引值。轮廓-3a呢?它有两个子轮廓。但hierarchy参数只记录第一个子轮廓,因此它是轮廓-4的索引值。因此,对于轮廓-3a,First_Child = 4。ParentParent表示当前轮廓的父轮廓索引。对于轮廓-4和轮廓-5,它们的父轮廓都是轮廓-3a。对于轮廓-3a,它的父轮廓是轮廓-3,依此类推。注意:Previous表示同一层级的前一个轮廓的索引;Parent表示其父轮廓的索引;如果某个轮廓没有子轮廓项或父轮廓,则对应的字段=-1.5.3 按hierarchy筛选轮廓有了上述对轮廓层级的理解,下面就可以根据需要筛选轮廓了。例如本文任务是找到两个火箭,而首次查找轮廓有许多中间有空洞的轮廓不符合要求,下面就通过遍历每一个轮廓的hierarchy级别参数的第3第4个参数来找到那些有子轮廓或者有父轮廓的轮廓,并删除之。注意向量迭代器的使用,删除后会返回下一个向量的指针;此外,contours与hierarchy元素需要同步删除和并递增迭代器,以保持编号对应关系,否则会删错。// 5.筛选轮廓// 初始化迭代器std::vector<std::vector<cv::Point>>::iterator itc = contours.begin();std::vector<cv::Vec4i>::iterator itc_hierarchy = hierarchy.begin();// 5.1使用层级结构筛选轮廓int i = 0;while(itc_hierarchy != hierarchy.end()){//验证轮廓大小if (hierarchy[i][2] > 0 || hierarchy[i][3] > 0) // 存在子轮廓/父轮廓{itc = contours.erase(itc);itc_hierarchy = hierarchy.erase(itc_hierarchy);}else{++i;++itc;++itc_hierarchy;}}printf('%d contours remaining after hierarchy filtering', contours.size());// 绘制级别筛选后的轮廓drawMyContours('contours after hierarchy filtering', image, contours, true);筛选过后的轮廓如下图所示,左上角和右下角的云层与地面的带有空洞的轮廓都被删除了。

5.4 按长度筛选轮廓尽管上一个步骤已经剔除了天空与地面背景轮廓,但仍然残留这一些细小的轮廓。下一步可以使用轮廓长度contours[i].size()来滤除过小或过大的轮廓:// 5.2使用轮廓长度滤波int min_size = 20;int max_size = 500;// 针对所有轮廓itc = contours.begin();itc_hierarchy = hierarchy.begin();while (itc != contours.end()){//验证轮廓大小if (itc->size() < min_size || itc->size() > max_size){itc = contours.erase(itc);itc_hierarchy = hierarchy.erase(itc_hierarchy);}else{++itc;++itc_hierarchy;}}printf('%d contours remaining after length filtering', contours.size());// 绘制长度筛选后的轮廓drawMyContours('contours after length filtering', image, contours, true);

感觉越来越接近真理了!6.联通域分析连通区域通常代表了场景中的某个物体。为了识别该物体,或将它与其他图像元素比较,需要对此区域进行测量,以提取部分特征。本节介绍opencv的形状描述子,用于描述连通区域的形状。OpenCV中用于形状描述的函数有很多。我们把其中几个用到上节提取到的区域。(1)矩形框cv::Rect r0 = cv::boundingRect()在表示和定位图像中的区域方法中,边界框可能是最简洁的。它的定义是:能完整包含该形状的最小垂直矩形。比较边界框的高度和宽度,可以获得物体在垂直和水平方向上的范围。(2)最小覆盖圆cv::minEnclosingCircle()最小覆盖圆通常用在只需要区域尺寸和位置的近似值的情况。(3)多边形逼近cv::approxPolyDP()如果要更紧凑地表示区域的形状,可以采用多边形逼近。在创建时需要设置精度参数,表示形状与对应的简化多边形之间能接受的最大距离。它是cv::approxPolyDP(contours[1],poly,5,true)函数的第四个参数。返回结果是cv::Point类型的向量,表示多边形顶点个数。在画这个多边形时,要迭代遍历整个向量,并在顶点之间画直线,把它们逐个连接起来。(4)凸包cv::convexHull()凸包是包含该形状的最小凸多边形。可以把它看作一条绕在区域周围的橡皮筋。在形状轮廓中凹进去的位置,凸包轮廓会与原始轮廓发生偏离。下面使用上述分析方法中的凸包与最小覆盖矩形两种方法分别对2架火箭的提取轮廓进行分析。// 6.形状描述子// 最小覆盖矩形cv::Mat result =image.clone();cv::Rect rect = cv::boundingRect(contours[0]);//轮廓1cv::rectangle(result, rect, cv::Scalar(0,255,255), 1);//画矩形// 凸包std::vector<cv::Point> hull;cv::convexHull(contours[1], hull);//轮廓2cv::polylines(result, hull, true, cv::Scalar(0, 255, 0), 1);//画多边形cv::imshow('bounding', result);cv::waitKey();得到的结果如下图所示:轮廓1为最小覆盖矩形(黄色线条),轮廓2为凸包(绿色线条)

7.标注轮廓重心终于到最后一步了。下面先求2个轮廓的重心,然后使用cv::Circle()与cv::putText()函数将重心位置与坐标标注到画面上。// 7.计算轮廓矩,画重心itc = contours.begin();while (itc != contours.end()) {// 计算全部轮廓矩cv::Moments mom = cv::moments(cv::Mat(itc++));// 画重心cv::Point pt = cv::Point(mom.m10 / mom.m00, mom.m01 / mom.m00); //使用前三个矩m00, m01和m10计算重心cv::circle(result, pt, 2, cv::Scalar(0, 0, 255), 2);//画红点// 标注重心坐标值string text_x = std::to_string(pt.x);string text_y = std::to_string(pt.y);string text = '(' + text_x + ', ' + text_y + ')';cv::putText(result, text, cv::Point(pt.x+10,pt.y+10), cv::FONT_HERSHEY_PLAIN, 1.5, cv::Scalar::all(255), 1, 8, 0);}cv::imshow('center', result);cv::waitKey();最终结果如下图所示。

参考链接:https://docs.opencv.org/3.1.0/d9/d8b/tutorial_py_contours_hierarchy.htmlhttps://docs.opencv.org/3.4.1/d3/dc0/groupimgprocshape.html#ga17ed9f5d79ae97bd4c7cf18403e1689a

(0)

相关推荐

  • 基于OpenCV实战:绘制图像轮廓(附代码)

    重磅干货,第一时间送达 山区和地形图中海拔高的区域划出的线称为地形轮廓,它们提供了地形的高程图.这些线条可以手动绘制,也可以由计算机生成.在本文中,我们将看到如何使用OpenCV在简单图像上绘制轮廓线 ...

  • 图像特征之傅里叶描述子

    使用C++.opencv获取轮廓的傅里叶描述子 傅里叶描述子是一种图像特征,具体来说,是一个用来描述轮廓的特征参数.其基本思想是用物体边界信息的傅里叶变换作为形状特征,将轮廓特征从空间域变换到频域内, ...

  • opencv笔记(二十九)——提取轮廓相关函数使用方法

    opencv中常用的跟轮廓相关的操作有:findContours()查找轮廓:drawContours()画轮廓:轮廓填充:计算轮廓的面积和周长:提取轮廓凸包,矩形,最小外接矩形,外接圆等.它们都有相 ...

  • opencv3/C++轮廓的提取与筛选

        版权声明:本文为博主原创文章. https://blog.csdn.net/akadiao/article/details/78843773 <div class="markd ...

  • (11条消息) opencv学习笔记十四:使用cv2.findContours()和cv2.drawContours()实现轮廓检测

    一.cv2.findContours()函数 contours, hierarchy=cv2.findContours(image, mode, method[, contours[, hierarc ...

  • 【从零学习OpenCV 4】轮廓发现与绘制

    重磅干货,第一时间送达 经过几个月的努力,小白终于完成了市面上第一本OpenCV 4入门书籍<OpenCV 4开发详解>.为了更让小伙伴更早的了解最新版的OpenCV 4,小白与出版社沟通 ...

  • findContours()函数

    函数原型 findContours(InputOutputArray image, OutputArrayOfArrays contours, OutputArray hierarchy, int m ...

  • OpenCV C++ 简单小技巧

    查找和绘制轮廓findContours 会找到vector<vector<cv::Point>> contours;vector<Vec4i> hierarchy; ...

  • OpenCV学习28

    查找轮廓 什么是轮廓:一个轮廓是由图像中的一系列点组成的,也就是图像中的一条曲线.在OpenCV中一般用序列来存储轮廓信息.序列中的每个元素是曲线中每个点的位置. 关于序列:序列是内存存储器中可以存储 ...

  • OPENCV之寻找并绘制轮廓以及提取轮廓重心坐标

    OPENCV之寻找并绘制轮廓以及提取轮廓重心坐标 1.寻找轮廓 声明:在寻找图像轮廓之前需要对图像进行阈值分割或者Canny.拉普拉斯等边缘检测算子处理. 寻找轮廓的算子: findContours( ...