OpenCV计算机图像视觉基础学习笔记5——形态学操作

图像形态学操作是基于形状的一系列图像处理操作的合集,主要是基于集合论基础上的形态学数学。形态学有四个基本操作:腐蚀,膨胀,开,闭。

在OCR领域里,图像形态学的操作是离不开的。


膨胀操作与腐蚀操作

它是图像处理中最常用的形态学操作手段。

膨胀: 与卷积操作类似,假设有图像A和结构元素B,结构元素B在A上面移动,其中B定义其中心为锚点,计算B覆盖下A的最大像素值用来替换锚点的像素,其中B作为结构体可以是任意形状。

腐蚀操作与膨胀操作类似,只是把最大像素值换成了最小像素值。它的定义为: 与卷积操作类似,假设有图像A和结构元素B,结构元素B在A上面移动,其中B定义其中心为锚点,计算B覆盖下A的最小像素值用来替换锚点的像素,其中B作为结构体可以是任意形状。

相关API:

1
2
3
4
5
6
7
8
9
10
11
12
getStructuringElement(int shape, Size ksize, Point anchor);
// 第一个参数表示形状
// (比如 MORPH_RECT 矩形\ MORPH_CROSS 十字形\ MORPH_ELLIPSE 曲线形,有时是圆)
// 第二个参数表示大小
// 第三个参数表示锚点,默认是Point(-1, -1),也就是中心像素
// 这个操作还是很有用的,用它获取结构元素,使图像在它的基础上进行各种操作

dilate(src, dst, kernel, Point anchor, int iterations, int borderType, const Scalar& borderValue);
// 膨胀操作,kernel由上面的getStructuringElement得到

erode(src, dst, kernel, Point anchor, int iterations, int borderType, const Scalar& borderValue);
// 腐蚀操作,kernel也由上面的getStructuringElement得到

此外,还可以在图中增加一个滑块用于控制,动态调整结构元素大小。这个操作是通用的,每当用户拖动滑块时都会调用callback函数,以后就可以使用这个操作对图像进行动态编辑了233333

(有一点美中不足的是,trackbar的UI做的相当丑,而且目前我还不知道怎么去改。。不过还好,毕竟这是个图像处理为主的库,应该并不会直接拿来给用户使用)

1
2
createTrackbar(const String &trackbarname, const String winName, int *value, int count, Trackbarcallback func, void *userdata = 0);
// 其中最主要的是callback函数的功能,如果设置为NULL就是说只有update,但是不会调用callback函数。

当操作为膨胀时,效果是这样的:

当操作为腐蚀时,效果是这样的:

源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <opencv2/opencv.hpp>
#include <iostream>
#define debug cout << "ok" << endl;

using namespace cv;
using namespace std;

Mat src, dst;
int element_size = 5;
int max_size = 21;
char input_title[] = "input image";
char output_title[] = "output image";

void Callback_Demo(int, void*) {
int s = element_size * 2 + 1;
Mat structureElement = getStructuringElement(MORPH_RECT, Size(s, s), Point(-1, -1));
//dilate(src, dst, structureElement, Point(-1, -1), 1);
erode(src, dst, structureElement, Point(-1, -1), 1);
imshow(output_title, dst);
}


int main() {
src = imread("D:/QQ_files4/1006607327/FileRecv/MobileFile/img1.jpg");
if (!src.data) {
cout << "could not load image..." << endl;
return -1;
}
char input_title[] = "input image";
char output_title[] = "output image";
namedWindow(input_title, CV_WINDOW_AUTOSIZE);
namedWindow(output_title, CV_WINDOW_AUTOSIZE);
imshow(input_title, src);

createTrackbar("Element size: ", output_title, &element_size, max_size, Callback_Demo);
Callback_Demo(0, 0);

waitKey(0);
return 0;
}

不难看出,膨胀操作和腐蚀操作是什么样的效果呢?

对于膨胀操作,是最大值替换最小值,直观点讲就是白色替换掉黑色,所以膨胀操作之后图上的白色块会越来越大。对于腐蚀操作,是最小值替换最大值,也就是黑色替换掉白色,所以腐蚀操作之后图上的黑色块会越来越大。

那么这个玩意有什么用呢?当你要提取某个图像中的一个大块用于分析时,如果存在同色的小块进行干扰,则你可以使用腐蚀或者膨胀将其消除。

开操作与闭操作

它其实是上面所说的膨胀操作和腐蚀操作的结合。开操作便是先腐蚀再膨胀。它可以去掉小的对象并且不会对大的对象造成很大影响,假设对象是前景色,背景是黑色。相对的,闭操作就是先膨胀再腐蚀了。

相关API:

1
2
3
4
5
6
7
8
morphologyEx(src, dest, int OPT, kernel, int Iteration);
// src和dest是输入图像和输出图像
// OPT是形态学操作类型,有以下几种:
// CV_MOP_OPEN \ CV_MOP_CLOSE
// CV_MOP_GRADIENT \ CV_MOP_TOPHAT
// CV_MOP \ CV_MOP_BLACKHAT
// Mat kernel为结构元素
// int Iteration为迭代次数,默认为1

