一、学习目标
- 理解什么是开运算、闭运算、形态学梯度、顶帽和黑帽
- 学会使用OpenCV实现上述的图像形态学操作
- 使用综合性的例子进行实验
二、各种操作简介
1、开运算
开运算:先腐蚀后膨胀,表达公式为:
dst = open(src, element) = dilate(erode(src, element), element)
开运算可以用来消除图像中的细小对象(前提是黑色为类背景,类白色为前景),在纤细点处分离物体和平滑较大物体的边界而又不明显改变其面积和形状。
例如,看看下面的例子。左边的图像是原始图像,右边的图像是应用开运算变换后的结果。我们可以看到白色小点消失了。
OpenCV中,使用函数cv::morphologyEx实现开运算,其函数原型为:
void cv::morphologyEx ( InputArray src,
OutputArray dst,
int op,
InputArray kernel,
Point anchor = Point(-1,-1),
int iterations = 1,
int borderType = BORDER_CONSTANT,
const Scalar & borderValue = morphologyDefaultBorderValue() )
- 参数 src: 源图像。信道的数量可以是任意的。深度应该是CV_8U、CV_16U、CV_16S、CV_32F或CV_64F之一
- 参数 dst: 大小和类型与源图像相同的输出图像
- 参数 op: 指定形态学操作的类型,请参见MorphTypes,实现开操作时指定为MORPH_OPEN
- 参数 kernel: 结构元素。它可以使用getStructuringElement创建
- 参数 anchor: 锚定在内核中的位置。负值表示锚点在内核中心
- 参数 iterations: 开运算(其它运算也是如此)迭代的次数
- 参数 borderType: 图像边界处像素填充方法,见BorderTypes不支持BORDER_WRAP
- 参数 borderValue: 指定图像边界像素填充方法为常量时的配套函数
函数cv::morphologyEx可以使用腐蚀和膨胀作为基本操作来执行高级形态学操作。任何操作都可以就地完成。对于多通道图像,每个通道都是独立处理的。通过指定不同的op实现不同的形态学操作,op的取值见下表:
参数取值 | 取值说明 |
---|---|
MORPH_OPEN | 开运算,dst=open(src,element)=dilate(erode(src,element)) |
MORPH_CLOSE | 闭运算,dst=close(src,element)=erode(dilate(src,element)) |
MORPH_GRADIENT | 形态学梯度,dst=morph_grad(src,element)=dilate(src,element)−erode(src,element) |
MORPH_TOPHAT | 顶帽,dst=tophat(src,element)=src−open(src,element) |
MORPH_BLACKHAT | 黑帽,dst=blackhat(src,element)=close(src,element)−src |
MORPH_HITMISS | “hit or miss”,只支持CV_8UC1二进制图像 |
可见,对于形态学操作的其它操作,也只需要改变op的值即可,所以后面的相关操作就只是简单地介绍原理,相应的函数API不再赘述。
注意: 迭代次数是应用腐蚀或膨胀操作的次数。例如,具有2次迭代的开运算(MORPH_OPEN)等效于依次应用:腐蚀->腐蚀->膨胀->膨胀(而不是腐蚀->膨胀->腐蚀->膨胀)。
使用实例:
先上传一张实验用图供大家下载:
实验代码:
#include<opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int main(int argc,char **argv)
{
char fileName[] = "O:\\CSDN\\MORPH.png";
Mat src = imread(fileName, IMREAD_COLOR);
if (src.empty())
{
cout << "couldn't open image: " << fileName << ",please check out!\n";
system("pause");
return EXIT_FAILURE;
}
// 定义结构元素,可以自行尝试不同的SIZE,去除小物体(噪声)的效果不同
Mat kernel = getStructuringElement(MORPH_RECT, Size(5, 5), Point(-1, -1));
Mat dst;
// 执行开运算
morphologyEx(src, dst, MORPH_OPEN, kernel);
namedWindow("Original", WINDOW_AUTOSIZE);
moveWindow("Original", 200, 200);
namedWindow("MORPH_OPEN", WINDOW_AUTOSIZE);
moveWindow("MORPH_OPEN", src.cols + 200, 200);
imshow("Original", src);
imshow("MORPH_OPEN", dst);
waitKey(0);
system("pause");
return EXIT_SUCCESS;
}
对比右边开运算的结果,很多较小的图形已经被去除,当然也还有一些没去掉,这个就与Size的取值有关,可以自行尝试。
2、闭运算
闭运算:先膨胀后腐蚀。其公式为:
dst=close(src,element)=erode(dilate(src,element))
闭运算可以用来填充目标内部的细小孔洞(fill hole),将断开的邻近目标连接,在不明显改变物体面积和形状的情况下平滑其边界。
如,闭运算可以达到下图的效果:
OpenCV实现闭运算的函数API不再赘述,直接看实例:
#include<opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int main(int argc,char **argv)
{
char fileName[] = "O:\\CSDN\\MORPH.png";
Mat src = imread(fileName, IMREAD_COLOR);
if (src.empty())
{
cout << "couldn't open image: " << fileName << ",please check out!\n";
system("pause");
return EXIT_FAILURE;
}
Mat kernel = getStructuringElement(MORPH_RECT, Size(7, 7), Point(-1, -1));
Mat dst;
morphologyEx(src, dst, MORPH_CLOSE, kernel);
namedWindow("Original", WINDOW_AUTOSIZE);
moveWindow("Original", 200, 200);
namedWindow("MORPH_CLOSE", WINDOW_AUTOSIZE);
moveWindow("MORPH_CLOSE", src.cols + 200, 200);
imshow("Original", src);
imshow("MORPH_CLOSE", dst);
waitKey(0);
system("pause");
return EXIT_SUCCESS;
}
从上图可以看出,许多的黑色小孔已经被堵上,但比较大的空是难以完全填充上的。
3、形态学梯度
形态学梯度操作能描述图像亮度变化的剧烈程度;当我们想要突出高亮区域的外围时,则可以选用形态学梯度来突出边缘,可以保留物体的边缘轮廓。
常见的几种梯度:
- 基本梯度:用膨胀后的图像减去腐蚀后的图像得到的差值图像。
- 内部梯度:用原图像减去腐蚀之后的图像得到的差值图像
- 外部梯度:用膨胀之后得到的图像减去原图像得到的差值图像
- 方向梯度:使用X方向与Y方向的直线作为结构元素,X的结构元素分别膨胀与腐蚀得到图像之后求差值为X方向梯度,也可以理解为用X的结构元素进行闭运算。用Y的结构元素分别膨胀与腐蚀得到的图像之后求差值为Y方向梯度,也可以理解为用Y的结构元素进行闭运算
如使用基本梯度可以得到如下效果:
这相当于提出了目标的外部轮廓或边缘。使用实例如下:
#include<opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int main(int argc,char **argv)
{
char fileName[] = "O:\\CSDN\\MORPH.png";
Mat src = imread(fileName, IMREAD_COLOR);
if (src.empty())
{
cout << "couldn't open image: " << fileName << ",please check out!\n";
system("pause");
return EXIT_FAILURE;
}
Mat kernel = getStructuringElement(MORPH_RECT, Size(5, 5), Point(-1, -1));
Mat dst;
morphologyEx(src, dst, MORPH_GRADIENT, kernel);
namedWindow("Original", WINDOW_AUTOSIZE);
moveWindow("Original", 200, 200);
namedWindow("MORPH_GRADIENT", WINDOW_AUTOSIZE);
moveWindow("MORPH_GRADIENT", src.cols + 200, 200);
imshow("Original", src);
imshow("MORPH_GRADIENT", dst);
waitKey(0);
system("pause");
return EXIT_SUCCESS;
}
4、顶帽
顶帽是原图与原图的开运算的差值图像。开运算放大了裂缝或者局部低亮度的区域,所以,从原图中减去开运算后的图,得到的结果突出了比原图轮廓周围的区域更明亮的区域,这个操作与选择的核的大小有关。TopHat运算一般用来分离比邻近点亮一些的斑块,可以使用这个运算提取背景。
其公式为:
dst=tophat(src,element)=src−open(src,element)
有如下效果:
使用实例为:
#include<opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int main(int argc,char **argv)
{
char fileName[] = "O:\\CSDN\\MORPH.png";
Mat src = imread(fileName, IMREAD_COLOR);
if (src.empty())
{
cout << "couldn't open image: " << fileName << ",please check out!\n";
system("pause");
return EXIT_FAILURE;
}
Mat kernel = getStructuringElement(MORPH_RECT, Size(5, 5), Point(-1, -1));
Mat dst;
morphologyEx(src, dst, MORPH_TOPHAT, kernel);
namedWindow("Original", WINDOW_AUTOSIZE);
moveWindow("Original", 200, 200);
namedWindow("MORPH_TOPHAT", WINDOW_AUTOSIZE);
moveWindow("MORPH_TOPHAT", src.cols + 200, 200);
imshow("Original", src);
imshow("MORPH_TOPHAT", dst);
waitKey(0);
system("pause");
return EXIT_SUCCESS;
}
5、黑帽
黑帽是闭运算结果与原图的差值图像。黑帽运算的结果突出了比原图轮廓周围区域更暗的区域,所以黑帽运算用来分离比邻近点暗一些的斑块。
其公式为:
dst=blackhat(src,element)=close(src,element)−src
可以达到如下的效果:
使用实例如下:
#include<opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int main(int argc,char **argv)
{
char fileName[] = "O:\\CSDN\\MORPH.png";
Mat src = imread(fileName, IMREAD_COLOR);
if (src.empty())
{
cout << "couldn't open image: " << fileName << ",please check out!\n";
system("pause");
return EXIT_FAILURE;
}
Mat kernel = getStructuringElement(MORPH_RECT, Size(5, 5), Point(-1, -1));
Mat dst;
morphologyEx(src, dst, MORPH_BLACKHAT, kernel);
namedWindow("Original", WINDOW_AUTOSIZE);
moveWindow("Original", 200, 200);
namedWindow("MORPH_BLACKHAT", WINDOW_AUTOSIZE);
moveWindow("MORPH_BLACKHAT", src.cols + 200, 200);
imshow("Original", src);
imshow("MORPH_BLACKHAT", dst);
waitKey(0);
system("pause");
return EXIT_SUCCESS;
}
三、综合实例
#include<opencv2/opencv.hpp>
using namespace cv;
using namespace std;
string TYPE_BAR = "Morph Type:\n 0:Open\n 1:Close\n 2:Gradient\n 3:TopHat\n 4:BlackHat";
void dealMorphologyEx(int, void* img);
int main(int argc,char **argv)
{
char fileName[] = "O:\\CSDN\\MORPH.png";
Mat src = imread(fileName, IMREAD_COLOR);
if (src.empty())
{
cout << "couldn't open image: " << fileName << ",please check out!\n";
system("pause");
return EXIT_FAILURE;
}
namedWindow("Original", WINDOW_AUTOSIZE);
moveWindow("Original", 200, 200);
namedWindow("MORPH", WINDOW_AUTOSIZE);
moveWindow("MORPH", src.cols + 200, 200);
int Type = 0;
int kernelSize = 0;
int iteration = 0;
createTrackbar(TYPE_BAR, "MORPH", &Type, 4, 0, 0);
createTrackbar("Kernel Size:", "MORPH", &kernelSize, 21, dealMorphologyEx, (void*)&src);
createTrackbar("Iteration times:", "MORPH", &iteration, 4, 0, 0);
imshow("MORPH", src);
imshow("Original", src);
waitKey(0);
system("pause");
return EXIT_SUCCESS;
}
void dealMorphologyEx(int, void* img)
{
int type = getTrackbarPos(TYPE_BAR, "MORPH") + 2;
int kernelSize = getTrackbarPos("Kernel Size:", "MORPH");
int iteration = getTrackbarPos("Iteration times:", "MORPH") + 1;
Mat src = ((Mat*)img)->clone();
Mat dst;
Mat kernel = getStructuringElement(MORPH_RECT, Size(2 * kernelSize + 1, 2 * kernelSize + 1));
morphologyEx(src, dst, type, kernel, Point(-1, -1), iteration);
imshow("MORPH", dst);
}
运行上面程序,便可以自由选择形态学操作了,可以根据需要设定kernelSize和迭代次数,当然也可以自行添加结构元素类型。
注意上图红色框中出现了一个问题,跨行的trackbar名称不能正确显示,本人也一直在寻找解决方案,知道问题的小伙伴可以私信我,谢谢。