美文网首页
《Learning OpenCV3》笔记——轮廓(一)

《Learning OpenCV3》笔记——轮廓(一)

作者: 周安陌 | 来源:发表于2020-03-24 18:52 被阅读0次

第14章 轮廓

Canny之类的边缘检测算法可以根据像素间差异检测出轮廓边界的像素,但它未将轮廓作为一个整体进行处理。
OpenCV提供了寻找轮廓的函数cv::findContours()

1. 轮廓查找

一个轮廓对应一系列点,这些点以某种方式表示图像中的一条曲线。

在OpenCV中采用向量vector<>进行表示,向量中的每一个值都包含轮廓上下一个点的位置信息,常见的有vector<cv::point>vector<cv::point2f>

cv::findContours()用于计算二维图像轮廓,图像先经过相关预处理(边缘检测、阈值处理等)

1.1 轮廓层次

轮廓是什么?一组轮廓之间如何相互关联?下面通过轮廓树的概念进行理解,如图:

一张输入cv::findContours()函数的测试图像(左图)。图中有五块颜色区域(分别标记为A,B,C,D,E),每块区域的外部边界和内部边界都各自组成轮廓。因此共有9条轮廓。每条轮廓都由一组输出列表表示(右上角图——轮廓参数)。也可以选择生成一组层次表达(右下角图——层次参数)。在右下角的图中(对应构筑的轮廓树),每个节点就是一条轮廓。根据每个节点在层次队列中的四元数组索引,图中的链接都做了相应标记。

其中,c表示“轮廓”(contour),h表示“孔”(hole),X表示数字。

OpenCV用数组(通常为vector)来表示轮廓树,其中每个值都代表一条特定轮廓(包含一个四整数组,通常表示为cv::Vec4i)。它们之间的层次关系如下表:

索引 含义
0 同级的下一条轮廓
1 同级的前一条轮廓
2 下级的第一个子节点
3 上级的父节点

当不存在某种联系时,该联系的值设为-1。

对于cv::findContours()函数,一条“边缘”只是一块非常窄的“白色区域”。对于每一条外部轮廓,总有一条几乎完全相同的孔轮廓。这个孔轮廓就是外部边界的内部区域,它是一个过渡区域,也表示边缘曲线的内部边界

cv::findContours()函数(OpenCV文档查阅):

void cv::findContours(
  cv::InputOutputArray  image,      // Input "binary" 8-bit single channel
  cv::OutputArrayOfArrays  contours,  // Vector of vectors or points
  cv::OutputArray  hierarchy,  // (optional) topology information
  int  mode,  // Contour retrieval mode  (#way_in_which_the_tree_node_variables_are)
  int method,  // Approximation method
  cv::Point  offset = cv::Point()  // (optional) Offset every point
);

void cv::findContours(
  cv::InputOutputArray  image,      // Input "binary" 8-bit single channel
  cv::OutputArrayOfArrays  contours,  // Vector of vectors or points
  int  mode,  // Contour retrieval mode  (#way_in_which_the_tree_node_variables_are)
  int method,  // Approximation method
  cv::Point  offset = cv::Point()  // (optional) Offset every point
);

参数解释:
image:输入图像,图像为8位单通道二值图像,处理时会被更改,建议传入前先clone。
contours:找到的轮廓,表示为一组数组(如vector),contours[i]是一条特定轮廓,contours[i][j]contours[i]中的一个点。

vector<vector<Point>> contours;

hierarchy:轮廓的树结构,表示轮廓间的层次关系,如前表所示。(是一个可选项)

vector<Vec4i> hierarchy;

mode:轮廓的提取方式,有四种可选方式:

  • cv::RETR_EXTERNAL
    只检索最外层轮廓。在前面提到的轮廓树对应图像中,只存在一条最外层轮廓,它的结果如下图所示。
  • cv::RETR_LIST
    检索所有轮廓并保存到表(List)中(一般不采用此项:以前检测的轮廓未自动排序为一个列表,但是现在的vector<>已实现此功能)。在前面提到的轮廓树对应图像中找到了9条轮廓,结果如下图所示。
  • cv::RETR_CCOMP
    检索所有的轮廓,并将它们组织成双层结构,顶层边界是所有成分的外部边界,第二层边界是孔的边界。在前面提到的轮廓树对应图像中找到了9条轮廓,结果如下图所示。
  • cv::RETR_TREE
    检索所有轮廓并重新建立网状轮廓结构。如之前轮廓树概念所介绍,其结果如下图。
    不同轮廓提取方式结果对比

