一、cv::findContours轮廓提取函数
1.1 cv::findContours
函数简介
cv::findContours
函数是用于从二值图像(灰度图)中检索轮廓。这个函数在OpenCV的不同版本中参数可能有所不同,但基本概念保持一致。特别是在OpenCV 3.x和4.x版本中,cv::findContours
的参数主要包括输入图像、输出轮廓、轮廓检索模式(mode)和轮廓近似方法(method)。
void cv::findContours(InputOutputArray image,
OutputArrayOfArrays contours,
OutputArray hierarchy,
int mode,
int method,
Point offset = Point());
//image:输入图像,必须是8位单通道图像,且通常是通过阈值处理(如cv::threshold或cv::adaptiveThreshold)或边缘检测(如cv::Canny)得到的二值图像。
//hierarchy:轮廓的层次结构信息,可选参数。
//mode:轮廓检索模式,可以是cv::RETR_EXTERNAL、cv::RETR_LIST、cv::RETR_CCOMP或cv::RETR_TREE。
//method:轮廓逼近方法,可以是cv::CHAIN_APPROX_NONE、cv::CHAIN_APPROX_SIMPLE、cv::CHAIN_APPROX_TC89_L1或cv::CHAIN_APPROX_TC89_KCOS。
//offset:可选的偏移量,所有找到的轮廓点都会移动这个偏移量。默认值为Point()。
轮廓检索模式(Mode)
轮廓检索模式决定了如何检索轮廓。OpenCV提供了以下几种模式:
- cv::RETR_EXTERNAL:只检索最外层的轮廓,忽略轮廓内部的孔洞。
- cv::RETR_LIST:检索所有的轮廓,但不创建任何父子关系。
- cv::RETR_CCOMP:检索所有的轮廓,并将它们组织为两层:外层轮廓和它们的内层轮廓(孔洞)。
- cv::RETR_TREE:检索所有的轮廓,并重新建立完整的轮廓层次结构。
轮廓近似方法(Method)
轮廓近似方法定义了轮廓如何被近似表示。主要有以下几种方法:
- cv::CHAIN_APPROX_NONE:存储轮廓的每一个点,即轮廓的精确表示。
- cv::CHAIN_APPROX_SIMPLE(或cv::CHAIN_APPROX_TC89_L1, cv::CHAIN_APPROX_TC89_KCOS):压缩水平、垂直和对角线段,只留下它们的端点。对于大多数应用来说,这种近似是足够的,并且可以显著减少轮廓点的数量。
- cv::CHAIN_APPROX_TC89_LKB:使用Teh-Chin链近似算法的一个变种。
1.2 需要将图像转为二值图
图像轮廓检测,通常需要将图像转换为灰度图(二值图像),并应用阈值操作。
// 读取图像
cv::Mat src = cv::imread(argv[1]);
// cv::Mat src = cv::imread("path_to_image.jpg");
if (src.empty()) {
std::cerr << "Could not read the image" << std::endl;
return 1;
}else{
// 显示结果
cv::imshow("orgimg", src);
cv::waitKey(0);
}
// 应用阈值 ,提取需要像素,主要是针对轮廓部分的值范围,例如215~235
cv::Mat binary;
cv::threshold(src, binary, 215, 235, cv::THRESH_BINARY);
if (binary.empty())
{
std::cerr << "Could show the image" << std::endl;
return 1;
}else{
// 显示结果
cv::imshow("binaryimg", binary);
cv::waitKey(0);
}
1.3 从二值图像中查找轮廓实现代码
假设存在一个cv::Mat binary二值图像(PS:确保你的输入图像是二值的,即图像中的像素值只有0和255),采用cv::findContours函数查询轮廓代码实现如下,不同参数组合成效不同:
// 查找轮廓
std::vector<std::vector<cv::Point>> contours;
cv::Mat hierarchy;
// 查找轮廓
// cv::findContours(binary, contours, hierarchy, cv::RETR_TREE, cv::CHAIN_APPROX_NONE);
cv::findContours(binary, contours, hierarchy, cv::RETR_TREE, cv::CHAIN_APPROX_SIMPLE);
// cv::findContours(binary, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
// cv::findContours(binary, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE);
// cv::findContours(binary, contours, hierarchy, cv::RETR_LIST, cv::CHAIN_APPROX_SIMPLE);
// cv::findContours(binary, contours, hierarchy, cv::RETR_CCOMP, cv::CHAIN_APPROX_SIMPLE);
// cv::findContours(binary, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_TC89_L1);
// cv::findContours(binary, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_TC89_KCOS);
// cv::findContours(binary, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_TC89_KCOS
// ,cv::Point(binary.cols/2,binary.rows/2));
1.4 绘制轮廓
cv::drawContours
函数用于在图像上绘制轮廓。这个函数非常有用,特别是在进行图像处理和计算机视觉任务时,如边缘检测、形状分析等。cv::drawContours
可以将找到的轮廓绘制在原始图像、空白图像或任何其他图像上。
//函数原型:
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: 目标图像,轮廓将被绘制在这个图像上。它应该是单通道(如灰度图)或三通道(如彩色图)的图像。
contours: 一个包含轮廓点的点的向量向量(std::vector<std::vector<cv::Point>>)。这通常是通过findContours函数获得的。
contourIdx: 指定要绘制的轮廓的索引。如果它是负数,则绘制所有轮廓。
color: 轮廓的颜色。对于灰度图像,它是一个表示灰度级的标量。对于彩色图像,它是一个表示BGR颜色的三元素向量。
thickness: 轮廓线的厚度。如果它是负数(如FILLED),则轮廓内部将被填充。
lineType: 线条的类型。它可以是LINE_8、LINE_AA等。LINE_8表示8连通的线条,而LINE_AA表示抗锯齿线条,这会使线条看起来更平滑。
hierarchy: 可选的,包含轮廓层次结构的向量。这是findContours函数的输出之一,但在绘制轮廓时通常不需要。
maxLevel: 绘制轮廓的最大层级。只有当hierarchy非空时才有效。
offset: 轮廓点从原图像到目标图像的偏移量。
*/
将已经提取的轮廓点绘制图像代码如下:
// 轮廓点存放
std::vector<std::vector<cv::Point>> contours;
// 查找轮廓
......
// 绘制轮廓
cv::Mat contoursImg = cv::Mat::zeros(binary.size(), CV_8UC1);
for (size_t i = 0; i < contours.size(); i++) {
cv::Scalar color = cv::Scalar(255); //这里为白色
cv::drawContours(contoursImg, contours, static_cast<int>(i), color, 2, cv::LINE_8, hierarchy, 0);
}
if (contoursImg.empty())
{
std::cerr << "Could show the image" << std::endl;
return 1;
}
// 显示结果
cv::imshow("Contours", contoursImg);
cv::waitKey(0);
二、在指定图像区域查找轮廓
2.1指定一个cv::Rect区域
在OpenCV中,如果你想在图像的指定区域内查找轮廓,通常使用ROI(Region of Interest,感兴趣区域)的概念。首先需要定义这个ROI区域,然后,你可以在这个ROI上应用轮廓查找函数。
// 读取图像
cv::Mat src = cv::imread(argv[1]);
//指定检测区域
int x = 180; // ROI左上角x坐标
int y = 135; // ROI左上角y坐标
int width = 210; // ROI宽度
int height = 320; // ROI高度
cv::Rect roi(x, y, width, height);
cv::Mat roiImg = src(roi);
上述代码中先读取一个图像,然后在该图像上规划处一个矩形区域作为轮廓查找的ROI。
2.2 指定多边形区域提取图像
在OpenCV中,从一幅图像中根据指定的多边形区域提取图像,使用cv::fillPoly
函数来绘制这个多边形区域,并填充它,或者使用cv::fillConvexPoly
(如果多边形是凸的,这会更高效),然后结合图像掩码(mask)来提取该区域内的图像。
通常做法是:
-
定义多边形:定义多边形顶点。这些顶点定义了你要从图像中提取的区域。
-
创建掩码:基于多边形顶点,创建一个与原图同样大小的掩码图像,并填充多边形区域为白色(或任何其他与背景不同的颜色),其余部分为黑色。
-
应用掩码:最后,使用掩码来提取多边形区域内的图像。
// 定义多边形顶点(注意:OpenCV中的坐标系统是(x,y))
//指定范围内提取轮廓
std::vector<cv::Point> pts;
pts.push_back(cv::Point(207, 209));
pts.push_back(cv::Point(279, 138));
pts.push_back(cv::Point(317, 139));
pts.push_back(cv::Point(357, 196));
pts.push_back(cv::Point(325, 228));
pts.push_back(cv::Point(328, 299));
pts.push_back(cv::Point(303, 443));
pts.push_back(cv::Point(234, 444));
pts.push_back(cv::Point(238, 353));
pts.push_back(cv::Point(242, 297));
pts.push_back(cv::Point(247, 236));
pts.push_back(cv::Point(212, 232));
pts.push_back(cv::Point(207, 209)); // 闭合多边形
// 创建掩码,大小与原图相同,初始化为全黑
cv::Mat mask = cv::Mat::zeros(gray.size(), CV_8UC1);
// 填充多边形区域为白色
const cv::Point* ppt[1] = { &pts[0] };
int npt[] = { static_cast<int>(pts.size()) };
fillPoly(mask, ppt, npt, 1, cv::Scalar(255), 8, 0);
// 应用掩码提取多边形区域内的图像
cv::Mat result;
bitwise_and(gray, gray, result, mask);
三、轮廓提取的预处理方法
3.1 降噪处理
在C++中使用OpenCV进行轮廓提取之前,进行降噪预处理是一个常见的步骤,以提高轮廓检测的准确性和鲁棒性。降噪处理可以减少图像中的噪声,使轮廓更加清晰和易于检测。
1. 高斯模糊(Gaussian Blur)
高斯模糊是一种常用的图像平滑技术,它通过应用高斯函数来减少图像中的噪声和细节。高斯模糊对于去除高斯噪声特别有效。
// 读取图像
cv::Mat src = cv::imread(argv[1]);
//高斯模糊
cv::Mat result;
cv::GaussianBlur(src, result, cv::Size(5, 5), 1.4);
2. 中值模糊(Median Blur)
中值模糊是一种非线性滤波技术,它用像素邻域内的中值替换该像素的值。中值模糊对于去除椒盐噪声特别有效。
// 读取图像
cv::Mat src = cv::imread(argv[1]);
//中值模糊
cv::Mat result;
cv::medianBlur(src, result, 5);
3. 双边滤波(Bilateral Filter)
双边滤波在平滑图像的同时,还能保持边缘的锐利。它同时考虑像素间的空间距离和像素值的差异。
// 读取图像
cv::Mat src = cv::imread(argv[1]);
//中值模糊
cv::Mat result;
cv::bilateralFilter(src, result, 5, 20, 20);
4. 形态学操作
形态学操作如开运算(先腐蚀后膨胀)和闭运算(先膨胀后腐蚀)也可以用于降噪和去除小的噪声点。这些操作对于二值化图像尤其有用。
// 读取图像
cv::Mat src = cv::imread(argv[1]);
// 应用阈值
cv::Mat binary;
cv::threshold(src, binary, 215, 235, cv::THRESH_BINARY);
// 使用形态学开运算去除小的白噪声
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3));
cv::morphologyEx(binary, binary/*result*/, cv::MORPH_OPEN, kernel);
3.2 图像缩放
在OpenCV中,轮廓提取时,不少图像是高清图像,由于显示屏幕限制,处理结果会存在显示不全的尴尬问题。cv::resize
函数是一个非常常用的函数,用于调整图像的大小。这个函数可以根据指定的尺寸或缩放比例来改变图像的大小。它定义在opencv2/imgproc.hpp
头文件中,并属于imgproc模块。
//函数原型:
void cv::resize(InputArray src, OutputArray dst,
Size dsize, double fx = 0, double fy = 0,
int interpolation = INTER_LINEAR );
/*
参数说明:
src: 输入图像。
dst: 输出图像,其大小和类型由dsize、src.type()和fx、fy决定。
dsize: 输出图像的大小。如果它是零,那么大小是通过src.size()、fx和fy计算得到的(两者都不能为零)。
fx: 沿水平轴的比例因子(宽度)。如果它是零,那么它会自动计算为 (double)dsize.width/src.cols。
fy: 沿垂直轴的比例因子(高度)。如果它是零,那么它会自动计算为 (double)dsize.height/src.rows。
interpolation: 插值方法。插值算法决定了在调整图像大小时使用的算法。常用的插值方法包括:
INTER_NEAREST - 最近邻插值
INTER_LINEAR - 双线性插值(默认)
INTER_AREA - 使用像素区域关系进行重采样(图像缩小时使用)
INTER_CUBIC - 双三次插值(图像放大时使用)
INTER_LANCZOS4 - Lanczos插值
*/
函数应用示例:
if(contoursImg.cols>1000 || contoursImg.rows>1000){
// 假设我们想要将图片宽高都缩小为原来的1/2
cv::Mat dst;
cv::Size size(contoursImg.cols / 2, contoursImg.rows / 2); // 目标尺寸
// 使用resize函数进行缩放
// 第一个参数是源图像,第二个参数是目标图像,第三个参数是目标尺寸
// 第四个参数是插值方法,这里使用INTER_LINEAR(双线性插值)
cv::resize(contoursImg, dst, size, 0, 0, cv::INTER_LINEAR);
if (contoursImg.empty())
{
std::cerr << "Could show the image" << std::endl;
return 1;
}
// 显示原图和处理后的图片
cv::imshow("dst_Contours", dst);
// 等待任意键盘按键
cv::waitKey(0);
}
四、完整案例
将前面涉及方法代码集成测试案例。
4.1 下载提取轮廓的图片
如图,提取图中人物轮廓,需要跟进该图片设置ROI或多边形轮廓,详细见程序代码。
4.2 程序代码
main.cpp实现
#include <opencv2/opencv.hpp>
#include <vector>
int main( int argc,char** argv )
{
if(argc< 2){
std::cout <<"specify input image" << std::endl;
return -1;
}
// 读取图像
cv::Mat src = cv::imread(argv[1]);
// cv::Mat src = cv::imread("path_to_image.jpg");
if (src.empty()) {
std::cerr << "Could not read the image" << std::endl;
return 1;
}else{
// 显示结果
cv::imshow("orgimg", src);
cv::waitKey(0);
}
//指定检测区域
int x = 180; // ROI左上角x坐标
int y = 135; // ROI左上角y坐标
int width = 210; // ROI宽度
int height = 320; // ROI高度
cv::Rect roi(x, y, width, height);
cv::Mat roiImg = src(roi);
// 转换为灰度图
cv::Mat gray;
// cv::cvtColor(roiImg, gray, cv::COLOR_BGR2GRAY);
cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY);
if (gray.empty())
{
std::cerr << "Could show the image" << std::endl;
return 1;
}else{
// 显示结果
cv::imshow("grayimg", gray);
cv::waitKey(0);
}
// 定义多边形顶点(注意:OpenCV中的坐标系统是(x,y))
//指定范围内提取轮廓
std::vector<cv::Point> pts;
pts.push_back(cv::Point(207, 209));
pts.push_back(cv::Point(279, 138));
pts.push_back(cv::Point(317, 139));
pts.push_back(cv::Point(357, 196));
pts.push_back(cv::Point(325, 228));
pts.push_back(cv::Point(328, 299));
pts.push_back(cv::Point(303, 443));
pts.push_back(cv::Point(234, 444));
pts.push_back(cv::Point(238, 353));
pts.push_back(cv::Point(242, 297));
pts.push_back(cv::Point(247, 236));
pts.push_back(cv::Point(212, 232));
pts.push_back(cv::Point(207, 209)); // 闭合多边形
// 创建掩码,大小与原图相同,初始化为全黑
cv::Mat mask = cv::Mat::zeros(gray.size(), CV_8UC1);
// 填充多边形区域为白色
const cv::Point* ppt[1] = { &pts[0] };
int npt[] = { static_cast<int>(pts.size()) };
fillPoly(mask, ppt, npt, 1, cv::Scalar(255), 8, 0);
// 应用掩码提取多边形区域内的图像
cv::Mat result;
bitwise_and(gray, gray, result, mask);
cv::Mat Filter_dst;
// cv::GaussianBlur(result, result, cv::Size(5, 5), 1.4);
// cv::medianBlur(result, result, 5);
cv::bilateralFilter(result, Filter_dst, 5, 20, 20);
// 应用阈值
cv::Mat binary;
cv::threshold(Filter_dst, binary, 215, 235, cv::THRESH_BINARY);
// cv::threshold(result, binary, 215, 235, cv::THRESH_BINARY);
if (binary.empty())
{
std::cerr << "Could show the image" << std::endl;
return 1;
}else{
// 显示结果
cv::imshow("binaryimg", binary);
cv::waitKey(0);
}
// // 使用形态学开运算去除小的白噪声
// cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3));
// cv::morphologyEx(binary, binary, cv::MORPH_OPEN, kernel);
// 查找轮廓
std::vector<std::vector<cv::Point>> contours;
cv::Mat hierarchy;
// 查找轮廓
// cv::findContours(binary, contours, hierarchy, cv::RETR_TREE, cv::CHAIN_APPROX_NONE);
cv::findContours(binary, contours, hierarchy, cv::RETR_TREE, cv::CHAIN_APPROX_SIMPLE);
// cv::findContours(binary, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
// cv::findContours(binary, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE);
// cv::findContours(binary, contours, hierarchy, cv::RETR_LIST, cv::CHAIN_APPROX_SIMPLE);
// cv::findContours(binary, contours, hierarchy, cv::RETR_CCOMP, cv::CHAIN_APPROX_SIMPLE);
// cv::findContours(binary, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_TC89_L1);
// cv::findContours(binary, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_TC89_KCOS);
// cv::findContours(binary, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_TC89_KCOS
// ,cv::Point(binary.cols/2,binary.rows/2));
// 绘制轮廓
cv::Mat contoursImg = cv::Mat::zeros(binary.size(), CV_8UC1);
for (size_t i = 0; i < contours.size(); i++) {
cv::Scalar color = cv::Scalar(255); //这里为白色
cv::drawContours(contoursImg, contours, static_cast<int>(i), color, 2, cv::LINE_8, hierarchy, 0);
}
if (contoursImg.empty())
{
std::cerr << "Could show the image" << std::endl;
return 1;
}
// 显示结果
cv::imshow("Contours", contoursImg);
cv::waitKey(0);
if(contoursImg.cols>1000 || contoursImg.rows>1000){
// 假设我们想要将图片宽高都缩小为原来的1/2
cv::Mat dst;
cv::Size size(contoursImg.cols / 2, contoursImg.rows / 2); // 目标尺寸
// 使用resize函数进行缩放
// 第一个参数是源图像,第二个参数是目标图像,第三个参数是目标尺寸
// 第四个参数是插值方法,这里使用INTER_LINEAR(双线性插值)
cv::resize(contoursImg, dst, size, 0, 0, cv::INTER_LINEAR);
if (contoursImg.empty())
{
std::cerr << "Could show the image" << std::endl;
return 1;
}
// 显示原图和处理后的图片
cv::imshow("dst_Contours", dst);
// 等待任意键盘按键
cv::waitKey(0);
}
return 0;
}
4.3 程序编译及运行
本文是采用win系统下,opencv采用MinGW编译的静态库(C/C++开发,win下OpenCV+MinGW编译环境搭建_opencv mingw-CSDN博客),建立makefile:
#/bin/sh
#win32
CX= g++ -DWIN32
#linux
#CX= g++ -Dlinux
BIN := ./
TARGET := contours_img01.exe
FLAGS := -std=c++11 -static
SRCDIR := ./
#INCLUDES
INCLUDEDIR := -I"../../opencv_MinGW/include" -I"./"
#-I"$(SRCDIR)"
staticDir := ../../opencv_MinGW/x64/mingw/staticlib/
#LIBDIR := $(staticDir)/libopencv_world460.a\
# $(staticDir)/libade.a \
# $(staticDir)/libIlmImf.a \
# $(staticDir)/libquirc.a \
# $(staticDir)/libzlib.a \
# $(wildcard $(staticDir)/liblib*.a) \
# -lgdi32 -lComDlg32 -lOleAut32 -lOle32 -luuid
#opencv_world放弃前,然后是opencv依赖的第三方库,后面的库是MinGW编译工具的库
LIBDIR := -L $(staticDir) -lopencv_world460 -lade -lIlmImf -lquirc -lzlib \
-llibjpeg-turbo -llibopenjp2 -llibpng -llibprotobuf -llibtiff -llibwebp \
-lgdi32 -lComDlg32 -lOleAut32 -lOle32 -luuid
source := $(wildcard $(SRCDIR)/*.cpp)
$(TARGET) :
$(CX) $(FLAGS) $(INCLUDEDIR) $(source) -o $(BIN)/$(TARGET) $(LIBDIR)
clean:
rm $(BIN)/$(TARGET)
编译如下:
程序运行输出如下:
各位读者可以自由调节各个参数来观察其效果,或更换你需要的图片来验证。