第14章 轮廓
Canny之类的边缘检测算法可以根据像素间差异检测出轮廓边界的像素,但它未将轮廓作为一个整体进行处理。
OpenCV提供了寻找轮廓的函数cv::findContours()
1. 轮廓查找
一个轮廓对应一系列点,这些点以某种方式表示图像中的一条曲线。
在OpenCV中采用向量vector<>进行表示,向量中的每一个值都包含轮廓上下一个点的位置信息,常见的有vector<cv::point>
或vector<cv::point2f>
。
cv::findContours()
用于计算二维图像轮廓,图像先经过相关预处理(边缘检测、阈值处理等)
1.1 轮廓层次
轮廓是什么?一组轮廓之间如何相互关联?下面通过轮廓树的概念进行理解,如图:
其中,表示“轮廓”(),表示“孔”(),表示数字。
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),是一条特定轮廓,是中的一个点。
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_L1 或 cv::CHAIN_APPROX_TC89_KCOS
使用链逼近算法中的一个,该算法不需要调节参数。
算法细节见论文: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
:绘制对应轮廓或全部轮廓。
color
、thickness
、lineType
:分别表示线条颜色、粗细,线的类型(四联通、八联通、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;
}
网友评论