method:轮廓的表示,有以下几种方式:

  • cv::CHAIN_APPROX_NONE
    将轮廓编码中的所有点转换为点保存,每个点为前一个点的8邻点之一。
  • CHAIN_APPROX_SIMPLE
    压缩水平、垂直、对角点,只保留其末端点。可大大减少返回点数。如对于沿着x-y方向的矩形,只返回4个点。
  • cv::CHAIN_APPROX_TC89_L1cv::CHAIN_APPROX_TC89_KCOS
    使用Teh-Chin链逼近算法中的一个,该算法不需要调节参数。
    算法细节见论文:Teh, C.H. and Chin, R.T., On the Detection of Dominant Points on Digital Curve. PAMI 11 8, pp 859-872 (1989)

offset:(可选项)使返回的轮廓中所有点发生偏移(跟坐标系表达有关)。

1.2 绘制轮廓

OpenCV中用cv::drawContours()函数实现轮廓绘制。
cv::drawContours()函数(OpenCV文档查阅

  void cv::drawContours(
    cv::InputOutputArray  image,  // Will draw on input image
    cv::InputArrayOfArrays  contours,  // Vector of vectors or points
    int  contourIdx,  //Contour to draw (-1 is "all")
    const  cv::Scalar&  color,  // Color for contours
    int  thickness = 1,  // Thickness for contour lines
    int  lineType = 8,  // Connectedness ('4' or '8')
    cv::InputArray  hierarchy = noArray(),  // optional (from findContours)
    int  maxLevel = INT_MAX,  // Max descent in hierarchy
    cv::Point  offset = cv::Point()  // (optional) Offset all points
  )

参数解释:
image:待绘制轮廓图像。
contours:要绘制的轮廓。
contourIdx:绘制对应轮廓或全部轮廓。
colorthicknesslineType:分别表示线条颜色、粗细,线的类型(四联通、八联通、cv::A4线)。
hierarchy:cv::findContours()函数输出的轮廓层次。
maxLevel:绘制的轮廓层次深度。
offset:使轮廓绘制在与原定义的绝对坐标系不同的地方。

1.3 轮廓实例

以下代码实现功能:读取图像、二值化、提取轮廓并绘制,通过加入滑动条控制阈值,得到不同的结果图像。

#include <opencv2/opencv.hpp>
#include <iostream>

using namespace std;

cv::Mat g_gray, g_binary;
int g_thresh = 100;

void on_trackbar( int, void* ) {

  cv::threshold( g_gray, g_binary, g_thresh,  255, cv::THRESH_BINARY );
  vector< vector< cv::Point> > contours;
  cv::findContours(
    g_binary,
    contours,
    cv::noArray(),
    cv::RETR_LIST,
    cv::CHAIN_APPROX_SIMPLE
  );
  g_binary = cv::Scalar::all(0);
  
  cv::drawContours( g_binary, contours, -1, cv::Scalar::all(255));  // 白色
  cv::imshow( "Contours", g_binary );  
  
}

 int main( int argc, char** argv ) {
  if ( argc != 2 || ( g_gray = cv::imread(argv[1], 0)).empty() ) {
    cout << "Find threshold dependent contours\n Usage: " << argv[0]
      << "fruits.jpg" << endl;
    return -1;
  }
  cv::nameWindow( "Contours", 1 );
  
  cv::createTrackbar(
    "Threshold",
    "Contours",
    &g_thresh,
    255,
    on_trackbar
  );
  on_trackbar(0, 0);

  cv::waitKey();
  
  return 0;

}

1.4 另一个轮廓实例

以下代码实现功能:检测输入图像轮廓、逐条绘制。

#include <opencv2/opencv.hpp>
#include <algorithm>
#include <iostream>

using namespace std;

struct AreaCmp {
  AreaCmp(const vector<float>& _areas) : areas(&_areas) {}
  bool operator()(int a, int b) const { return (*areas)[a] > (*areas)[b]; }
  const vector<float>* areas;
};

int main(int argc, char* argv[]) {
  cv::Mat img, img_edge, img_color;
  
  // load image or show help if no image was provided
  //
  if ( argc != 2 || (img = cv::imread(aegv[1], cv::LOAD_IMAGE_GRAYSCALE)).empty() ) {
    cout << "\n Example 8_2 Drawing Contours\n call is:\n./ch8_ex8_2 image\n\n";
    return -1;
  }

  cv::threshold( img, img_edge, 128, 255, cv::THRESH_BINARY);
  cv::imshow("Image after threshold", img_edge);
  vector< vector< cv::Point > > contours;
  vector< cv::Vec4i > hierarchy;

  cv::findContours(
    img_edge,
    contours,
    hierarchy,
    cv::RETR_LIST,
    cv::CHAIN_APPROX_SIMPLE 
  );
  cout << "\n\nHit any key to draw the next contour, Esc to quit\n\n";
  cout << "Total Contours Detected: " << contours.size() << endl;
  
  vector<int> sortIdx(contours.size());
  vector<float> areas(contours.size());
  for ( int n = 0; n < (int)contours.size(); n++ ) {
    sortIdx[n] = n;
    areas[n] = contourArea(contours[n], false);
  }

  // sort contours so that the largest contours contours go first
  //
  std::sort( sortIdx.begin(), sortIdx.end(), AreaCmp(areas ));

  for ( int n = 0; n < (int)sortIdx.size(); n++ ) {
    int idx = sortIdx[n];
    cv::cvtColor( img, img_color, cv::GRAY2BGR );
    cv::drawContours(
      img_color, contours, idx,
      cv::Scalar(0, 0, 255), 2, 8, hierarchy,
      0      // Try different values of max_level, and see what happens
    );  
    cout << "Contour #" << idx << ": area=" << areas[idx] << 
      ", nvertices=" << contours[idx].size() << endl;
    cv::imshow(argv[0], img_color);
    int k;
    if ( (k = cv::waitKey()&255) == 27 )  // 暂停,按下Esc键(ASCII码=27)退出
      break;
  }
  cout << "Finished all contours\n";
  
  return 0;

}

1.5 快速连通区域分析

OpenCV中的连通区域分析算法,输入要求是一张二值图像,输出是一张像素标记图,其中属于同一连通区域的非零像素都是同一定值。在背景分割算法中,连通区域分析常常被用作后处理滤波器,用于去掉小噪声块。

OpenCV3中采用cv::connectedComponents()cv::connectedComponentsWithStats()函数来实现这一功能。

int  cv::connectedComponents (
  cv::InputArrayn  image,  // input 8-bits single-channel (binary)
  cv::OutputArray  labels,  // output label map
  int  connectivity = 8,  // 4- or 8-connected components
  int  ltype = CV_32S  // Output label type (CV_32S or CV_16U)
);

int  cv::connectedComponentsWithStats (
  cv::InputArrayn  image,  // input 8-bit single-channel (binary)
  cv::OutputArray  labels,  // output label map
  cv::OutputArray  stats,  // Nx5 matrix (CV_32S) of statistics:
                           // [x0, y0, width0, height0, area0; ... ;
                           // x(N-1), y(N-1), width(N-1), height(N-1), area(N-1)]
  cv::OutputArray centroids,  // Nx2 CV_64F matrix of centroids: 
                              // [cx0, cy0; ... ; cx(N-1), cy(N-1)]
  int  connectivity = 8,  // 4- or 8-connected components
  int  ltype  = CV_32S  // Output label type (CV_32S or CV_16U)
);

cv::connectedComponents()函数简单生成了标记图。cv::connectedComponentsWithStats()函数生成标记图的同时返回关于每块连通区域的一些重要信息,如包围框、面积、质心等。
以下代码实现功能:去掉所有较小的连通区域、绘制剩余区域

#include <opencv2/opencv.hpp>
#include <algorithm>
#include <iostream>

using namespace std;
int main(int argc, char*argv[]) {
  
  cv::Mat img, img_edge, labels, img_color, stats;
  
  // load image or show help if no image was provided
  if ( argc != 2 
    || ( img = cv::imread( argv[1], cv::LOAD_IMAGE_GRAYSCALE )).empty()) {
    cout << "\nExample 8_3 Drawing Connected componnects\n" \
      << "Call is :\n" << argv[0] << " image\n\n";
    return  -1;
  }

  cv::threshold(img, img_edge, 128, 255, cv::THRESH_BINARY);
  cv::imshow("Image after threshold", img_edge);

  int i, nccomps = cv::connectedComponentsWithStats (
    img_edge,  labels,
    stats,  cv::noArray()
  );
  cout << "Total Connected Components Detected:" << nccomps << endl;

  vector<cv::Vec3b>  colors(nccomps+1);
  colors[0] = Vec3b(0, 0, 0);  // backgroud pixels remain black.
  for ( i = 1;   i <= nccomps; i++ ) {
    colors[i] = Vec3b(rand()%256, rand()%256, rand()%256);
    if ( stats.at<int>(i-1, cv::CC_STAT_AREA) < 100 )
      colors[i] = Vec3b(0, 0, 0);  // small regions are painted with black too.
  }
  img_color = Mat::zeros(img.size(), CV_8UC3);
  for ( int y = 0; y < img_color.rows; y++ )
    for ( int x = 0; x < img_color.cols; x++ )
    {
      int label = label.at<int>(y, x);
      CV_Assert( 0 <= label && label <= nccomps);
      img_color.at<cv::Vec3b>(y, x) = colors[label];
    }
  cv::imshow("Labeled map", img_color);
  cv::waitKey();
  return  0;
}

相关文章

网友评论

      本文标题:《Learning OpenCV3》笔记——轮廓(一)

      本文链接:https://www.haomeiwen.com/subject/ltwwyhtx.html