这次为了方便测试,我用画图随便画了一张测试用图,图上有一个我想保留的白色大块和不想保留的白色小块,用开操作就能很好的把这个问题解决。

(可以看到,下图的矩形四角部分也被侵蚀掉了一些,这是因为模式选取不准确,这里用MORPH_RECT好一些,但是我用了MORPH_ELLIPSE)

源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <opencv2/opencv.hpp>
#include <iostream>
#define debug cout << "ok" << endl;

using namespace cv;
using namespace std;

Mat src, dst;
char input_title[] = "input image";
char output_title[] = "output image";

int main() {
src = imread("D:/imgtest.png");
if (!src.data) {
cout << "could not load image..." << endl;
return -1;
}
char input_title[] = "input image";
char output_title[] = "output image";
namedWindow(input_title, CV_WINDOW_AUTOSIZE);
namedWindow(output_title, CV_WINDOW_AUTOSIZE);
imshow(input_title, src);

Mat kernel = getStructuringElement(MORPH_ELLIPSE, Size(11, 11), Point(-1, -1));
morphologyEx(src, dst, CV_MOP_OPEN, kernel);
imshow(output_title, dst);

waitKey(0);
return 0;
}

对于闭操作,它的操作正好相反。如果说开操作的作用是“把多的删掉”,那闭操作就是“把缺的补回来”。

还是假设我们有一个需要提取的白色大块,但是里面有一个黑色的小洞,这时候就可以使用闭操作把小洞补上。

(下图我画的一些小洞比较大,我把Size调到了(17,17)才算可以去掉。。)

源代码与上面开操作类似。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <opencv2/opencv.hpp>
#include <iostream>
#define debug cout << "ok" << endl;

using namespace cv;
using namespace std;

Mat src, dst;
char input_title[] = "input image";
char output_title[] = "output image";

int main() {
src = imread("D:/imgtest.png");
if (!src.data) {
cout << "could not load image..." << endl;
return -1;
}
char input_title[] = "input image";
char output_title[] = "output image";
namedWindow(input_title, CV_WINDOW_AUTOSIZE);
namedWindow(output_title, CV_WINDOW_AUTOSIZE);
imshow(input_title, src);

Mat kernel = getStructuringElement(MORPH_RECT, Size(17, 17), Point(-1, -1));
morphologyEx(src, dst, CV_MOP_CLOSE, kernel);
imshow(output_title, dst);

waitKey(0);
return 0;
}

形态学梯度操作

它又称为基本梯度操作(其他的还有内部梯度和方向梯度),操作可以简单理解为膨胀减去腐蚀。

只需要把morphologyEx(src, dst, CV_MOP_CLOSE, kernel);里面的CV_MOP_CLOSE换成CV_MOP_GRADIENT就行了。。。。

做出来效果是这样,感觉好迷。。

我加个trackbar调一下试试:

嗯。。。感觉好多了

顶帽操作与黑帽操作

顶帽是原图像与开操作之间的差值图像。改一下模式到CV_MOP_TOPHAT就可以。

我们知道,开操作是先腐蚀再膨胀,可以去掉小对象,原图是一个大块加一堆小块,开操作之后图中仅剩一个大块,二者相减后自然是剩下小对象了。如图所示。

对于黑帽操作,它是闭操作与原图像的差值图像。我们知道,闭操作是“把缺的补回来”,这个图像与原图像做差值的话,剩下的就是通过闭操作补回来的色块。仍然是改一下模式到CV_MOP_BLACKHAT就可以。

提取水平线与垂直线

回顾一下,膨胀输出的像素值是结构元素覆盖下输入图像的最大像素值,腐蚀输出的像素值是结构元素覆盖下输入图像的最小像素值。提取水平线与垂直线是形态学操作的一个应用。图像形态学操作的时候, 可以通过自定义的结构元素实现结构元素的一种效果,即对输入图像的一些对象敏感、另外一些对象不敏感,这样就会让敏感的对象改变而不敏感的对象保留输出。通过使用两个最基本的形态学操作膨胀与腐蚀,使用不同的结构元素实现对输入图像的操作,得到想要的结果。比如说要提取水平线,发现垂直线对提取有干扰,就通过一些结构元素的操作把垂直线膨胀掉或者腐蚀掉,然后提取水平线就简单了。

这个膨胀与腐蚀过程可以使用任意形状。常见的形状:矩形、圆、直线、磁盘形,钻石形,十字形等等各种自定义形状。

提取步骤简单来说是下面这么做:

  • 输入彩色图像imread
  • 转化为灰度图像cvtColor
  • 转换为二值图像adaptiveThreshold
  • 定义结构元素
  • 开操作(腐蚀+膨胀)提取水平线与垂直线

