OpenCV(C++) 学习笔记(二)

使用教材,《OpenCV3编程入门》—毛星云 电子工业出版社。(感谢毛佬留下的宝贵知识财富!

使用的IDE。Visual Studio 2022+OpenCV 4.6.0。环境什么的安装细节很多blog都有,不多赘述。

旧坑还在继续加油,但是难度比较大,一是因为教授的飞机语速,二是由于确实内容比较难懂。只能说,菜鸟会尽力写完的。

别质疑,先相信!
 

五、core组件进阶

5.1 访问图像中的像素

5.1.1 图像在内存中的存储方式

        如果想要访问图像中的像素,那么我们必须对图像在内存中的存储方式有所了解。

        对于灰度图像,矩阵的列中只会包含一个子列。而对于多通道图像,每一列会有多个子列,具体子列的数量与通道数量相等,例如,RGB颜色模型的矩阵就有三个子列。(请注意,OpenCV中默认为BGR)。

        多数情况下,因为内存足够大,因此,图像中的各行就能一行一行地连接起来,形成一个长行。连续存储有利于提升图像扫描速度,我们可以使用isContinuous()来判断矩阵是否是连续存储地。

5.1.2 颜色空间缩减

        我们知道,如果矩阵元素存储地是单通道像素,使用C或C++的Uchar类型,那么像素只有256个不同的值。但是如果是3通道,那么颜色数量就太多了。如果使用如此多的颜色进行处理,那么对于算法的性能会产生严重的影响。

        其实,只是使用这些颜色中代表性的很小一部分,就能够达到同样的效果。

        如此,就引出了颜色空间缩减(color space reduction)。它的做法是:将现有颜色空间值除以某个输入值,以获得较少的颜色数。也就是“做减法”。

        具体来说:如uchar类型的三通道,一共有256x256x256个不同的值。我们可以定义,0~9的像素值为0、10~19的像素值为10、20~29的像素值为20。这样,就缩减到26x26x26种情况。这个方法可以由一个简单的公式实现。(因为C++中int类型除法操作会自动截余。)例如 Iold=14;Inew=(Iold/10)*10=(14/10)*10=1*10=10;

        uchar是0~255的无符号字符,uchar除以int,结果依然是uchar类型。因此,结果的小数也会向下取整。利用这一点,刚才提到在uchar定义域中进行的颜色缩减运算就可以表达为:

Iold = 14;
Inew = ( Iold / 10 ) * 10 = ( 14 / 10 ) * 10 = 1 * 10 = 10;

        在处理像素过程中,每个像素都要进行上述计算,也要一定时间。但是我们可以发现,只有0~255种像素(256种情况),进一步可以把256种情况的结果直接存放在表中(table)。这样直接取出结果就行。

int divideWith = 10;
uchar table[256];
for (int i = 0;i < 256;i++)
{
	table[i] = divideWith * (i / divideWith);
}

        由此可知,对于较大图像,有效方法是预先计算所有可能的值,然后需要时,直接利用查找表直接赋值即可。查找表是一维或多维数组。只需读取,无需计算。

5.1.3 LUT函数:Look up table操作

        对于上文提到的Look up table操作,OpenCV官方强烈推荐我们使用原型为operationsOnArrays:LUT()<lut>的函数来进行。他用于批量进行图像元素查找、扫描与操作图像。使用方法如下:

//建立一个mat型用于查找表
	Mat lookUpTable(1, 256, CV_8U);
	uchar* p = lookUpTable.data;
	for (int i = 0;i < 256;++i) 
	{
		p[i] = table[i];

	}
	//调用函数(I是输入,J是输出)
	for (int i = 0;i < times;++i)
	{
		LUT(I, lookUpTable, J);
	}

5.1.4 计时函数

        另外有个问题是如何计时。可以利用两个简单的计时函数——getTickCount()和getTickFrequency().

getTickCount()——返回CPU自某个时间(如启动电脑)以来走过的时钟周期数

getTickFrequency()——返回CPU一秒钟所走的时钟周期数。这样我们能轻松地以秒(s)为单位对某运算计时。

        组合使用示例如下:

	double time0 = static_cast<double>(getTickCount());//记录起始时间
	//进行图像处理操作
	time0 = ((double)getTickCount() - time0) / getTickFrequency();
	cout << "此方法运行时间为" << time0 << "秒" << endl;//输出运行时间

5.1.5 访问图像中像素的三类方法(重点)

         OpenCV一共提供了3种访问每个像素的方法:

1、指针访问:C操作符[ ];

2、迭代器iterator;

3、动态地址计算。

        以上三种方法在访问速度上略有差异。debug模式下,这种差异非常明显;release模式下,差异就不太明显了。下面通过一个程序来说明这3种方法。(程序的目的是减少图像中颜色的数量)。主程序代码如下:

// draft.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。


//------------------【头文件、命名空间包含部分】--------------------

//            描述:包含程序所使用的头文件和命名空间
//------------------------------------------------------------------
#include <iostream>
#include<opencv2/opencv.hpp>
#include<opencv2/highgui/highgui.hpp>
#include<opencv2/imgproc/imgproc.hpp>
using namespace cv;
using namespace std;

//-----------------------【全局函数声明部分】--------------------------
//                       描述:全局函数声明
//---------------------------------------------------------------------
void colorReduce(Mat& inputImg, Mat& outputImg, int div);

//---------------------------【main()函数】--------------------------
int main()
{	
	//【1】创建原始图并显示
	Mat srcImg = imread("1.jpg");
	imshow("原始图像", srcImg);

	//【2】按原始图的参数规格来创建效果图
	Mat dstImg;
	dstImg.create(srcImg.rows, srcImg.cols, srcImg.type());//令效果图的大小类型与原图相同

	//【3】记录起始时间
	double time0 = static_cast<double>(getTickCount());

	//【4】调用颜色空间缩减函数
	colorReduce(srcImg, dstImg, 32);

	//【5】计算运行时间并输出
	time0 = ((double)getTickCount() - time0) / getTickFrequency();
	cout << "此方法运行时间为" << time0 << "秒" << endl;//输出运行时间

	//【6】显示效果图
	imshow("效果图", dstImg);
	waitKey(0);
	return  0;

}

        主程序中调用colorReduce函数完成减少颜色的工作。鹅肉我们根据访问的三种方法,写出了三个版本的colorReduce()函数。下面进行简单的讲解。

1、【方法一】用指针访问像素

        用指针访问的方法利用的是C语言中的 [ ] 操作符。这种方法最快,但是略有抽象。实验条件下单次运行时间为0.00665378。范例代码如下:

//--------------------------【colorReduce()】------------------------------
//       描述:使用【指针访问】方法的colorReduce()函数
//-------------------------------------------------------------------------
void colorReduce(Mat& inputImg, Mat& outputImg, int div)
{
	//参数准备
	outputImg = inputImg.clone();//复制实参到临时变量
	int rowNumber = outputImg.rows;//行数
	int colNumber = outputImg.cols * outputImg.channels();//列数x通道数=每一行元素数

	//双重循环,遍历所有的像素值
	for (int i = 0;i < rowNumber;i++)//行循环
	{
		//【开始处理每个像素】
		uchar* data = outputImg.ptr<uchar>(i);//获取第i行的首地址
		for (int j = 0;j < colNumber;j++)
		{
			data[j] = data[j] / div * div + div / 2;
		}
	}


}

        cols和rows给出图像的宽和高,成员函数channels()用于返回图像的通道数。灰度图的通道数为1,彩色为3。

        为了简化指针运算,Mat类提供了ptr函数可以得到图像任意行的首地址。而双层循环内部的那句data [ i ] 代码,可以等效地使用指针运算从一行移动到下一列。所以,也可以写成:

        * data++=*data/div*div+div/2;

        核心思想:通过ptr获取行指针,然后对行指针使用 [ ] 访问其元素

2、【方法二】用迭代器操作像素

        迭代法中,我们需要的仅仅是获得图像矩阵的begin和end,然后增加迭代直至从begin到end。将*操作符添加在迭代指针前,即可访问当前指向内容。(使用指针访问可能出现越界问题,迭代器绝对是非常安全的方法)

//--------------------------【colorReduce()】------------------------------
//       描述:使用【迭代器访问】方法的colorReduce()函数
//-------------------------------------------------------------------------
void colorReduce1(Mat& inputImg, Mat& outputImg, int div)
{
	//参数准备
	outputImg = inputImg.clone();//复制实参到临时变量
	//获取迭代器
	Mat_<Vec3b>::iterator it = outputImg.begin<Vec3b>();
	Mat_<Vec3b>::iterator itend = outputImg.end<Vec3b>();

	//获取彩色图像像素
	for (;it != itend;++it)
	{
		//【处理每个像素】
		(*it)[0] = (*it)[0] / div * div + div / 2;
		(*it)[1] = (*it)[1] / div * div + div / 2;
		(*it)[2] = (*it)[2] / div * div + div / 2;
	}

}

        实验条件下单次运行时间为0.242588.(不熟悉迭代器概念的读者,可以搜索“STL迭代器”进行简单学习)。

3、【方法三】动态地址计算

//--------------------------【colorReduce2()】------------------------------
//       描述:使用【动态地址计算】方法的colorReduce2()函数
//-------------------------------------------------------------------------
void colorReduce2(Mat& inputImg, Mat& outputImg, int div)
{
	//参数准备
	outputImg = inputImg.clone();//复制实参到临时变量
	int rowNumber = outputImg.rows;//行数
	int colNumber = outputImg.cols;//列数

	//存取彩色图像像素
	for (int i = 0;i < rowNumber;i++)//行循环
	{
		
		for (int j = 0;j < colNumber;j++)
		{
			//【开始处理每个像素】
			outputImg.at<Vec3b>(i, j)[0] = outputImg.at<Vec3b>(i, j)[0] / div * div + div / 2;//蓝色通道
			outputImg.at<Vec3b>(i, j)[1] = outputImg.at<Vec3b>(i, j)[1] / div * div + div / 2;//绿色通道
			outputImg.at<Vec3b>(i, j)[2] = outputImg.at<Vec3b>(i, j)[2] / div * div + div / 2;//红色通道
		}
	}
}

        单次运行时间为0.334131。这种方法简洁明了,符合大家对像素的直观认识。(值得注意的是,OpenCV的图像矩阵坐标为横width,纵height。使用at(i,j)时,请留意,i 是height对应的值,而 j 是width对应的值。这一点和matlab有所区别)

        成员函数at(int y ,int x),可以用来存取图像元素,但是必须在编译期知道图像的数据类型。需要注意的是,一定要确保指定的数据类型与矩阵中的数据类型相符合,因为at方法本身不会对任何数据类型进行转换。

Vec3b的解释        

        对彩色图像,每个像素由三部分构成,蓝色通道、绿色通道、红色通道(BGR)。因此,对于一个包含彩色图像的Mat,会返回一个由三个8位数组成的向量(3通道,uchar 8)。OpenCV将此类型的向量定义为Vec3b,即由3个uchar组成的向量。这也解释了为什么代码格式是:

//channel是颜色通道号
image.at<Vec3b>(j, i)[channel] = value;

5.2 ROI区域图像叠加&图像混合

        在本节中,我们将一起学习在OpenCV中如何定义感兴趣区域ROI,如何使用addWeighted函数进行图像混合操作,以及如何将 ROI和addWeighted 函数结合运用,对指定区域进行图像混合操作。

5.2.1 感兴趣区域:ROI

        在图像处理领域,我们常常需要设置感兴趣区域(ROI,region ofinterest)来专注或者简化工作过程。也就是从图像中选择的一个图像区域,这个区域是图像分析所关注的重点。我们圈定这个区域,以便进行进一步处理。而且,使用 ROl指定想读入的目标,可以减少处理时间,增加精度,给图像处理来带不小的便利。

        定义ROI区域有两种方法:第一种是使用表示矩形区域的 Rect。它指定了ROI矩形的左上角坐标(作为构造函数的前两个参数)和矩形的长宽(作为构造函数的后两个参数)以定义一个矩形区域。

        另一种方法是指定感兴趣区域的行或列的范围(Range)。Range是指从起始索引到终止索引(不包括终止)的一连段连续序列。cRange可以用来定义Range。

        下面是两种定义的示例代码:

Mat imgROI;
//方法一
imgROI = image(Rect(500, 250, cols, rows));
//方法二
imgROI = image(Range(250, 250 + cols), Range(200, 200 + rows));

        下面是一个具体的应用实例,场景是使用AOI将一幅图片添加到另一幅图像的指定位置之上。

int main()
{
	Mat img = imread("1.jpg",0);
	Mat bigimg = imread("2.jpg",0);
	//判断是否读取成功
	if (!img.data)
	{
		printf("读取小图片失败 \n");
	}
	if (!bigimg.data)
	{
		printf("读取大图片失败 \n");
	}
	Mat ROI = bigimg(Rect(0, 0, img.cols, img.rows));
	assert(ROI.cols == img.cols && ROI.rows == img.rows);
	namedWindow("[ 1 ]");
	imshow("[ 1 ]", ROI);
	waitKey(0);
	Mat mask = imread("1.jpg", 0);
	imshow("[ 1 ]", mask);
	waitKey(0);
    
    【1】法1
    //请注意copyTo的用法
    //将mask赋给ROI。与此同时,bigimg也改变了。
    mask.copyTo(ROI);
    【2】法2
    //注意copyTo的用法。
    //前方的img应当和mask一样大,效果是得到一个加了mask的ROI
    img.copyTo(ROI,mask);

	imshow("[ 1 ]", bigimg);
	waitKey(0);
	return 0;
}

 

                                                          上图为添加结果

        这里插入一下对于copyTo中mask的说明:copyTo前的参数是待复制图像,根据mask中不为0的位置相对应的待复制元素都将被复制,并且拷贝到outputArray中。(所以mask是一个复制器类似的,为0的位置不复制,其他都复制)

        这个函数就是,先读取了img和bigimg图像,前者是需要添加到别的图像上的,后者是被添加。然后在bigimg创造ROI图像来确定img添加的位置,创造和img一样大小的mask用于copyTo,最后复制。

5.2.2 线性混合操作

        线性混合的理论公式如下:

        我们根据改变alpha的值,来实现两张图片的过渡,也就是幻灯片中实现的缓慢的两张图片的过渡效果。

        实现中多使用addWeighted()函数。

5.2.3 计算数组加权和函数:addWeighted()

void addWeighted(InputArray src, double alpha, InputArray src2, double beta, double gamma, OutputArray dst, int dtype = -1);

        两个src数组需要有相同的通道数和大小,此外,alpha对应第一个src的权重,beta对应第二个,gamma是对于二者之和的一个增大或减小,而dtype表示输出阵列的可选深度,当两个数组深度相同,默认为-1.

        当遇到多通道数组时,每个通道要进行单独处理。需要注意的是,如果数组的深度是CV_32S时,此函数不再适用——内存溢出或者结果错误。

        下面是一个例子:

// draft.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。


//------------------【头文件、命名空间包含部分】--------------------

//            描述:包含程序所使用的头文件和命名空间
//------------------------------------------------------------------
#include <iostream>
#include<opencv2/opencv.hpp>
#include<opencv2/highgui/highgui.hpp>
#include<opencv2/imgproc/imgproc.hpp>
using namespace cv;
using namespace std;

int main()
{
	double alpha = 0.5;
	double beta;
	Mat src1, src2, dst;
	Mat img, img2;
	img = imread("1.jpg");
	src1 = img;
	img2 = imread("2.jpg");
	src2 = img2(Rect(0, 0, img.cols, img.rows));
	beta = 1 - alpha;
	addWeighted(src1, alpha, src2, beta, 0.0, dst);

	namedWindow("[ 1 ]", 1);
	imshow("[ 1 ]", src1);
	waitKey(0);
	namedWindow("[ 2 ]", 1);
	imshow("[ 2 ]", src2);
	waitKey(0);
	namedWindow("[ 3 ]", 1);
	imshow("[ 3 ]", dst);
	waitKey(0);
	return 0;

}

        

                                             上图为效果图

5.2.4 初级图像混合

        下面我们将尝试将ROI和addWeighted混合使用。先指定ROI区域,然后使用addWeighted()进行叠加混合。代码如下:

// draft.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。


//------------------【头文件、命名空间包含部分】--------------------

//            描述:包含程序所使用的头文件和命名空间
//------------------------------------------------------------------
#include <iostream>
#include<opencv2/opencv.hpp>
#include<opencv2/highgui/highgui.hpp>
#include<opencv2/imgproc/imgproc.hpp>
using namespace cv;
using namespace std;

int main()
{
	Mat src1 = imread("1.jpg");
	Mat src2 = imread("2.jpg");
	Mat ROI = src2(Rect(200, 250, src1.cols, src1.rows));
	addWeighted(ROI, 0.5, src1, 0.3, 0.0, ROI);
	namedWindow("[ 1 ]", 1);
	imshow("[ 1 ]", ROI);
	waitKey(0);
	return 0;


}

                                                               上图为效果图

5.3 分离颜色通道、多通道图像混合

        很多时候,为方便观察,需要将RGB三通道分别显示出来,Opencv的split和merge可以很好的做到这一点(这两个函数互为“冤家”)。

5.3.1 通道分离函数:split()

        此处split的原型有两个版本。

	void split(const Mat & src, Mat * mvbegin);
	void split(InputArray m, OutputArrayOfArrays mv);

        第一个参数,const Mat &类型的src或者InputArray的m,是需要分离的多通道数组。

        第二个参数,mvbegin或者mv,填输出数组或者输出的容器vector

split分离数组的公式如下图:

// draft.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。


//------------------【头文件、命名空间包含部分】--------------------

//            描述:包含程序所使用的头文件和命名空间
//------------------------------------------------------------------
#include <iostream>
#include<opencv2/opencv.hpp>
#include<opencv2/highgui/highgui.hpp>
#include<opencv2/imgproc/imgproc.hpp>
using namespace cv;
using namespace std;

int main()
{
	Mat src, src2;
	src2 = imread("2.jpg");
	Mat ROI;
	vector<Mat> channels;
	src = imread("1.jpg",0);
	split(src2, channels);
	ROI = channels.at(0);
	addWeighted(ROI(Rect(0, 0, src.cols, src.rows)), 1.0, src, 0.5, 0.0, ROI(Rect(0, 0, src.cols, src.rows)));
	namedWindow("[ 1 ]", 1);
	imshow("[ 1 ]", ROI);
	waitKey(0);
	vector<Mat> mgr(3);
	Mat bk(ROI.size(), CV_8UC1, Scalar(0));
	mgr[0] = ROI;
	mgr[1] = bk;
	mgr[2] = bk;
	Mat bk1(ROI.size(), CV_8UC3);
	merge(mgr,bk1);
	imshow("[ 结果 ]", bk1);
	waitKey(0);
	return 0;


}

        代码中构建了一个三通道Mat类型的数组mgr,用以储存channels不同通道对应的值,而bk是用于填充通道的单通道数组,bk1是三通道的Mat数组。使用merge将二者混合,显示bk1即为蓝色的图像(第一个通道为蓝色分量)

                                                          上图为处理结果

5.3.2 通道合并:merge()函数

        merge()函数作用是将多个数组合成为一个多通道的数组,与split()互为逆操作。

        函数原型:

void merge(InputArrayOfArrays mv, OutputArray dst);

        参数mv,填入需要被合并的输入矩阵或者vector容器的阵列,此参数中的矩阵要求具有相同行列数。

        dst是输出矩阵,通道数是矩阵阵列中矩阵通道数总和。

        merge 函数的功能是将一些数组合并成一个多通道的数组,输出矩阵中的每个元素都将是输出数组的串接。其中,第i个输入数组的元素被视为 mv[i]。C一般用其中的 Mat::at()方法对某个通道进行存取,也就是这样用:channels.at(0).

        值得注意的是这里使用Mat::at()方法返回一个引用到指定数组的元素,因此修改其中一个,另一个也会发生变化。

5.4 图像对比度、亮度值调整

5.4.1 理论依据

        算子是一个函数,接受一个或者多个输入图像,并且产生输出图像。一般形式为:

        本节主讲的操作,属于图像处理变换中简单的——点操作(pointoperators)。点操作的特点是:仅根据输入像素值(有时加上某些全局信息或参数),来计算对应的输出。包括亮度、对比度调整、颜色校正以及变换。

        两种最常用的点算子(点操作)如下:

        其中a(通常a>0)被称为增益(gain),常用于控制图像的对比度(成比例变化)

        b是bias(偏置),控制图像的亮度(因为是整体加减灰度)

        btw,在使用算子时,需要确保计算后结果处于像素取值范围,所以经常用saturate_cast进行结果转换。此外,a对比度取值一般为0.0~3.0的浮点数,但由于轨迹条取值一般为整值,所以我们可以将轨迹条中代表对比度的nContrastValue参数设为0~300的整型,然后乘以0.01,这样就完成了轨迹条中300个不同变化。

5.5 离散傅里叶变换

5.5.1 dft()函数

        dft()函数作用是对一维或者二维浮点数数组进行正向或者反向离散傅里叶变换,原型为:

        src代表输入矩阵,可以实数或者虚数;dst用于存储计算结果,尺寸和类型取决于标识符flags;flags标识符,默认为0,取值可以参考下表

        以下是dft()函数标识符取值列表:

        第四个参数,int类型的nonzeroRows,有默认值0。当此参数设为非零时(最好是取值为想要处理的那一行的值,比如C.rows),函数会假设只有输入矩阵的第一个非零行包含非零元素(没有设置DFTINVERSE标识符),或只有输出矩阵的第一个非零行包含非零元素(设置了DFTINVERSE 标识符)。这样的话,函数就可对其他行进行更高效的处理,以节省时间开销。这项技术尤其是在采用 DFT计算矩阵卷积时非常有效。(简单来说就是,通过提供非零行的数量,来使需要计算的行数量减少;这告诉 OpenCV 算法只处理实际上是非零的行,而忽略那些零行,从而避免了不必要的计算。)

5.5.2 返回DFT最优尺寸大小:getOptimalDFTSize()

        getOptimalDFTSize()能够返回给定向量的DFT最优尺寸大小。我们通过扩充(Pedding)图像来提高DFT速度,而扩充多少,就由这个函数计算得到。(dft()也会自动填充,但是会降低计算速度)

        参数为图像的宽或者高(一般宽或者高是2、3、5倍数时计算速度会快)

5.5.3 扩充图像边界copyMakeBorder()

        上面提到了getOptimalDFTSize()用于计算需要扩充多少,那么我们还需要一个扩充图像边界的函数,也就是copyMakeBorder()。

        

void copyMakeBorder(InputArray src, OutputArray dst, int top, int bottom, int left, int right, int borderType, const Scalar& value = Scalar());

        第一个参数,InputArray类型的src,输入图像,即源图像(Mat类)
        第二个参数,OutputArray类型的dst,这个参数用于存放函数调用后的输出结果,需和源图片有一样的尺寸和类型,且size应该为Size(src.cols+eft+right,src.rows+top+bottom)。

        接下来的4个参数分别为int类型的top、bottom、lef、right,分别表示在源图像的四个方向上扩充多少像素,例如top=2,bottom=2,left=2,right=2就意味着在源图像的上下左右各扩充两个像素宽度的边界。
        第七个参数,borderType类型的,边界类型,常见取值为BORDER CONSTANT(其余见下表)

        第八个参数,const Scalar&类型的value,有默认值ScalarO,可以理解为默认值为0。当borderType取值为BORDERCONSTANT时,这个参数表示边界值。

5.5.4 计算二维矢量的幅值:magnitude()

        magnitude()用于计算二维矢量的幅值。

        

5.5.6 计算自然对数:log()

        log()能够计算每个数组元素绝对值的自然对数值。

5.5.7 矩阵归一化:normalize()

        normalize函数原型如下:

        第一个参数,InputArray类型的src。输入图像,即源图像(Mat类)。
        第二个参数,OutputArray类型的dst。函数调用后的运算结果存在这里,和源图片有一样的尺寸和类型。
        第三个参数,double 类型的 alpha。归一化后的最大值,有默认值1。

        第四个参数,double 类型的 beta。归一化后的最小值,有默认值0。

        第五个参数,int类型的 norm type。归一化类型,有NORM INF、NORM L1、NORM L2和NORMMINMAX等参数可选,有默认值NORM_L2

        第六个参数,int类型的 dtype,有默认值-1。当此参数取负值时,输出矩阵和src有同样的类型,否则,它和src有同样的通道数,且此时图像深度为
CV MAT DEPTH(dtype)。
        第七个参数,ImnputArray 类型的mask,可选的操作掩膜,有默认值 noArray()。

5.5.8 离散傅里叶变换的实现

        实现代码步骤:

        1、构造存储图像的数组A并初始化

        2、计算扩充尺寸并且扩充A

        3、进行DFT计算,存入构造好的数组A。使用split()获取DFT后对应位置的实部和虚部的值,使用magnitude计算幅值。

        4、频谱中心化,使低频分量位于图像中心,高频位于边缘

        值得注意的是,我在学习的时候,就觉得和Matlab的实现过程有出入。经查证,Matlab中对于DFT以及fft的结果,是以幅度+相位的方式存储的,所以可以直接输出显示频谱图。

5.6 输入输出XML和YAML文件

5.6.1 XML、YAML文件介绍

        所谓 XML,即eXtensible Markup Language,翻译成中文为“可扩展标识语言”。首先,XML是一种元标记语言。所谓“元标记”,就是开发者可以根据自身需要定义自己的标记,比如可以定义标记<book>、<name>。任何满足XML命名规则的名称都可以标记,这就向不同的应用程序打开了的大门。此外,XML是一种语义/结构化语言,它描述了文档的结构和语义。
        YAML是“YAML Ain'taMarkup Language”(译为“YAML 不是一种置标语言”)的递回缩写。在开发的这种语言时,YAML的原意是:“YetAnother MarkupLanguage”(仍是一种置标语言),但为了强调这种语言以数据为中心,而不是以置标语言为重点,而用返璞词进行重新命名。YAML是一个可读性高,用来表达资料序列的格式。它参考了其他多种语言,包括:XML、C语言、Python、Perl,以及电子邮件格式 RFC2822。

.yml、.yaml都是YAML文件的后缀

5.6.2 FileStorage类操作文件的使用引导

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值