当前位置:   article > 正文

(OpenCV)图像算法笔记 (Part 1)_图像算法 opencv

图像算法 opencv

1. opecv读取与显示图像

读入与显示图像是opencv中最最简单也是最基础的操作。虽然只是使用两个简单的API就可以实现,但是其中有一些小的细节还是应该注意。`

#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;

int main()
{
	Mat src = imread("C:/Users/51731/Desktop/2.jpeg");
	if (!src.data)
	{
		cout << "no image" << endl;
		return 0;
	}

	namedWindow("frame_1", WINDOW_AUTOSIZE);
	imshow("frame_1", src);
	waitKey(0);
	return 1;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

使用cv::imread() 方法来读出 一张照片,这个函数返回的类型是一个Mat 类型。将在下一节记录这个数据类型的特征和使用方法。使用在C++中使用opencv读取图像的时候,最好是在读取之后,使用if判断一下照片是否读取成功。因为如果是因为图片读取出现了错误,C++的报错是如下图所示。如果程序结构简单明了,这个Bug很容易被发现并解决,但是如果程序结构复杂,出现这样的错误就会比较不好找。

在这里插入图片描述
除以之外,namedWindow这个是打开一个新的窗口,窗口的尺寸可以使用第二个参数进行调整。在函数imshow的第一个参数上使用创建好的窗口就可以将图像显示在创建好的窗口上了。如果没有提前创建窗口,则imshow会以第一个参数输入的字符串为窗口名字生成一个新的窗口。waitKey()则是让窗口持续显示在窗口上,可以用参数来调整当前窗口的停留时间。如果是

  • waitkey(0): 按任意键继续。
  • waitkey(500) : 等待500毫秒之后继续。

读取与显示图像大致就是这样,很容易理解和掌握。

2. Mat 对象的性质和使用

Mat对象是Opencv 提供的一个非常重要的数据类型,为我们提供了一个好用的容器来承载从硬盘中读入的图像。它存在的主要意图是用来存储任意大小的array。

The cv::Mat class is used to represent dense arrays of any number of dimensions. In
this context, dense means that for every entry in the array, there is a data value stored
in memory corresponding to that entry, even if that entry is zero

由上述的引用可以发现,Mat这个数据类型是用 来存储dense arrays的,如果我们遇到一个很大的稀疏矩阵,也一股脑的使用Mat数据类型来进行存储的话,理论上当然可行但是就会造成很多比必要的内存消耗,稀疏矩阵大部分都是0。对于这个问题,Opencv也提供了另一种数据类型 cv::SparseMat() 来应对稀疏矩阵的情况。
cv::Mat 对象的常用属性:

  • rows : 该矩阵有多少行 ( indicating the number of rows)
  • cols: 该矩阵有多少列 (indicating the number of columns)
  • dims: 该矩阵的维度 (indicating the dimension)
  • flag: 该矩阵的一些信息 (flags element signaling the contents of the array)
    需要记录一下的是 flag 是一个 int 类型的变量,每一位数都代表着独特意义。具体可以参考这一篇文章
  • ptr<>(i,j):获取第i行 第j列的指针。<>中是指针类型。
    Mat 还有很多属性,但是这几个属性货比较常用一些。在接下来的笔记中,也会大量的使用到Mat的这几个属性。

2.1 创建Mat对象

创建Mat对象的方法有很多种,第一种就是直接创建一个Mat 类型的变量 然后直接给他赋值即可。

Mat src = (Mat_<int>(3,3) << -1, 0, -1, 0, 5, 0, -1, 0, -1);
  • 1

就像上述代码就创建出了一个 3 * 3 大小, 成员类型是Int的一个Mat。这种方法可用于创建一个较小且值固定的Mat对象。比较适合用来创建小的卷积核。

除了这个方法还可以使用

Mat src;
src.create(3,3,CV_8U);
  • 1
  • 2

上述代码创建了一个 3 * 3 , 数据类型是CV_8U 的 src 的Mat对象。但是当前的src这个Mat 类型的变量中的值是根据数据类型随机的。比如CV_8U得到的结果是:
在这里插入图片描述
但是如果是CV_16S,生成的Mat对象的值就变成:
在这里插入图片描述
(这些小细节,可能现在看上去没有很大的作用,但是也许在未来的某个时刻,会成为debug的关键。)
在设置Mat的尺寸的时候,除了像上述代码一样,还可以使用Size(3,3),可以得到和上述代码一样的效果。

new_.create(Size(3, 3), CV_16S);
  • 1

create方法创建的Mat对象中的值都是“无意义的”,如果想要创建一个全是1 或者全是0 的矩阵,可以如下操作:

src = Mat::ones(Size(3, 3), CV_16S);
src = Mat::zeros(Size(3, 3), CV_16S);
  • 1
  • 2

在实际的操作中经常会遇到,需要创建一个与某一个图像一样大小的Mat对象,这个时候可以先获取图像的rows和cols,然后使用create 方法来创建

Mat src = imread("C:/Users/Desktop/2.jpeg");
	if (!src.data)
	{
		cout << "no image" << endl;
		return 0;
	}
	int src_rows = src.rows;
	int src_cols = src.cols;
	Mat new_mat;
	new_mat.create(src_rows, src_cols, CV_16S);
	cout << "number of rows:" << new_mat.rows << endl;
	cout << "number of cols:" << new_mat.cols << endl;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

当然也有其它的方式可以不使用create:

Mat new_mat(src.size(), src.type());
  • 1

上述方式可以在定义一个新Mat对象的时候就定义好它的大小和类型。

2.2 Mat对象的深浅拷贝

除了上述这几种常见的情况之外,我们也可以拷贝一个现有的Mat对象作为新的
对象进行操作。但是复制的时候需要注意深浅拷贝的区别。深浅拷贝在这里不再进行赘述。Opencv中有几个常用的拷贝Mat对象的方法,虽然它们最后得到的结果都是一致的,但是有一些轻微的小细节可以注意,在提高计算效率和高效资源分配上也许会起到一些作用。

  • =:赋值号进行赋值属于典型的浅拷贝。更改任何一个浅拷贝得到的Mat将更改所有和它相关的Mat.
  • src.clone(): 该函数提供了一个深拷贝的方法,该方法会直接在内存中申请新的内存地址。
  • src.copyto(dst): 参数dst是拷贝结果。该函数也是深拷贝的方式,但是和clone不同的是:是否申请新的内存空间取决于 dst 和 src 的大小是否一致。如果大小一致就不会在申请,若不一致,将开辟一个新的空间。

2.3 Mat对象的指针操作(操作Mat中元素)

使用指针来操控和处理Mat中的元素,在图像处理算法中经常会用到。大量的图像算法都是从像素出发来处理图像。Opencv中 可以使用:pointer = src.ptr<uchar>(),假设想要读取src的第一行的,就可以使用 pointer = src.ptr<uchar>(0)。同理,如果让矩阵中的元素发生运算,也可以用类似的方法,比如现在想让第0行第0列的数据与第0行第一列的数据发生运算。我们可以:

//首先获得第0行第0列 和 第0行第1列 两个元素的指针,然后再进行计算

	const uchar* pointer_1 = src.ptr<uchar>(0, 0); // pointer_1
	const uchar* pointer_2 = src.ptr<uchar>(0, 1); // pointer_2


	printf("value that pointer_1 is pointing to : %d\n", *pointer_1);
	printf("value that pointer_2 is pointing to : %d\n", *pointer_2);

	uchar var_1 = *pointer_1 - *pointer_2;

	printf("result:%d\n", var_1);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

得到的结果如下:
在这里插入图片描述

其实有一点我不是很理解,为什么指针的类型一定要是uchar。因为我尝试了 int double, uint 等多个数据类型,得到的结果看上去都是随机数。

3.提高图像对比度

通过上面的对Mat对象的操作,以及读写照片的基本方法的简述之后,可以利用这些知识点来做一个图像对比度提升的效果。图像对比度提升的原理是利用一个固定的卷积核与原图进行卷积运算,使得图像边缘和轮廓部分更加的明显。
因为涉及到像素与像素之间的运算,所以会用到前面提到的获取每一个像素的指针。

#include<opencv2/opencv.hpp>

using namespace std;
using namespace cv;


int main()
{

	Mat src = imread("1.jpeg");
	if (!src.data)
	{
		printf("no image");
		return -1;
	}

	//循环图像中的每一个像素
	int nchannel = src.channels();
	int img_rows = src.rows;
	int img_cols = src.cols * src.channels();
	cout << "img_row" << img_rows << endl;
	cout << "img_cols" << img_cols << endl;
	Mat result_img = Mat::zeros(src.size(), src.type()); // 新建一个Mat来存放处理好的照片。

	//假设有一个 3 * 3 的卷积核,核中的权重已经固定
	for (int row = 1; row < img_rows - 1; row++) //遍历每一行
	{

		const uchar* previous = src.ptr<uchar>(row - 1); //找到当前行,前一行,后一行的指针位置。
		const uchar* current = src.ptr<uchar>(row); // 当前行的指针位置。
		const uchar* next = src.ptr<uchar>(row + 1); //下一行的指针位置。
		uchar* output_pointer = result_img.ptr<uchar>(row);


		for (int col = nchannel; col < img_cols; col++)
		{
#			//当前像素经过卷积之后的值。
			output_pointer[col] = saturate_cast<uchar>(5 * current[col] - (current[col - nchannel] + current[col + nchannel] + previous[col] + next[col]));

		}
	}

	namedWindow("original", CV_WINDOW_AUTOSIZE);
	imshow("original", src);

	namedWindow("test_frame_1", CV_WINDOW_AUTOSIZE);
	imshow("test_frame_1", result_img);
	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
  • 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

通过上面的代码可以得到一个如下图所示的效果
在这里插入图片描述
很明显可以发现,再提升了对比度之后,图像的细节会显得比之前的图像要明显很多。但是会发现上面的代码也许有些过于冗长了。Opencv提供了一个简单的方法来完成滤波操作: filter2D()。该方法的得到的效果与上面的代码几乎没有特别大的差距。

	Mat dst; 
	Mat kernel = (Mat_<int>(3, 3) << 0, -1, 0, -1, 5, -1,0,-1,0);
	filter2D(src, dst,-1, kernel);
  • 1
  • 2
  • 3

在这里插入图片描述

4. 图像的混合

图像的混合操作是将两张照片按照不同的强度融合成一张照片。在图像处理中的应用很广泛,比如将mask rcnn输出的mask画到原图像上,就可以使用add weighted的方法。这个方法的本质是将两个array每一个对应的元素按照一定的权重进行相加。

cv::addWeighted() Perform element-wise weighted addition of two arrays (alpha blending)

这个方法背后的理论支撑是:
在这里插入图片描述
其中src1是第一张输入图像,src2是第二张输入图像,α和β分别对应的是这两个图像相加的强度,γ是相加结果的偏置( Offset added to weighted sum)。下面会做几个实验并且记录改变参数对最终结果的影响。

其实根据公式也很容易发现α和β改变对结果的影响,无非是当α变大 src1 再结果中更加明显,Beta变大 src2变得更加明显。 但是改变gamma会带来什么样的效果。 为了方便观察某一个变量的改变引起的变化,可以使用一个trackbar来观察这个变化所带来的影响。具体代码可以如下图所示:

#include<opencv2/opencv.hpp>

using namespace std;
using namespace cv;


void weighted(int, void*);

Mat src = imread("1.jpeg");
Mat src_1 = imread("2.jpeg");
double alpha = 0.5;
double beta = 0.5;
int gamma_ = 2;
Mat dst;


int main()
{
	if (!(src.data && src_1.data ))
	{
		printf("no image");
		return -1;
	}
	namedWindow("original"); //显示原图1
	namedWindow("original_1"); //显示原图2
	namedWindow("weighted"); // 显示加权图

	imshow("original", src);
	imshow("original_1", src_1);
	resize(src_1, src_1, src.size(), 0.5, 0.5);
	cout << "size of src_1" << src_1.size() << endl;
	cout << "size of src" << src.size() << endl;
	//使用这个函数调取 weighted函数,实现拖动bar 进行调参的功能。
	createTrackbar("gamma", "weighted", &gamma_, 20, weighted); 

	waitKey(0);
	return 1;
	
}

void weighted(int, void*)
{
	addWeighted(src, alpha, src_1, beta, gamma_, dst);
	imshow("weighted", dst);
}
  • 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

需要注意的是使用addweighted 函数 两张图像的大小应该一致,不然会出现报错,所以在相加之前,应将尺寸不对等的两张照片做resize处理。 那么结果如下图所示:
在这里插入图片描述下图是偏置gamma的强度为3和20时候的差距,可以看出当gamma的值比较大的时候,图像的亮度会比gamma小的时候要大。也就是说每一个像素值都会加上gamma的值。
在这里插入图片描述

5. 绘制基本的几何图像

绘制基本图像的API相对而言会比较枯燥,但是这些基础的API在后续的图像处理过程中起到了非常大的作用,比如在物体检测中,我们通常需要将物体在原图中框出来,这个时候就会使用到这些图像的画图工具,这也是属于图像处理的基本操作。

5.1 点

点通常使用Point(a,b)来定义一个点。为什么会把它单独记录,是因为有特别多的绘图工具在使用的时候都需要提供多个点的位置才可以绘制想要的图形。所以它可以说是非常基础但是很重要的API。具体的使用方法如下:

	Point a = Point(1, 2);
	cout << a << endl;
  • 1
  • 2

上述代码的结果应该是:

通过上述的代码可以发现point可以理解成是OpenCV提供的一中数据类型,就像Mat一样。

5.2 线

画线的API和使用方法如下所示:

int main()
{

	Mat mat = Mat::zeros(300, 300, CV_8U);
	Point point_a = Point(100, 100);
	Point point_b = Point(200, 200);
	Scalar color = Scalar(255,255,255);
	line(mat,point_a,point_b,Scalar(255,255,255),LINE_8);
	namedWindow("frame_1");
	imshow("frame_1", mat);
	waitKey(0);
	return 1;

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

API:

line(mat,point_a,point_b,color,LINE_8);
mat: 是目标图像,说简单一点就是在那张图上画。
point_a / point_b:画一条线需要的两个点。
color : 线的颜色,是一个Scalar类型的变量。
LINE_8 是线类型的一种。在opencv中提供了三种线

 - LINE_8
 - LINE_4
上面两种的区别在于粗细,明显的LINE_8要比LINE_4粗
 - LINE_AA:抗锯齿

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

下图显示了三个线类型的效果,从上到下,分别是 LINE_4, LINE_8, LINE-AA。

在这里插入图片描述

5.3 矩形

绘画矩形的方法和画直线的方法很类似。画直线需要使用Point定义好两个点,画矩形则是需要先定义一个矩形。
定义矩形API:

Rect rect = Rect(200, 100, 150, 100);
前两个参数代表了矩形左上点的坐标,后两个点代表了右下两个点的坐标。
  • 1
  • 2

然后使用画矩形的API即可 和 画直线基本一致,不再过多赘述。

	Rect rect = Rect(200, 100, 150, 100);
	Scalar color = Scalar(255, 0, 0);
	rectangle(src, rect, color, LINE_8);
  • 1
  • 2
  • 3

5.4 椭圆

画椭圆的API相较于前两个要稍微复杂一些,这个API 不仅仅可以用于画椭圆,也可以根据给定的参数画出弧线或者圆形。椭圆的API大致如下:

void ellipse(InputOutputArray img, Point center, Size axes,
                        double angle, double startAngle, double endAngle,
                        const Scalar& color, int thickness = 1,
                        int lineType = LINE_8, int shift = 0);
参数的意义:
src: 需要绘制在哪一张图上。
center : 圆心。
Size axes: 长轴与短轴。
angle:倾斜角。
startAngle: 开始角度
endAngle: 结束角度。
color : 颜色。
thickness: 线粗。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

其中angle参数的单位是度(°)不是弧度。它代表着椭圆长轴的倾斜角度。具体一点,它代表了长轴逆时针偏离水平方向(X轴)的角度。

The angle is the angle (in degrees) of the major axis, which is measured
counterclockwise from horizontal (i.e., from the x-axis).

更改angle参数可以调整椭圆在图中的倾斜角度,调整startAngle 和 endAngle可以调整弧的大小,当startAngle=0, endAngle = 360时,就是一个椭圆,通过实际的代码演示可以更加清晰的看到不同startAngle 和 endAngle对结果的影响。

Similarly, the startAngle and endAngle indicate (also in degrees) the angle for the arc to start and for it to fin‐
ish. Thus, for a complete ellipse, you must set these values to 0 and 360, respectively.

#include<opencv2/opencv.hpp>

using namespace std;
using namespace cv;

 
void modification(int, void*);
int startAngle = 270;
int endAngle = 360;
Mat src = Mat::zeros(500, 500, CV_8U);
int main()
{

	namedWindow("frame_1");
	createTrackbar("startAngle","frame_1",&startAngle,endAngle,modification);
	modification(0,0);
	waitKey(0);
	return 1;
}

void modification(int, void*)
{	
	cout << startAngle << endl;
	ellipse(src, Point(250, 250), Size(src.rows / 4, src .cols / 4), 45, startAngle, endAngle, Scalar(255, 255, 255), 2, LINE_8);
	imshow("frame_1", src);
}

  • 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

通过trackbar调整startAngle来观察不同的startAngle和EndAngle之间有什么区别,下图是startAngle = 180,endAngle = 360时候的图像:
在这里插入图片描述可以观察到这时的图像只是一个弧线。通过调整startAngle到更小的值,椭圆(这里设置的参数会输出一个圆形)的形状越来越完整。

在这里插入图片描述
下图可以非常直观的解释这个API中每一个参数的含义是什么。
在这里插入图片描述
除了以上操作之外,我们可以使用一个矩形去拟合椭圆,就如下图所示:

在这里插入图片描述

6. 线性滤波与非线性滤波

滤波操作在图像处理中的应用非常广泛,滤波的作用是做一些轻度的图像模糊,让细节更加突出,找出边界,去除噪音等效果。通常滤波操作可以用在图像处理之前和处理之后,

filter images in order to add soft blur, sharpen details, accentuate edges, or remove noise.

下面分别简单记录一些比较基础的滤波方式。

6.1 线性滤波

最常见的一种就是线性滤波,它可以理解为是临近像素值的加权和,其中加权的权重是由另一个更小的矩阵决定的,通常叫做核(kernel)或者滤波器(filter) -->原理核卷积神经网络的前向传播原理非常类似。下图是线性滤波的表达式:
在这里插入图片描述
根据想要的效果,适当的更改核的值以及大小,会起到很不同的效果。如前文演示的提高图像对比度就是线性滤波的一种实际应用。线性滤波可以使用filter2D来实现,因为前文已经提到过了,所以这里就不再赘述。Kernel的取值通常是基数。在实际的开发过程中,因为使用线性滤波,每一个像素需要K * K次运算,K是kernel的size,所以由此可知,常规的线性滤波虽然可行,但是效率低。所以有时候会使用可分离的核来代替传统的卷积核。可分离的卷积核是指,先使用一个卷积方向的核对水平方向进行卷积,然后再使用一个垂直方向的卷积核对图象进行卷积,从而使得每一个像素的计算量从 K^2 降至 2* K。

6.2 非线性滤波

6.2.1 中值滤波

中值滤波就是选取临近的像素中的中值来代替当前像素的值。中值滤波的原理比较简单,所以这也成为了它的一大优势,中值滤波可以再线性时间内完成。中值滤波的API:

medianBlur(src,dst,ksize);
参数:
src: 原图。
dst : 存放结果的矩阵。
ksize : kernel的大小。
  • 1
  • 2
  • 3
  • 4
  • 5

使用 7 * 7 的中值滤波效果如下:
在这里插入图片描述
但是它的缺点也非常明显,就是使用一个值来代替每一个输出的像素,对于高斯噪声的效果没有特别理想。所以可以使用加权中值滤波来弥补这一问题。加权中值滤波的原理比较简单:根据像素离中心像素的距离来决定该像素的使用次数。

Another possibility is to compute a weighted median, in which each pixel is used a number of times depending on its distance from the center.

6.2.2 双边滤波

双边滤波和前面提到的滤波算法其实有着很多相似的地方,它的主要原理也是使用加权平均的方法:用像素a周围的像素的加权平均值来代表像素a的值。但是双边滤波与其他滤波算法最大的不同就是它的权重不只是考虑到了像素离中心像素的距离,也考虑到了像素本身存在的一些差异。正式一点,双边滤波的输出应如下:

在这里插入图片描述

其中权重w有两个核组成,一个是domain kernel, 一个是range kernel。 其中domain kernel 的表达式为

在这里插入图片描述

range kernel 的表达是为:
在这里插入图片描述
其中f(i,j) 和 f(k,l)代表的是像素(i,j) 和 像素(k,l)的intensity。双边滤波器的权重是上述两者的乘积:

在这里插入图片描述
所以双边滤波器的优势在于,当像素的变化值很小的时候(像素的intensity变化很小),那么range kernel的值就会比较小,这时主要是domin kernel在主导算法的输出,其效果和高斯模糊的效果类似。但是在像素强度变化很大的时候(通常是在边缘地区,像素强度的变化会很大),这时range kernel的权重开始变大,使得这些边缘地区的信息得以保留。

在OpenCV中也由双边滤波算法的API:

bilateralFilter(src, dst, d, sigmaColor, sigmaSpace);
d : 中心像素的领域范围。
sigmaColor: 颜色空间过滤器,该值越大产生的半相等颜色区域也就越大。
sigmaSpace:该值越大,影响的区域范围也就越大。
  • 1
  • 2
  • 3
  • 4

下图是当 d = 10 ,sigmaColor 和 sigmaSpace 分别等于50,50的时候的效果:
在这里插入图片描述
可明显的感觉到使用双边滤波算法之后的图像非常光滑细节没有原图这么突出但是边缘能得以保留。所以根据双边滤波算法的原理,它可以使用在一些简单的美颜算法上。

7.基本的图形学操作

7.1 膨胀

膨胀操作的API:

dilate(src, dst,kernel);
Mat kernel = getStructuringElement(MORPH_RECT, Size(11, 11));
//kernel 是一个structuingelement,直接使用Mat是会报错的。

  • 1
  • 2
  • 3
  • 4

在这里插入图片描述

7.2 腐蚀

腐蚀也是形态学的基本操作之一。
opencv提供的腐蚀的API:

erode(src, dst,kernel);
kernel: Mat kernel = getStructuringElement(MORPH_RECT, Size(11, 11));
  • 1
  • 2

像下图是大小为 (11,11)的长方形kernel与图像发生腐蚀的效果。

在这里插入图片描述

7.3 开操作与闭操作

开操作是先腐蚀后膨胀,可用于去除一些细小的颗粒(物体),在平滑边缘上也有一定作用。开操作的其实是先腐蚀后膨胀的结果。相较于开操作而言,闭操作也可以消除一些细小的噪声而且可以填充闭合区域。他则是先膨胀后腐蚀的结果。在opencv中两者使用的API是相同的是需要注明使用的形态学操作即可。Opencv提供的形态学操作的API是:morphologyEX


Mat kernel = getStructuringElement(MORPH_RECT, Size(11, 11));
morphologyEx(src, dst,MORPH_CLOSE,kernel );
//开操作则将MORPH_CLOSE替换为MORPH_OPEN即可。

  • 1
  • 2
  • 3
  • 4
  • 5

开操作的效果如下所示:
在这里插入图片描述
闭操作的结果如下图所示:
在这里插入图片描述

7.4 图像的基本梯度

在形态学中,图形的基本梯度定义为图形膨胀的结果减去腐蚀的结果。
根据下图结果可以发现,形态学梯度可以表示出像素值变化较大的区域,从而使用合理的核,可以勾勒出图像的边缘部分。
在这里插入图片描述

int main()
{
	Mat src = imread("path/to/img");
	if (src.empty())
	{
		cout << "no image" << endl;
		return 0;
	}
	Mat dst;
	Mat dst_dilate;
	Mat dst_erode;
	Mat RES;
	GaussianBlur(src, dst, Size(3, 3), 0.3);
	Mat kernel = getStructuringElement(MORPH_RECT, Size(3,3));
	dilate(dst, dst_dilate, kernel);
	erode(dst, dst_erode, kernel);
	RES = dst_dilate - dst_erode;

	namedWindow("original");
	imshow("original", src);

	namedWindow("frame_1");
	imshow("frame_1", RES);

	waitKey(0);

	return 1;
}
  • 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

7.5 顶帽和黑帽操作

顶帽是原图像和开操作之间的差值。而黑帽相反的,则是原图像和闭操作之间的差值。开操作的之后图像中的裂痕和局部低亮度的地方得到了放大。该操作时常用于分离物体和背景。 黑帽运算用来分离比邻近点暗一些的斑块。

顶帽与黑帽操作的结果分别如下两张图所示:
在这里插入图片描述
在这里插入图片描述
opencv中可以是直接使用

morphologyEx(dst, dst_open, MORPH_BLACKHAT,kernel) // 黑帽
//或
morphologyEx(dst, dst_open, MORPH_TOPHAT,kernel) // 顶帽
  • 1
  • 2
  • 3

也可以使用原图减去开操作或者闭操作之后的结果。

8. 图像金字塔

图形金字塔的意义在于在不同的图像空间中去寻找图像的特征,图像金字塔让图像的特征得到了较好的保留。
通常尺寸很小的物体,需要使用较大的分辨率取观察,相反的如果物体的尺寸比较大,则仅仅只需要低分辨率就足以。若在一项任务中,既需要观察小物体,也需要观察大物体,则需要使用多分辨率去观察,这时候就诞生了图像金字塔。具体操作可以理解成图像的上采样和下采样操作,下采样的之后的图像是原图宽高的1/2,上采样得到的是原来图像的两倍。
上采样和下采样的具体实现方式如下:


int main()
{
	Mat src = imread("2.jpeg");
	if (src.empty())
	{
		cout << "no image" << endl;
		return 0;
	}
	Mat RES;
	int cols, rows;
	cols = src.cols;
	rows = src.rows;
	
	//上采样
	pyrUp(src, RES, Size(cols * 2, rows * 2));  //这里只能是先cols,后rows,且只能乘以2倍,不然会出现报错。
	//下采样
	pyrDown(src, RES, Size(cols / 2, rows / 2));
	
	
	namedWindow("original");
	imshow("original", src);

	namedWindow("frame_1");
	imshow("frame_1", RES);
	
	waitKey(0);
	
	return 1;
}
  • 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

8.1 高斯金字塔

In a Gaussian pyramid, subsequent images are weighted down using a Gaussian average (Gaussian blur) and scaled down. Each pixel containing a local average corresponds to a neighborhood pixel on a lower level of the pyramid.

8.1.1 高斯不同以及它的作用

高斯不同(Diffierence of Gaussian - DOG):
定义: 把同一张图像,在不同参数下做高斯模糊之后的结果相减,得到输出的图像,称之为高斯不同,在灰度图像增强,角点检测中经常用到。

高斯不同的步骤可以这么理解。
1. 首先使用一张灰度图像用高斯模糊进行处理,得到处理之后的图像 p1。
2. 在将P1用高斯模糊的方式进行处理得到P2.
3. 在使用p2 - p1,得到两者之间的差值。

8.2 拉普拉斯金字塔

A Laplacian pyramid is very similar to a Gaussian pyramid but saves the difference image of the blurred versions between each levels. Only the smallest level is not a difference image to enable reconstruction of the high resolution image using the difference images on higher levels. This technique can be used in image compression.

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/646939
推荐阅读
相关标签
  

闽ICP备14008679号