腐蚀

定义B对A的腐蚀为:

AB={z(B)zA}A\ominus B = \{z|(B)_z\subseteq A\}

其中,A是前景像素的一个集合,B是一个结构元,z项是前景像素值。

腐蚀的目的是去除图像中的某些部分以及会缩小细化目标

但是,对于白色背景,黑色目标变大;对于黑色背景,白色目标变小

可以理解为结构元像素是白色的,腐蚀是腐蚀图像中的白色像素,白色像素被腐蚀,则黑色元素膨胀。

代码如下:

相关函数有:

1
2
3
4
5
double threshold(InputArray src,
OutputArray dst,
double thresh,
double maxval,
int type); // 二值化

参数列表有:

  • src:输入数组(多通道,8位或32位浮点)。
  • dst:与src相同大小和类型、相同通道数的输出数组。
  • thresh:阈值.
  • maxval:dst图像中的最大值。
  • type:阈值。

type可选有:

  • cv::THRESH_BINARY,效果:

dst(x,y)={maxval,if src(x,y)>thresh0,otherwisedst(x,y)= \begin{cases} maxval,\quad if\ src(x,y)> thresh\\ 0, \quad otherwise \end{cases}

  • cv::THRESH_BINARY_INV,效果:

dst(x,y)={0,if src(x,y)>threshmaxval,otherwisedst(x,y)= \begin{cases} 0,\quad if\ src(x,y)> thresh\\ maxval, \quad otherwise \end{cases}

  • cv::THRESH_TRUNC,效果:

dst(x,y)={threshold,if src(x,y)>threshsrc(x,y),otherwisedst(x,y)= \begin{cases} threshold,\quad if\ src(x,y)> thresh\\ src(x,y), \quad otherwise \end{cases}

  • cv::THRESH_TOZERO,效果:

dst(x,y)={src(x,y),if src(x,y)>thresh0,otherwisedst(x,y)= \begin{cases} src(x,y),\quad if\ src(x,y)> thresh\\ 0, \quad otherwise \end{cases}

  • cv::THRESH_TOZERO_INV,效果:

dst(x,y)={0,if src(x,y)>threshsrc(x,y),otherwisedst(x,y)= \begin{cases} 0,\quad if\ src(x,y)> thresh\\ src(x,y), \quad otherwise \end{cases}

  • cv::THRESH_OTSU,效果:标志,使用Otsu算法选择最优阈值。

  • cv::THRESH_TRIANGLE,效果:标志,使用三角算法选择最优阈值。

  • cv::THRESH_MASK

1
2
3
Mat getStructuringElement(int shape,
Size ksize,
Point anchor = Point(-1,-1)); // 构造结构元函数

参数列表有:

  • shape:结构元形状,0表示矩形,1表示十字架,2表示椭圆。
  • ksize:结构元大小。
  • anchor:结构元中心点所在位置。
1
2
3
4
5
6
7
void erode(InputArray src,
OutputArray dst,
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:输出与src相同大小和类型的图像。
  • kernel:用于腐蚀的结构元。
  • anchor:中心点在元素中的位置。
  • iterations:应用侵蚀的次数。
  • borderType:推断图像外部像素的边界模式。
  • borderValue:当边界为常数时的边界值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <opencv2/opencv.hpp>

int main()
{
cv::Mat input = cv::imread("src/test.jpg", 0);
cv::Mat image_bw, image_erosion; // 二值化图像和腐蚀后的图像

cv::threshold(input, image_bw, 100, 255, cv::THRESH_BINARY); // 二值化
cv::Mat se = cv::getStructuringElement(0, cv::Size(3, 3)); // 腐蚀核
cv::erode(image_bw, image_erosion, se, cv::Point(-1, -1), 3); // 腐蚀

cv::namedWindow("before", cv::WINDOW_NORMAL);
cv::imshow("before", input);

cv::namedWindow("二值化后", cv::WINDOW_NORMAL);
cv::imshow("二值化后", image_bw);

cv::namedWindow("after", cv::WINDOW_NORMAL);
cv::imshow("after", image_erosion);

cv::waitKey(0);
return 0;
}

效果展示:

膨胀

定义B对A的膨胀为:

AB={z[(B^)zA]A}A\oplus B = \{z|[(\hat{B})_z\cap A]\subseteq A \}

其中,A是前景像素的一个集合,B是一个结构元,z项是前景像素值。

膨胀的目的是增大图像中的目标,或者填充、连接某些目标

但是,对于白色背景,黑色目标变小;对于黑色背景,白色目标变大

可以理解为结构元像素是白色的,膨胀是膨胀图像中的白色像素,白色像素被膨胀,则黑色元素腐蚀。

代码如下:

相关函数有:

1
2
3
4
5
6
7
void dilate(InputArray src,
OutputArray dst,
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:输出与src相同大小和类型的图像。
  • kernel:用于腐蚀的结构元。
  • anchor:中心点在元素中的位置。
  • iterations:应用侵蚀的次数。
  • borderType:推断图像外部像素的边界模式。
  • borderValue:当边界为常数时的边界值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <opencv2/opencv.hpp>

int main()
{
cv::Mat input = cv::imread("src/test.jpg", 0);
cv::Mat image_bw, image_dilate; // 二值化图像和膨胀后的图像

cv::threshold(input, image_bw, 100, 255, cv::THRESH_BINARY); // 二值化
cv::Mat se = cv::getStructuringElement(0, cv::Size(3, 3)); // 膨胀核
cv::dilate(image_bw, image_dilate, se, cv::Point(-1, -1), 3); // 膨胀

cv::namedWindow("before", cv::WINDOW_NORMAL);
cv::imshow("before", input);

cv::namedWindow("二值化后", cv::WINDOW_NORMAL);
cv::imshow("二值化后", image_bw);

cv::namedWindow("after", cv::WINDOW_NORMAL);
cv::imshow("after", image_dilate);

cv::waitKey(0);
return 0;
}

效果展示:

开运算与闭运算

结构元B对集合A的开运算定义为:

AB=(AB)BA\circ B=(A\ominus B)\oplus B

即B对A先腐蚀,接着B对腐蚀结果膨胀。

  • 作用是:平滑物体轮廓、断开狭窄的狭颈、消除细长的突出和物体。

结构元B对集合A的闭运算定义为:

AB=(AB)BA\bullet B=(A\oplus B)\ominus B

即B对A先膨胀,接着B对膨胀结果腐蚀。

  • 作用是:弥合狭窄的狭颈或断裂处、消除小孔、填补轮廓缝隙。

代码如下:

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
#include <opencv2/opencv.hpp>

void imageOpenOperation(cv::Mat input, cv::Mat &output)
{
output = input.clone();
cv::Mat se = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3));
cv::erode(input, output, se);
cv::dilate(output, output, se);
}

void imageCloseOperation(cv::Mat input, cv::Mat &output)
{
output = input.clone();
cv::Mat se = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3));
cv::dilate(input, output, se);
cv::erode(output, output, se);
}

int main()
{
cv::Mat input = cv::imread("src/pic2.png", 0);
cv::Mat img_bw, result1, result2; // 二值化后的图像、开运算结果、比运算结果
cv::threshold(input, img_bw, 100, 255, cv::THRESH_BINARY); // 二值化

imageOpenOperation(img_bw, result1);
imageCloseOperation(img_bw, result2);

cv::namedWindow("before", cv::WINDOW_NORMAL);
cv::namedWindow("开运算后", cv::WINDOW_NORMAL);
cv::namedWindow("闭运算后", cv::WINDOW_NORMAL);

cv::imshow("before", img_bw);
cv::imshow("开运算后", result1);
cv::imshow("闭运算后", result2);

cv::waitKey(0);
return 0;
}

morphologyEx() 函数的运用

膨胀腐蚀、开运算闭运算、击中-击不中变换、顶帽变换与底帽变换、形态学梯度

相关函数原型

1
2
3
4
5
6
7
8
void 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 or CV_64F中的一种。

  • dst:输出图像,数据类型与大小和输入图像一样。

  • op:形态学处理的类型。

MORPH_ERODE = 0:腐蚀处理

MORPH_DILATE = 1:膨胀处理

MORPH_OPEN = 2:开运算处理

MORPH_CLOSE = 3:闭运算处理

MORPH_GRADIENT = 4:形态学梯度

MORPH_TOPHAT = 5:顶帽变换

MORPH_BLACKHAT = 6:黑帽变换

MORPH_HITMISS = 7 :击中-击不中变换

  • kernel:结构元矩阵。

  • iterations:腐蚀膨胀处理的次数,默认值为1;如果是开运算闭运算,次数表示先腐蚀或者膨胀几次,再膨胀腐蚀几次,而不是开运算闭运算几次。

  • borderType:图像边框插值类型,默认类型为固定值填充。

  • borderValue:边界值(如果是恒定边界)。默认值具有特殊含义。

进行腐蚀膨胀

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <opencv2/opencv.hpp>

int main()
{
cv::Mat input = cv::imread("src/pic2.png", 0);
cv::Mat img_bw, result1, result2;
cv::threshold(input, img_bw, 100, 255, cv::THRESH_BINARY); // 二值化

cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3));
cv::morphologyEx(img_bw, result1, 0, kernel); // 腐蚀
cv::morphologyEx(img_bw, result2, 1, kernel); // 膨胀

cv::namedWindow("before", cv::WINDOW_NORMAL);
cv::namedWindow("腐蚀结果", cv::WINDOW_NORMAL);
cv::namedWindow("膨胀结果", cv::WINDOW_NORMAL);

cv::imshow("before", img_bw);
cv::imshow("腐蚀结果", result1);
cv::imshow("膨胀结果", result2);

cv::waitKey(0);
return 0;
}

效果展示:

进行开运算和闭运算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <opencv2/opencv.hpp>

int main()
{
cv::Mat input = cv::imread("src/pic2.png", 0);
cv::Mat img_bw, result1, result2;
cv::threshold(input, img_bw, 100, 255, cv::THRESH_BINARY); // 二值化

cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3));
cv::morphologyEx(img_bw, result1, 2, kernel); // 开运算
cv::morphologyEx(img_bw, result2, 3, kernel); // 闭运算

