8.1 查找并绘制轮廓
一个轮廓一般对应一系列的点,也就是图像中的一条曲线。其表示方法可能 根据不同的情况而有所不同。在OpenCV 中,可以用findContours()函数从二值图 像中查找轮廓
8.1.1 寻找轮廓: findContours() 函数
findContours) 函数用于在二值图像中寻找轮廓。
void findContours(InputOutputArray image, OutputArrayOfArrays contours, OutputArray hierarchy, int mode, intmethod, Point offset = Point())
- 第一个参数,InputArray类型的image, 输入图像,即源图像,填Mat 类的 对象即可,且需为8位单通道图像。图像的非零像素被视为1,0像素值被 保留为0,所以图像为二进制。我们可以使用 compare() 、inrange()、
threshold() 、adaptivethreshold() 、cannyO 等函数由灰度图或彩色图创建二进 制图像。此函数会在提取图像轮廓的同时修改图像的内容。 - 第二个参数,OutputArrayOfArrays 类 型 的contours、检测到的轮廓、函数 调用后的运算结果存在这里。每个轮廓存储为一个点向量,即用point 类 型 的 vector表示。
- 第三个参数,OutputArray 类型的hierarchy,可选的输出向量,包含图像的 拓扑信息。其作为轮廓数量的表示,包含了许多元素。每个轮廓contours[i] 对 应 4 个hierarchy 元 素hierarchy[i][0]~hierarchy[i][3], 分别表示后一 个轮廓、前一个轮廓、父轮廓、内嵌轮廓的索引编号。如果没有对应项,
对应的hierarchy[i] 值设置为负数。 - 第四个参数,int 类型的mode, 轮廓检索模式,取值如表8.1所示。
表8.1 findContours函数可选的轮廓检索模式
标识符 | 含义 |
---|---|
RETR_EXTERNAL | 表示只检测最外层轮廓。对所有轮廓,设置 hierarchy[i][2]=hierarchy[i][3]=-1 |
RETR_LIST | 提取所有轮廓,并且放置在list中。检测的轮廓 不建立等级关系 |
RETR_CCOMP | 提取所有轮廓,并且将其组织为双层结构(two-level hierarchy:顶层为连通域的外围边界, 次层为孔的内层边界 |
RETR_TREE | 提取所有轮廓,并重新建立网状的轮廓结构 |
- 第五个参数,int类 型 的method, 为轮廓的近似办法,取值如表8.2所示。
表8.2 findContours函数可选的轮廓近似办法
标识符 | 含义 |
---|---|
CHAIN_APPROX NONE | 获取每个轮廓的每个像素,相邻的两个点的像素位置差不超过 1,即max(abs(xl-x2),abs(y2-y1))=1 |
CHAIN_APPROX_SIMPLE | 压缩水平方向,垂直方向,对角线方向的元素,只保留该方向 的终点坐标,例如一个矩形轮廓只需4个点来保存轮廓信息 |
CHAIN_APPROX_TC89_L1 ,CHAIN_APPROX_TC89_KCOS | 使用Teh-Chinl链逼近算法中的一个 |
同样地,在表8.1和8.2中列出的宏之前加上”CV_” 前缀,便是OpenCV2 中可以使用的宏。如"RETR_CCOMP” 宏 的OpenCV2 版 为“CV_RETR_CCOMP"。
- 第六个参数,Point 类 型 的 offset,每个轮廓点的可选偏移量,有默认值 Point()。对 ROI 图像中找出的轮廓,并要在整个图像中进行分析时,这个 参数便可排上用场。
findContours 经 常 与drawContours 配合使用一使用用findContours (函数检测 到图像的轮廓后,便可以用drawContours (函数将检测到的轮廓绘制出来。接下来, 让我们一起看看drawContours() 函数的用法。
8.1.2 绘 制 轮 廓 :drawContours ()函 数
drawContours() 函数用于在图像中绘制外部或内部轮廓。
void drawContours(InputoutputArray image, InputArrayofArrays contours, int contourIdx, const Scalar &color, int thickness = 1, int lineType = 8, InputArray hierarchy = noArray(0), int maxLevel = INT_MAX, Point offset = Point())
- 第 一 个参数,InputArray类 型 的image, 目标图像,填Mat 类的对象即可。
- 第二个参数,InputArrayOfArrays类型的contours,所有的输入轮廓。每个 轮廓存储为一个点向量,即用point类 型 的vector表示。
- 第三个参数,int类 型 的contourldx,轮廓绘制的指示变量。如果其为负值, 则绘制所有轮廓。
- 第四个参数,const Scalar&类型的color, 轮廓的颜色。
- 第五个参数,int thickness,轮廓线条的粗细度,有默认值1。如果其为负 值(如 thickness=cv_filled), 便会绘制在轮廓的内部。可选为FILLED 宏(OpenCV2版为CV_FILLED)。
- 第六个参数,int 类型的lineType,线条的类型,有默认值8。取值类型如 表8 . 3所示。
表8.3 可选线性
lineType线性 | 含义 |
---|---|
8(默认值) | 8连通线型 |
4 | 4连通线型 |
LINE_AA(OpenCV2版为CV_AA) | 抗锯齿线型 |
- 第七个参数,InputArray 类型的hierarchy,可选的层次结构信息,有默认 值noArray()。
- 第八个参数,int类型的maxLevel,表示用于绘制轮廓的最大等级,有默认 值INT_MAX
- 第九个参数,Point类型的offset,可选的轮廓偏移参数,用指定的偏移量 offset=(dx,dy) 偏移需要绘制的轮廓,有默认值Point()。
下面是 一 个调用小示例。 //在白色图像上绘制黑色轮廓
Mat result(image.size(),CV_8U,cv::Scalar(255));
drawContours(result,contours,- 1,Scalar(0),3);
8.1.3 基础示例程序:轮廓查找
void Test52() {Mat srcImage = imread("image.jpg", 0); //灰度图读入imshow("src", srcImage);Mat dstImage = Mat::zeros(srcImage.rows, srcImage.cols, CV_8UC3);srcImage = srcImage > 119;imshow("mid", srcImage); //取阈值后的图像//定义轮廓和层次结构std::vector<std::vector<Point>>contours;std::vector<Vec4i>hierachy;//查找轮廓findContours(srcImage, contours, hierachy, RETR_CCOMP, CHAIN_APPROX_SIMPLE);//遍历所有顶层的罗坤,随机颜色绘制出每个连接组件颜色int index = 0;for (; index >= 0; index = hierachy[index][0]) {Scalar color(rand() & 255, rand() & 255, rand() & 255);drawContours(dstImage, contours, index, color, FILLED, 8, hierachy);imshow("dst", dstImage);}waitKey(0);
}
8.1.4 综合示例程序:查找并绘制轮廓
除了上述这个精简版的示例程序,还为大家准备了一个更加复杂一些的关 于查找并绘制轮廓的综合示例程序。此程序利用了图像平滑技术(blur() 函数)和边缘检测技术(cannyO 函数),根据滑动条的调节,可以动态地检测出图形的 轮 廓 。
namespace test53 {Mat g_srcImage, g_grayImage;int g_nThresh = 80;int g_nThresh_max = 255;RNG g_rng(12345);Mat g_cannyMat_output;std::vector<std::vector<Point>>g_vContours;std::vector<Vec4i>g_vHierarchy;void on_ThreshChange(int, void*) {Canny(g_grayImage, g_cannyMat_output, g_nThresh, g_nThresh * 2, 3); //Canny算子边缘检测findContours(g_cannyMat_output, g_vContours, g_vHierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0));//轮廓提取Mat drawing = Mat::zeros(g_cannyMat_output.size(), CV_8UC3);for (int i = 0; i < g_vContours.size(); ++i) {Scalar color = Scalar(g_rng.uniform(0, 255), g_rng.uniform(0, 255), g_rng.uniform(0, 255));//随机值drawContours(drawing, g_vContours, i, color, 2, 8, g_vHierarchy, 0, Point());}imshow("drawing", drawing);}void Test() {g_srcImage = imread("image.jpg");cvtColor(g_srcImage, g_grayImage, COLOR_BGR2GRAY);blur(g_grayImage, g_grayImage, Size(3, 3)); //降噪namedWindow("window1");imshow("window1", g_srcImage);createTrackbar("value", "window1", &g_nThresh, g_nThresh_max, on_ThreshChange);on_ThreshChange(0, 0);waitKey(0);}
}void Test53() {test53::Test();
}
8.2 寻找物体的凸包
8.2.1 凸 包
凸包(Convex Hull) 是一个计算几何(图形学)中常见的概念。简单来说, 给定二维平面上的点集,凸包就是将最外层的点连接起来构成的凸多边型,它是 能包含点集中所有点的。理解物体形状或轮廓的一种比较有用的方法便是计算一 个物体的凸包,然后计算其凸缺陷(convexity defects)。很多复杂物体的特性能很 好地被这种缺陷表现出来。
如图8.9所示,我们用人手图来举例说明凸缺陷这一概念。手周围深色的线 描画出了凸包,A 到 H 被标出的区域是凸包的各个“缺陷”。正如看到的,这些 凸度缺陷提供了手以及手状态的特征表现的方法。
新版OpenCV 中 ,convexHull 函数用于寻找图像点集中的凸包,我们一起来 看一下这个函数。
8.2.2 寻找凸包:convexHullO函数
上文已经提到过,convexHullO 函数用于寻找图像点集中的凸包,其原型声明 如 下 。
void convexHull(InputArray points, OutputArray hull, bool clockwise = false, bool returnPoints = true)
- 第一个参数,InputArray类型的points,输入的二维点集,可以填Mat 类型 或者std::vector。
- 第二个参数,OutputArray类型的hull,输出参数,函数调用后找到的凸包。
- 第三个参数,bool 类型的clockwise,操作方向标识符。当此标识符为真时, 输出的凸包为顺时针方向。否则,就为逆时针方向。并且是假定坐标系的 x 轴指向右,y 轴指向上方。
- 第四个参数,bool 类型的returnPoints,操作标志符,默认值true。当 标 志 符为真时,函数返回各凸包的各个点。否则,它返回凸包各点的指数。当 输出数组是std::vector 时,此标志被忽略。
8.2.3 基础示例程序:凸包检测基础
为了理解凸包检测的运用方法,下面放出一个完整的示例程序。程序中会首 先随机生成3~103个坐标值随机的彩色点,然后利用convexHull, 对由这些点链 接起来的图形求凸包。
void Test54() {Mat image(600, 600, CV_8UC3);RNG& rng = theRNG();while (1) {char key;int count = (unsigned)rng % 100 + 3;std::vector<Point>points;//随机生成坐标for (int i = 0; i < count; ++i) {Point point;point.x = rng.uniform(image.cols / 4, image.cols * 3 / 4);point.y = rng.uniform(image.rows / 4, image.rows * 3 / 4);points.push_back(point);}//检测凸包std::vector<int>hull;convexHull(Mat(points), hull, true);//绘制出随机颜色的点image = Scalar::all(0);for (int i = 0; i < count; ++i) {circle(image, points[i], 3, Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)),FILLED,LINE_AA);}int hullcount = hull.size();Point point0 = points[hull.back()];//依次连接凸包的点for (int i = 0; i < hullcount; ++i) {Point point = points[hull[i]];line(image, point0, point, Scalar(255, 255, 255), 2, LINE_AA);point0 = point;}imshow("hull", image);key = waitKey(0);if (key == 27) break;}}
8.2.4 综合示例程序:寻找和绘制物体的凸包
这一节的综合示例程序,依然是结合滑动条,通过滑动条控制阈值,来得到 不同的凸包检测效果图。程序详细注释的源代码如下。
namespace test55 {Mat g_srcImage, g_grayImage;int g_nThresh = 50;int g_maxThresh = 255;RNG g_rng(12345);Mat srcImage_copy = g_srcImage.clone();Mat g_thresholdImage_output;std::vector<std::vector<Point>>g_vContours;std::vector<Vec4i>g_vHierarchy;void on_ThreshChange(int, void*) {// 二值化threshold(g_grayImage, g_thresholdImage_output, g_nThresh, 255, THRESH_BINARY);findContours(g_thresholdImage_output, g_vContours, g_vHierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0)); //轮廓std::vector<std::vector<Point>>hull(g_vContours.size());for (int i = 0; i < g_vContours.size(); ++i) {convexHull(Mat(g_vContours[i]), hull[i], false);}//绘制出轮廓及凸包Mat drawing = Mat::zeros(g_thresholdImage_output.size(), CV_8UC3);for (int i = 0; i < g_vContours.size(); ++i) {Scalar color = Scalar(g_rng.uniform(0, 255), g_rng.uniform(0, 255), g_rng.uniform(0, 255)); drawContours(drawing, g_vContours, i, color, 1, 8, std::vector<Vec4i>(), 0, Point()); //轮廓drawContours(drawing, hull, i, color, 1, 8, std::vector<Vec4i>(), 0, Point()); //凸包}imshow("drawing", drawing);}void Test() {g_srcImage = imread("image.jpg");cvtColor(g_srcImage, g_grayImage, COLOR_BGR2GRAY);blur(g_grayImage, g_grayImage, Size(3, 3));//降噪//创建窗口namedWindow("src");imshow("src", g_srcImage);//创建滚动条createTrackbar("value", "src", &g_nThresh, g_maxThresh, on_ThreshChange);on_ThreshChange(0, nullptr);waitKey(0);}
}void Test55() {test55::Test();
}
8.3 使用多边形将轮廓包围
在实际应用中,常常会有将检测到的轮廓用多边形表示出来的需求。本节就 为大家讲解如何用多边形来表示出轮廓,或者说如何根据轮廓提取出多边形。先 让我们一起学习用OpenCV 创建包围轮廓的多边形边界时会接触到的一些函数。
8.3.1 返回外部矩形边界:boundingRect(O函数
此函数计算并返回指定点集最外面(up-right)的矩形边界。 C++:Rect boundingRect(InputArray points)
其唯一的一个参数为输入的二维点集,可以是std::vector 或 Mat 类型。
8.3.2 寻找最小包围矩形:minAreaRect(函数
此函数用于对给定的2D 点集,寻找可旋转的最小面积的包围矩形。 C++:RotatedRect minAreaRect(InputArray points)
其唯一的一个参数为输入的二维点集,可以为std::vector> 或 Mat 类型。
8.3.3寻找最小包围圆形:minEnclosingCircle(函数
minEnclosingCircle函数的功能是利用一种迭代算法,对给定的2D 点集,去 寻找面积最小的可包围它们的圆形。
void minEnclosingCircle(InputArray points, Point2f ¢er, float &radius)
- 第 一 个 参 数 ,InputArray 类 型 的points,输入的二维点集,可以为std::vector◇ 或Mat 类型。
- 第二个参数,Point2f&类型的center,圆的输出圆心。
- 第三个参数,float&类型的radius,圆的输出半径。
8.3.4 用椭圆拟合二维点集:fitEllipse ( 函 数
此函数的作用是用椭圆拟合二维点集。
RotatedRect fitEllipse(InputArray points)
其唯一的一个参数为输入的二维点集,可以为std::vector<>或Mat 类型。
8.3.5 逼近多边形曲线: approxPolyDPO 函 数
approxPolyDP函数的作用是用指定精度逼近多边形曲线。
void approxPolyDP(InputArray curve, OutputArray approxCurve,double epsilon, bool closed)
- 第一个参数,InputArray类型的curve,输入的二维点集,可以为std::vecto 或Mat 类型。
- 第二个参数,OutputArray类型的approxCurve,多边形逼近的结果,其类 型应该和输入的二维点集的类型 一 致。
- 第三个参数,double类型的epsilon,逼近的精度,为原始曲线和即近似曲 线间的最大值。
- 第四个参数,bool 类型的closed,如果其为真,则近似的曲线为封闭曲线 (第一个顶点和最后一个顶点相连),否则,近似的曲线曲线不封闭。
8.4 图像的矩
矩函数在图像分析中有着广泛的应用,如模式识别、目标分类、目标识别与 方位估计、图像编码与重构等。一个从一幅数字图形中计算出来的矩集,通常描 述了该图像形状的全局特征,并提供了大量的关于该图像不同类型的几何特性信 息,比如大小、位置、方向及形状等。图像矩的这种特性描述能力被广泛地应用 在各种图像处理、计算机视觉和机器人技术领域的目标识别与方位估计中。一阶 矩与形状有关,二阶矩显示曲线围绕直线平均值的扩展程度,三阶矩则是关于平 均值的对称性的测量。由二阶矩和三阶矩可以导出一组共7个不变矩。而不变矩 是图像的统计特性,满足平移、伸缩、旋转均不变的不变性,在图像识别领域得 到了广泛的应用。
那 么 , 在OpenCV 中,如何计算 一个图像的矩呢? 一般由 moments、 contourArea 、arcLength 这三个函数配合求取。
- 使用moments 计算图像所有的矩(最高到3阶)
- 使用contourArea来计算轮廓面积
- 使用arcLength来计算轮廓或曲线长度 下面对其进行一一剖析。
8.4.1 矩的计算:momentsO函数
moments()函数用于计算多边形和光栅形状的最高达三阶的所有矩。矩用来计 算形状的重心、面积,主轴和其他形状特征,如7Hu不变量等。
Moments moments(InputArray array, bool binaryImage = false)
- 第一个参数,InputArray类型的array,输入参数,可以是光栅图像(单通 道,8位或浮点的二维数组)或二维数组 (IN 或 N1)。
- 第二个参数,bool类型的binaryImage,有默认值false。若此参数取 true, 则所有非零像素为1。此参数仅对于图像使用。
需要注意的是,此参数的返回值返回运行后的结果。
8.4.2计算轮廓面积:contourArea(函数
contourArea()函数用于计算整个轮廓或部分轮廓的面积
double contourArea(InputArray contour,bool oriented=false)
- 第一个参数,InputArray类型的contour,输入的向量,二维点(轮廓顶点), 可以为std::vector 或 Mat 类型。
- 第二个参数,bool类型的oriented,面向区域标识符。若其为true,该函数 返回一个带符号的面积值,其正负取决于轮廓的方向(顺时针还是逆时针)。 根据这个特性我们可以根据面积的符号来确定轮廓的位置。需要注意的是, 这个参数有默认值false, 表示以绝对值返回,不带符号。
8.4.3 计算轮廓长度:arcLengthO函数
arcLength(函数用于计算封闭轮廓的周长或曲线的长度。
double arcLength(InputArray curve,bool closed)
- 第一个参数,InputArray类型的curve,输入的二维点集,可以为std:vector或Mat 类型。
- 第二个参数,bool 类型的closed, 一个用于指示曲线是否封闭的标识符, 有默认值closed, 表示曲线封闭。
8.4.4 综合示例程序:查找和绘制图像轮廓矩
学习完函数的讲解,让我们一起通过一个综合的示例程序,真正了解本节内 容的实战用法 。
namespace test56{Mat g_srcImage, g_grayImage;int g_nThresh = 100;int g_nMaxThresh = 255;RNG g_rng(12345);Mat g_cannyMat_output;std::vector<std::vector<Point>>g_vContours;std::vector<Vec4i>g_vHierarchy;//回调函数void on_ThreshChange(int, void*) {Canny(g_grayImage, g_cannyMat_output, g_nThresh, g_nThresh * 2); //边缘检测findContours(g_cannyMat_output, g_vContours, g_vHierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0)); //找到轮廓//计算矩std::vector<Moments>mu(g_vContours.size());for (int i = 0; i < g_vContours.size(); ++i) {mu[i] = moments(g_vContours[i], false);}//计算中心矩std::vector<Point2f>mc(g_vContours.size());for (int i = 0; i < g_vContours.size(); ++i) {mc[i] = Point2f(static_cast<float>(mu[i].m10 / mu[i].m00), static_cast<float>(mu[i].m01 / mu[i].m00));}//绘制轮廓Mat drawing = Mat::zeros(g_cannyMat_output.size(), CV_8UC3);for (int i = 0; i < g_vContours.size(); ++i) {Scalar color = Scalar(g_rng.uniform(0, 255), g_rng.uniform(0, 255), g_rng.uniform(0, 255));drawContours(drawing, g_vContours, i, color, 2, 8, g_vHierarchy, 0, Point()); //绘制内层和外层轮廓circle(drawing, mc[i], 4, color, -1, 8, 0);//绘制圆}imshow("drawing", drawing);//计算轮廓面积std::cout << "\t";for (int i = 0; i < g_vContours.size(); ++i) {std::cout << ">Through m00 Areas[" << i << "]:" << mu[i].m00 << "Length = "<< arcLength(g_vContours[i], true)<< std::endl;std::cout << ">Through OpenCV m_00 Areas[" << i << "]" <<contourArea(g_vContours[i]) <<"Length = " << arcLength(g_vContours[i], true) << std::endl;Scalar color = Scalar(g_rng.uniform(0, 255), g_rng.uniform(0, 255), g_rng.uniform(0, 255));drawContours(drawing, g_vContours, i, color, 2, 8, g_vHierarchy, 0, Point());circle(drawing, mc[i], 4, color, -1, 8, 0);}}void Test() {g_srcImage = imread("image.jpg");cvtColor(g_srcImage, g_grayImage, COLOR_BGR2GRAY);blur(g_grayImage, g_grayImage, Size(3, 3));namedWindow("srcImage");imshow("srcImage", g_srcImage);createTrackbar("value:", "srcImage", &g_nThresh, g_nMaxThresh,on_ThreshChange);waitKey(0);}}void Test56() {test56::Test();
}
8.5 分水岭算法
在许多实际运用中,我们需要分割图像,但无法从背景图像中获得有用信 息。分水岭算法 (watershed algorithm) 在这方面往往是非常有效的。此算法可 以将图像中的边缘转化成“山脉”,将均匀区域转化为“山谷”,这样有助于分 割目标。
分水岭算法,是一种基于拓扑理论的数学形态学的分割方法,其基本思想 是把图像看作是测地学上的拓扑地貌,图像中每一点像素的灰度值表示该点的 海拔高度,每一个局部极小值及其影响区域称为集水盆,而集水盆的边界则形 成分水岭。分水岭的概念和形成可以通过模拟浸入过程来说明:在每一个局部 极小值表面,刺穿一个小孔,然后把整个模型慢慢浸入水中,随着浸入的加深, 每一个局部极小值的影响域慢慢向外扩展,在两个集水盆汇合处构筑大坝,即 形成分水岭。
分水岭的计算过程是一个迭代标注过程。分水岭比较经典的计算方法是由 L.Vincent 提出的。在该算法中,分水岭计算分两个步骤: 一个是排序过程, 一个是淹没过程。首先对每个像素的灰度级进行从低到高的排序,然后在从低 到高实现淹没的过程中,对每一个局部极小值在h 阶高度的影响域采用先进先 出 (FIFO) 结构进行判断及标注。分水岭变换得到的是输入图像的集水盆图像, 集水盆之间的边界点,即为分水岭。显然,分水岭表示的是输入图像的极大值 点。
也就是说,分水岭算法首先计算灰度图像的梯度;这对图像中的“山谷”或 没有纹理的“盆地”(亮度值低的点)的形成是很有效的,也对“山头”或图像中 有主导线段的“山脉”(山脊对应的边缘)的形成有效。然后开始从用户指定点(或 者算法得到点)开始持续“灌注”盆地直到这些区域连成一片。基于这样产生的 标记就可以把区域合并到0一起,合并后的区域又通聚集的方式进行分割,好像 图像被“填充”起来一样。
8.5.1 实现分水岭算法:watershedO 函 数
函 数watershed 实现的分水岭算法是基于标记的分割算法中的 一种。在把图像 传给函数之前,我们需要大致勾画标记出图像中的期望进行分割的区域,它们被 标记为正指数。所以,每 一 个区域都会被标记为像素值1、2、3等,表示成为 一 个或者多个连接组件。这些标记的值可以使用findContours() 函 数 和drawContours()
函数由二进制的掩码检索出来。不难理解,这些标记就是即将绘制出来的分割区 域的“种子”,而没有标记清楚的区域,被置为0。在函数输出中,每 一 个标记中 的像素被设置为“种子”的值,而区域间的值被设置为- 1。
void watershed(InputArray image, InputOutputArray markers)
- 第 一 个 参 数 ,InputArray 类 型 的src, 输入图像,即源图像,填Mat 类 的 对 象
即可,且需为8位三通道的彩色图像。 - 第 二 个 参 数 ,InputOutputArray 类 型 的markers, 函数调用后的运算结果存在 这里,输入/输出32位单通道图像的标记结果。即这个参数用于存放函数调用后 的输出结果,需和源图片有 一 样的尺寸和类型。
8.5.2 综合示例程序:分水岭算法
namespace test57 {Mat g_maskImage, g_srcImage;Point prevPt(-1, -1);static void on_Mouse(int event, int x, int y, int flags, void*) {if (x < 0 || x >= g_srcImage.cols || y < 0 || y >= g_srcImage.rows) {return;}if (event == EVENT_LBUTTONUP || !(flags & EVENT_FLAG_LBUTTON)) {prevPt = Point(-1, -1);}else if (event == EVENT_LBUTTONDOWN) {prevPt = Point(x, y);}else if (event == EVENT_MOUSEMOVE && (flags & EVENT_FLAG_LBUTTON)) {Point pt(x, y);if (prevPt.x < 0) {prevPt = pt;}line(g_maskImage, prevPt, pt, Scalar::all(255), 5, 8, 0);line(g_srcImage, prevPt, pt, Scalar::all(255), 5, 8, 0);prevPt = pt;imshow("srcImage", g_srcImage);}}void Test() {g_srcImage = imread("mountain.jpg");imshow("srcImage", g_srcImage);Mat srcImage, grayImage;g_srcImage.copyTo(srcImage);cvtColor(g_srcImage, g_maskImage, COLOR_BGR2GRAY);cvtColor(g_maskImage, grayImage, COLOR_GRAY2BGR);g_maskImage = Scalar::all(0);setMouseCallback("srcImage", on_Mouse, 0);while (1) {int c = waitKey(1); // 改为 1 来更流畅地显示if (c == 27) {break;}//恢复原图if ((char)c == '2') {g_maskImage = Scalar::all(0);srcImage.copyTo(g_srcImage);imshow("srcImage", g_srcImage);}else if ((char)c == '1') {int i, j, compCount = 0;std::vector<std::vector<Point>>contours;std::vector<Vec4i>hierarchy;//寻找轮廓findContours(g_maskImage, contours, hierarchy, RETR_CCOMP, CHAIN_APPROX_SIMPLE);if (contours.empty()) {continue;}Mat maskImage(g_maskImage.size(), CV_32S); //复制掩膜maskImage = Scalar::all(0);for (int index = 0; index >= 0; index = hierarchy[index][0]) {compCount++;drawContours(maskImage, contours, index, Scalar::all(compCount), -1, 8, hierarchy, INT_MAX);}if (compCount == 0) continue;//生成随机颜色std::vector<Vec3b>colorTab;for (int i = 0; i < compCount; ++i) {uchar b = theRNG().uniform(0, 255);uchar g = theRNG().uniform(0, 255);uchar r = theRNG().uniform(0, 255);colorTab.push_back(Vec3b(b, g, r));}watershed(srcImage, maskImage); //分水岭算法//将分水岭图像遍历存入watershedImage中Mat watershedImage(maskImage.size(), CV_8UC3);for (int i = 0; i < maskImage.rows; ++i) {for (int j = 0; j < maskImage.cols; ++j) {int index = maskImage.at<int>(i, j);if (index == -1) {watershedImage.at<Vec3b>(i, j) = Vec3b(0, 0, 0); }else if (index <= 0 || index > compCount) {watershedImage.at<Vec3b>(i, j) = Vec3b(0, 0, 0);}else {watershedImage.at<Vec3b>(i, j) = colorTab[index - 1];}}}//混合灰度图和分水岭效果图watershedImage = watershedImage * 0.5 + grayImage * 0.5;imshow("watershed transform", watershedImage);}}}
}void Test57() {test57::Test();
}
8.6 图像修补
在实际应用中,我们的图像常常会被噪声腐蚀,这些噪声或者是镜头上的灰 尘或水滴,或者是旧照片的划痕,或者由于图像的部分本身已经损坏。而“图像 修复”(Inpainting), 就是妙手回春,解决这些问题的良方。图像修复技术简单来说,就是利用那些已经被破坏区域的边缘,即边缘的颜色和结构,繁殖和混合到 损坏的图像中,以达到图像修补的目的。图8.34~8.36就是示例程序截图,演示 将图像中的字迹移除的效果。
8.6.1 实现图像修补:inpaint ( 函 数
在新版OpenCV 中,图像修补技术由inpaint 函数实现,它可以用来从扫描的 照片中清除灰尘和划痕,或者从静态图像或视频中去除不需要的物体。其原型声 明如下。
C++:void inpaint(InputArray src,InputArray inpaintMask,OutputArray
dst,double inpaintRadius,int flags)
- 第 一 个参数,InputArray类 型 的src, 输入图像,即源图像,填Mat 类的对 象即可,且需为8位单通道或者三通道图像。
- 第二个参数,InputArray类型的inpaintMask, 修复掩膜,为8位的单通道 图像。其中的非零像素表示需要修补的区域。
- 第三个参数,OutputArray 类 型 的dst, 函数调用后的运算结果存在这里, 和源图片有一样的尺寸和类型。
- 第四个参数,double 类型的 inpaintRadius,需要修补的每个点的圆形邻域, 为修复算法的参考半径。
- 第五个参数,int 类型的flags,修补方法的标识符,可以是表8.4所示两者 之一。
标识符 | 说明 |
---|---|
INPAINT_NS | 基于Navier-Stokes方程的方法 |
INPAINT_TELEA | Alexandru Telea方法 |
OpenCV2 中INPAINT_NS 和INPAINT_TELEA 标识符可以分别写作CV_ INPAINT_NS 和 CV_INPAINT_TELEA
8.6.2 综合示例程序:图像修补
函数和概念讲解完毕,下面我们依然是学习 一 个以本节所讲内容为核心的示 例程序,将本节所学内容付诸实践,融会贯通。此示例程序会先让我们在图像中 用鼠标绘制出白色的线条破坏图像,然后按下键盘按键【1】或【SPACE】 进 行 图 像修补操作。且如果对自己的绘制不够满意,可以按下键盘按键【2】恢复原始图 像。
namespace test58 {Mat g_srcImage, inpaintMask,srcImage;Point previousPoint(-1, -1);static void On_Mouse(int event,int x,int y,int flags,void*) {if (x < 0 || x >= g_srcImage.cols || y < 0 || y >= g_srcImage.rows) {return;}if (event == EVENT_LBUTTONUP || !(flags & EVENT_FLAG_LBUTTON)) {previousPoint = Point(-1, -1);}else if (event == EVENT_LBUTTONDOWN) {previousPoint = Point(x, y);}else if (event == EVENT_MOUSEMOVE && (flags & EVENT_FLAG_LBUTTON)) {Point pt(x, y);if (previousPoint.x < 0) {previousPoint = pt;}line(inpaintMask, previousPoint, pt, Scalar::all(255), 5, 8, 0);line(g_srcImage, previousPoint, pt, Scalar::all(255), 5, 8, 0);previousPoint = pt;imshow("srcImage", g_srcImage);}}void Test() {g_srcImage = imread("sky.jpg");srcImage = g_srcImage.clone();inpaintMask = Mat::zeros(srcImage.size(), CV_8U);imshow("srcImage", g_srcImage);setMouseCallback("srcImage", On_Mouse, nullptr);while (1) {char c = waitKey(1);if (c == 27) break;if (c == '2') {inpaintMask = Scalar::all(0);srcImage.copyTo(g_srcImage);imshow("srcImage", g_srcImage);}if (c == '1') {Mat inpaintedImage;inpaint(g_srcImage, inpaintMask, inpaintedImage, 3, INPAINT_TELEA); imshow("fixed", inpaintedImage);}}}}void Test58() {test58::Test();
}
8.7 本章小结
本章中,我们先学习了查找轮并绘制轮廓,然后学习了如何寻找到物体的凸 包,接着是使用多边形来包围轮廓,以及计算一个图像的矩。在本章后面几节, 还学习了分水岭算法和图像修补操作的实现方法。
函数名称 | 说明 | 对应讲解章节 |
---|---|---|
BoundingRect | 计算并返回指定点集最外面(up-right)的矩形边 界 | 8.3.1 |
minAreaRect | 寻找可旋转的最小面积的包围矩形 | 8.3.2 |
minEnclosingCircle | 利用一种迭代算法,对给定的2D点集,寻找面 积最小的可包围他们的圆形 | 8.3.3 |
fitEllipse | 用椭圆拟合二维点集 | 8.3.4 |
approxPolyDP | 用指定精度逼近多边形曲线 | 8.3.5 |
moments | 计算多边形和光栅形状的最高达三阶的所有矩 | 8.4.1 |
contourArea | 计算整个轮廓或部分轮廓的面积 | 8.4.2 |
arcLength | 计算封闭轮廓的周长或曲线的长度 | 8.4.3 |
watershed | 实现分水岭算法 | 8.5.1 |
inpaint | 进行图像修补,从扫描的照片中清除灰尘和划痕, 或者从静态图像或视频中去除不需要的物体 | 8.6.1 |
本章示例程序清单
示例程序序号 | 程序说明 | 对应章节 |
---|---|---|
69 | 轮廓查找 | 8.1.3 |
70 | 查找并绘制轮廓 | 8.1.4 |
71 | 凸包检测基础 | 8.2.3 |
72 | 寻找和绘制物体的凸包 | 8.2.4 |
73 | 创建包围轮廓的矩形边界 | 8.3.6 |
74 | 创建包围轮廓的圆形边界 | 8.3.7 |
75 | 使用多边形包围轮廓 | 8.3.8 |
76 | 图像轮廓矩 | 8.4.4 |
77 | 分水岭算法的使用 | 8.5.2 |
78 | 实现图像修补 | 8.6.2 |