其中转换为二值图像adaptiveThreshold这个操作是没有见过的,这里给出原型。

1
2
3
4
5
6
7
8
void adaptiveThreshold( InputArray src, OutputArray dst, double maxValue, int adaptiveMethod, int thresholdType, int blockSize, double C );
// src为输入的灰度图像
// dst为输出的二值图像
// maxValue为二值图像的最大值
// adaptiveMethod为自适应方法,只能是 ADAPTIVE_THRESH_MEAN_C 或 ADAPTIVE_THRESH_GAUSSIAN_C
// thresholdType为阈值类型
// blockSize为块大小
// C是一个常量,可以是正数,0,负数

这个操作里面有一个小技巧,将输入的src取反(即把src换成~src,让其黑白颠倒,得到的二值图像会比原来更好处理一些)

在没加~的时候是这样的:

加了之后是这样的:

使用对竖线敏感模式擦除竖线之后是这样的:

使用对横线敏感模式擦除横线之后是这样的:

源代码(对横线敏感,若要改成对竖线敏感只需要把vline换成hline就可以了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <opencv2/opencv.hpp>
#include <iostream>
#define debug cout << "ok" << endl;

using namespace cv;
using namespace std;

Mat src, gsrc, binImg, dst;
char input_title[] = "input image";
char gray_title[] = "gray image";
char bin_title[] = "bin image";
char final_title[] = "final image";


int main() {
src = imread("D:/imgtest2.png");
if (!src.data) {
cout << "could not load image..." << endl;
return -1;
}
namedWindow(input_title, CV_WINDOW_AUTOSIZE);
namedWindow(gray_title, CV_WINDOW_AUTOSIZE);
namedWindow(bin_title, CV_WINDOW_AUTOSIZE);
namedWindow(final_title, CV_WINDOW_AUTOSIZE);
imshow(input_title, src);

cvtColor(src, gsrc, CV_BGR2GRAY);
imshow(gray_title, gsrc);

adaptiveThreshold(~gsrc, binImg, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 15, -2);
imshow(bin_title, binImg);

Mat hline = getStructuringElement(MORPH_RECT, Size(src.cols / 16, 1), Point(-1, -1));
Mat vline = getStructuringElement(MORPH_RECT, Size(1, src.rows / 16), Point(-1, -1));

Mat temp;
erode(binImg, temp, vline);
dilate(temp, dst, vline);
imshow(final_title, dst);

waitKey(0);
return 0;
}

拓展:识别简单的验证码

这里我先自己造一个简单的验证码,并且为其加一些干扰。

然后我们采用矩形结构的掩膜,并对其做一次膨胀和腐蚀,然后用bitwise_not取一下反,最后用blur模糊平滑一下,看看效果如何。

嗯。。。可能对于它来说,这个验证码还是有点难。。效果还算可以吧。。不过至少它可以完整的提取出文字信息了。。

源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <opencv2/opencv.hpp>
#include <iostream>
#define debug cout << "ok" << endl;

using namespace cv;
using namespace std;

Mat src, gsrc, binImg, dst;
char input_title[] = "input image";
char gray_title[] = "gray image";
char bin_title[] = "bin image";
char final_title[] = "final image";


int main() {
src = imread("D:/yzm.png");
if (!src.data) {
cout << "could not load image..." << endl;
return -1;
}
namedWindow(input_title, CV_WINDOW_AUTOSIZE);
namedWindow(gray_title, CV_WINDOW_AUTOSIZE);
namedWindow(bin_title, CV_WINDOW_AUTOSIZE);
namedWindow(final_title, CV_WINDOW_AUTOSIZE);
imshow(input_title, src);

cvtColor(src, gsrc, CV_BGR2GRAY);
imshow(gray_title, gsrc);

adaptiveThreshold(~gsrc, binImg, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 15, -2);
imshow(bin_title, binImg);

// Mat hline = getStructuringElement(MORPH_RECT, Size(src.cols / 16, 1), Point(-1, -1));
// Mat vline = getStructuringElement(MORPH_RECT, Size(1, src.rows / 16), Point(-1, -1));
Mat kernel = getStructuringElement(MORPH_RECT, Size(2, 2), Point(-1, -1));

Mat temp;
erode(binImg, temp, kernel);
dilate(temp, dst, kernel);

bitwise_not(dst, dst);
blur(dst, dst, Size(1, 1), Point(-1, -1));

imshow(final_title, dst);

waitKey(0);
return 0;
}

-------------本文结束,感谢您的阅读转载请注明原作者及出处-------------


本文标题:OpenCV计算机图像视觉基础学习笔记5——形态学操作

文章作者:Shawn Zhou

发布时间:2019年08月04日 - 10:08

最后更新:2019年08月07日 - 14:08

原始链接:http://shawnzhou.xyz/2019/08/04/19-08-04-01/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

知识无价,码字不易。对您有用,敬请打赏。金额随意,感谢关心。
0%