cv::namedWindow("before", cv::WINDOW_NORMAL);
cv::namedWindow("开运算结果", cv::WINDOW_NORMAL);
cv::namedWindow("闭运算结果", cv::WINDOW_NORMAL);

cv::imshow("before", img_bw);
cv::imshow("开运算结果", result1);
cv::imshow("闭运算结果", result2);

cv::waitKey(0);
return 0;
}

效果展示:

击中-击不中变换

原理:

IB={z(B)zI}I\circledast B=\{z|(B)_z\subseteq I\}

可以检测图像中满足结构元B的所有像素点,如:

图中结构元B中深色像素为前景,白色表示为背景, × 表示不关心像素(即不影响结果)。

击中-击不中变换是形状检测的基本工具,寻找与结构元相符合的像素。

在击中击不中变换的结构元中,-1表示背景,1表示前景,0表示不关心。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <opencv2/opencv.hpp>

int main()
{
cv::Mat input = cv::imread("src/rice.jpg", 0);
cv::Mat img_bw, result;
cv::threshold(input, img_bw, 100, 255, cv::THRESH_BINARY); // 二值化

cv::Mat kernel = (cv::Mat_<float>(3, 3) << 1, 1, -1, 1, 1, -1, 1, 1, -1);

cv::morphologyEx(img_bw, result, 7, kernel);

cv::namedWindow("before", cv::WINDOW_NORMAL);
cv::namedWindow("击中-击不中变换结果", cv::WINDOW_NORMAL);

cv::imshow("before", img_bw);
cv::imshow("击中-击不中变换结果", result);

cv::waitKey(0);
return 0;
}

效果展示:

顶帽变换与底帽变换

顶帽变换(礼帽变换)

  • 是原图像与开运算结果图之差。

  • 作用:提取出细线状的部分或者噪声。

底帽变换(黑帽变换)

  • 是闭运算与原图像结果图之差。

  • 作用:得到图像内部的小孔,或前景色的小黑点。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <opencv2/opencv.hpp>

int main()
{
cv::Mat input = cv::imread("src/rice.jpg", 0);
cv::Mat img_bw, result1, result2;
cv::threshold(input, img_bw, 100, 255, cv::THRESH_BINARY);
cv::Mat kernel = cv::getStructuringElement(0, cv::Size(5, 5));

cv::morphologyEx(img_bw, result1, 5, kernel);
cv::morphologyEx(img_bw, result2, 6, kernel);

cv::namedWindow("before", cv::WINDOW_NORMAL);
cv::namedWindow("顶帽变换结果", cv::WINDOW_NORMAL);
cv::namedWindow("底帽变换结果", cv::WINDOW_NORMAL);

cv::imshow("before", img_bw);
cv::imshow("顶帽变换结果", result1);
cv::imshow("底帽变换结果", result2);

cv::waitKey(0);
return 0;
}

效果截图:

形态学梯度

  • 膨胀图与腐蚀图之差;

  • 对二值图进行这一操作可以得到图像中白色区域的边界,因此可以用形态学梯度来保留物体的边界轮廓。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <opencv2/opencv.hpp>

int main()
{
cv::Mat input = cv::imread("src/pic2.png", 0);
cv::Mat img_bw, result;
cv::threshold(input, img_bw, 100, 255, cv::THRESH_BINARY_INV);
cv::Mat kernel = cv::getStructuringElement(0, cv::Size(5, 5));

cv::morphologyEx(img_bw, result, 4, kernel);

cv::namedWindow("before", cv::WINDOW_NORMAL);
cv::namedWindow("形态学梯度结果", cv::WINDOW_NORMAL);

cv::imshow("before", img_bw);
cv::imshow("形态学梯度结果", result);

cv::waitKey(0);
return 0;
}

效果截图:

连通域分析

连通域:指图像中具有相同像素值且位置相邻的像素组成的区域。

connectedComponents 函数

1
2
3
4
int connectedComponents(InputArray image,
OutputArray labels,
int connectivity = 8,
int ltype = CV_32S);

参数如下:

  • image:输入图像,待标记8位单通道图像。

  • labels:输出图像,目标标记图像。

  • connectivity:连通域大小,四连通域还是八连通域。

  • 输出类型:CV32SCV_16U ,默认是 CV_32S

  • 返回连通域个数。

代码如下:

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
49
50
#include <opencv2/opencv.hpp>
#include <iostream>

int main()
{
cv::Mat input = cv::imread("src/rice.jpg");
cv::Mat img_bw, connectimg;
cv::cvtColor(input, img_bw, cv::COLOR_BGR2GRAY);
cv::threshold(img_bw, img_bw, 100, 255, cv::THRESH_BINARY_INV);

// 做一些膨胀和腐蚀使得图像连通域明显
cv::Mat element = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(5, 5));
cv::erode(img_bw, img_bw, element, cv::Point(-1, -1), 2);
cv::dilate(img_bw, img_bw, element, cv::Point(-1, -1), 2);

int num = cv::connectedComponents(img_bw, connectimg, 8, CV_16U);
std::cout << "连通域个数:" << num << std::endl;

cv::namedWindow("原图", cv::WINDOW_NORMAL);
cv::imshow("原图", img_bw);

//以不同颜色标记出不同的连通域
cv::Mat result = cv::Mat::zeros(img_bw.size(), CV_8UC3); //定义标记结果图像
//定义五种颜色
std::vector<cv::Vec3b> color;
color.push_back(cv::Vec3b(0, 0, 255));
color.push_back(cv::Vec3b(0, 255, 0));
color.push_back(cv::Vec3b(255, 0, 0));
color.push_back(cv::Vec3b(0, 255, 255));
color.push_back(cv::Vec3b(255, 255, 0));

for (int i = 0; i < result.cols; i ++)
{
for (int j = 0; j < result.rows; j ++)
{
int label = connectimg.at<uint16_t>(i, j);
if (label == 0)
{
continue; //背景的黑色不改变
}
result.at<cv::Vec3b>(i, j) = color[label % 5];
}
}

cv::namedWindow("标记连通域后", cv::WINDOW_NORMAL);
cv::imshow("标记连通域后", result);

cv::waitKey(0);
return 0;
}

效果截图:

注:黑色也算一个连通域

connectedComponentsWithStats 函数

函数:

1
2
3
4
5
6
int connectedComponentsWithStats(InputArray image,
OutputArray labels,
OutputArray stats,
OutputArray centroids,
int connectivity = 8,
int ltype = CV_32S);

参数如下:

  • image:要标记的8位单通道图像

  • labels:目标标签图像

  • stats:每个标签的统计输出,是一个5列的矩阵,每一行对应每个连通区域的外接矩形的左上角坐标x、y,以及外接矩形的宽高width、height和面积area。

  • centroids:连通域中心点,数据类型CV_64F。

  • connectivity:8或4分别用于八连通域和四连通域。

  • ltype输出图像标签类型。目前支持CV_32S和CV_16U。

  • 返回连通域个数。

其中, stats包含了标签为i的连通域的一些信息,可以如下访问标签为i的连通域的面积:

1
stats.at<int>(i, CC_STAT_AREA)

连通域外接矩形的参数:

1
2
3
4
5
6
7
8
// 矩形左上角x
x = stats.at<int>(max_idx, cv::CC_STAT_LEFT);
// 矩形左上角y
y = stats.at<int>(max_idx, cv::CC_STAT_TOP);
// 矩形高
h = stats.at<int>(max_idx, cv::CC_STAT_HEIGHT);
// 矩形宽
w = stats.at<int>(max_idx, cv::CC_STAT_WIDTH);