文章目录
- 1. 访问图像中的像素
-
- 1.1 图像在内存中的存储方式
- 1.2 颜色空间缩减
- 1.3 LUT 函数: Look up table 操作
- 1.4 计时函数
- 1.5 访问图像中像素的三类方法
-
- 1.5.1 用指针访问像素
- 1.5.2 用迭代器操作像素
- 1.5.3 动态地址计算
- 2. RIO 区域图像叠加 & 图像混合
-
- 2.1 感兴趣区域:RIO
- 2.2 线性混合操作
- 2.3 计算数组加权和:addWeighted() 函数
- 3. 分离颜色通道,多通道图像混合
-
- 3.1 通道分离:spilt() 函数
- 3.2 通道合并
- 3.3 示例程序
- 4. 图像的对比度、亮度值调整
-
- 4.1 理论依据
- 4.2 访问图片中的像素
- 4.3 实例程序:图像对比度、亮度值的调整
- 5.离散傅里叶变换
-
- 5.1 离散傅里叶变换的原理
- 5.2 dft() 函数详解
- 5.3 返回 DFT 最优尺寸大小:getOptimalDFTSize() 函数
- 5.4 扩充图像边界:copyMakeBorder() 函数
- 5.5 计算二维矢量的幅值:magnitude() 函数
- 计算自然对数:log() 函数
- 5.7 矩阵归一化:normalize()
- 5.8 示例程序:离散傅里叶变换
- 6.输入输出 XML 和 YAML 文件
-
- 6.1 XML 和 YAML 文件简介
- 6.2 FileStorage 类操作文件的使用引导
- 6.3 示例程序:XML 和 YAML 文件的写入
- 6.4 示例程序: XML 和 YAML 文件的读取
1. 访问图像中的像素
1.1 图像在内存中的存储方式
??图像矩阵大小取决于所用的颜色模型,确切的说,取决于所用的通道数。
??若是灰度图像,其矩阵会如下图,即只有一个颜色通道:
??而对于多通道图像来说,矩阵中的列会包含多个子列,其子列个数与通道数相等。
??如图所示 RGB 颜色模型的矩阵:
?? OpenCV 中子列的通道顺序是反过来的—BGR 而不是 RGB 。很多情况下,因为内存足够大,可以实现连续存储,图像中的各行就能一行一行的连接起来,形成一个长行。连续存储有助于提升图像扫描速度,可以使用 isContinuous() 来判断矩阵是否是连续存储的。
1.2 颜色空间缩减
??对于多通道的图像,其颜色数非常多(如三通道有一千六百多万种),用如此之多的颜色来进行处理,会对算法的性能造成严重影响。
??其实,仅用这些颜色中具有代表性的很小的部分,就足以达到同样的效果,这时,颜色空间缩减便派上了用场。
??颜色空间缩减的做法是:将现有颜色空间值除以某个输入值,以获得较少的颜色数。也就是“做减法”,比如颜色值 0 ~ 9 可以取值 0,10 ~ 19 可以取值 10 ,以此类推。
??如 uchar 类型的三通道图像,每个通道取值可以是 0 ~ 255 ,于是就有 256 x 256 x 256 种不同的值。我们可以定义:
??0 ~ 9 范围的像素值为 0。
??10 ~ 19 范围的像素值为 10。
??20 ~ 29 范围的像素值为 20。
??这样的操作将颜色的取值降低为 26 x 26 x 26 种情况。
??uchar (无符号字符,即 0 到 255 之间取值的数)类型的值除以 int 值,结果仍是 char 。所以求出来的小数也要向下取整。利用这一点,刚才提到的 uchar 定义域中进行的颜色缩减运算就可以表达为下面的形式:
????????????????Inew=(Iold10)?10Inew = \left ( \frac{Iold}{10} \right ) * 10Inew=(10Iold?)?10
??为了节省时间花销,进一步把 256 种计算好的值提前存在表 table 中,这样每种情况不需计算,直接从 table 中取结果即可。
int divideWidth = 10;uchar table[256];for(int i = 0; i < 256; ++i){
table[i] = divideWidth * (i / divideWidth);}
??于是 table[i] 存放的值为 i 的像素减小颜色空间的结果,这样也就可以理解上述方法中的操作:
p[j] = table[p[j]]; //p[j] 代表颜色矩阵中的每一个像素
??这样,简单的颜色空间缩减算法就可以由下面两步完成:
??1)遍历图像矩阵的每一个像素;
??2)对像素用上述公式。
??上述做法用到了乘和除的操作,而这两种运算又很耗时,因此,应尽量用加和减操作替换它们。
1.3 LUT 函数: Look up table 操作
??对于上面提到的 table 操作,可用一个原型为 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); }
1.4 计时函数
??可利用简便的计时函数----getTickCount() 和 getTickFrequency() 进行计时。
??● getTickCount() 函数返回 CPU 自某个事件(如启动电脑)以来走过的时钟周期数。
??● getTickFrequency() 函数返回 CPU 一秒钟所走的时钟周期数。这样,就能以秒为单位,对某个运算计时。
??示例如下:
double time0 = static_cast<double>(getTickCount()); //记录起始时间// 进行图像操作······time0 = ((double)getTickCount() - time0) / getTickFrequency();cout << "此方法运行时间为:" << time0 << "秒" << endl; //输出运行时间
1.5 访问图像中像素的三类方法
??在 OpenCV 中,提供了三种访问每个元素的方法。
??● 方法一 指针访问: C 操作符[ ] ;
??● 方法二 迭代器 iterator;
??● 方法三 动态地址计算。
??使用一个示例说明这几种方法:程序的目的是减少图中颜色的数量,比如原来的图像是 256 种颜色,我们希望将它变为 64 种颜色,那只需将原来的颜色除以 4 (整除)以后再乘以 4 就可以了(因为C 中会自动向下取整)。
#include<opencv2/opencv.hpp>#include<iostream>using namespace std;using namespace cv;/*-----------------------------全局函数声明-----------------------------*/void colorReduce(Mat& inputImage, Mat& outputImage, int div);int main(int argc, char** argv){
//1.创建原始图并显示Mat srcImage = imread("1.jpg");imshow("原始图", srcImage);//2.按照原始图的参数规格来创建效果图Mat dstImage;dstImage.create(srcImage.rows, srcImage.cols, srcImage.type()); //效果图的大小,类型与原始图相同//3.记录起始时间double time0 = static_cast<double>(getTickCount());//4.调用颜色空间缩减函数colorReduce(srcImage, dstImage, 32);//5.计算运行时间并输出time0 = ((double)getTickCount() - time0) / getTickFrequency();cout << "此方法的运行时间为: " << time0 << "秒" << endl;//6.显示效果图imshow("效果图", dstImage);waitKey(0);//system("pause");return 0;}
??根据访问每个像素的三类方法,有以下三个版本的 colorReduce 函数。
1.5.1 用指针访问像素
??用指针访问像素利用的是 C 语言中的操作符 [ ] ,这种方法最快。
void colorReduce(Mat& inputImage, Mat& outputImage, int div){
//参数准备outputImage = inputImage.clone(); //复制实参到临时变量int rowNumber = outputImage.rows; //行数//列数 x 通道数 = 每一行元素的个数int colNumber = outputImage.cols * outputImage.channels(); //双重循环,遍历所有的像素值for (int i = 0; i < rowNumber; ++i) //行循环{
uchar* data = outputImage.ptr<uchar>(i); //获取第 i 行的首地址for (int j = 0; j < colNumber; ++j) //列循环{
//处理每个像素data[j] = data[j] / div * div + div / 2;}}}
??其中,成员函数 channels() 用于返回图像的通道数,灰度图的通道数为 1,彩色图的通道数为 3。
??为了简化指针运算,Mat 类提供了 ptr 函数可以得到图像任意行的首地址。ptr 是一个模板函数,它返回第 i 行的首地址:
uchar* data = outputImage.ptr<uchar>(i); //获取第 i 行的首地址
??双层循环中的那句处理像素的代码,可以等效的用指针运算从一列移动到下一列:
*data++ = *data / div * div + div / 2;
1.5.2 用迭代器操作像素
??这种方法类似于 STL 库的用法。
??相比用指针运算可能出现越界的问题,迭代器绝对是非常安全的方法。
void colorReduce(Mat& inputImage, Mat& outputImage, int div){
//参数准备outputImage = inputImage.clone(); //复制实参到临时变量//获取迭代器Mat_<Vec3b>::iterator it = outputImage.begin<Vec3b>(); //起始位置的迭代器Mat_<Vec3b>::iterator itend = outputImage.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;}}
1.5.3 动态地址计算
??下面是使用动态地址运算符配合 at 方法的 colorReduce 函数的代码:
void colorReduce(Mat& inputImage, Mat& outputImage, int div){
//参数准备outputImage = inputImage.clone(); //复制实参到临时变量int rowNumber = outputImage.rows;int colNumber = outputImage.cols; //列数,而不是每行像素个数//存取彩色图像像素for (int i = 0; i < rowNumber; i++){
for (int j = 0; j < colNumber; j++){
//处理每个像素outputImage.at<Vec3b>(i, j)[0] = outputImage.at<Vec3b>(i, j)[0] / div * div + div / 2; //蓝色通道outputImage.at<Vec3b>(i, j)[1] = outputImage.at<Vec3b>(i, j)[1] / div * div + div / 2; //绿色通道outputImage.at<Vec3b>(i, j)[2] = outputImage.at<Vec3b>(i, j)[2] / div * div + div / 2; //红色通道}}}
??成员函数 at(int y,int x) 可以用来存取图像元素,值得注意的是,一定要确保指定的数据类型和矩阵中的数据类型相符合,因为 at 方法本身不会对任何数据类型进行转换。
??对于彩色图像,每个像素由三个部分构成:蓝色通道、绿色通道和红色通道(BGR)。因此,对于一个包含彩色图像的 Mat ,会返回一个由三个 8 位数组成的向量。OpenCV 将此类型的向量定义为 Vec3b,即由三个 unsigned char 组成的向量。因此存取彩色图像像素的代码可以写成如下格式:
image.at<Vec3b>(i, j)[channel] = value;
??其中,索引值 channel 标明了颜色通道号。
??再次说明,OpenCV 中彩色图像不是以 RGB 的顺序存放的,而是 BGR,所以程序中的 outputImage.at< Vec3b >(i, j)[0] 代表的是该点的 B 分量。同理还有 (*it)[0]。
2. RIO 区域图像叠加 & 图像混合
2.1 感兴趣区域:RIO
??图像处理过程中,经常要设置感兴趣区域(RIO,region of interest),来专注或者简化工作的过程。
??定义 RIO 区域有两种方法:
??● 第一种是使用表示矩形区域的 Rect。它指定矩形的左上角坐标(构造函数的前两个参数)和矩形的长宽(构造函数的后两个参数)以定义一个矩形区域。
Mat imageRIO;//方法一,image 为已经载入好的图片imageRIO = image(Rect(500, 250, logo.cols. logo.rows));
??● 另一种 RIO 的定义方式是指感兴趣的行或列的范围(Range)。Range 是指从起始索引到终止索引(不包括终止索引)的一段连续序列。cRange 可以用来定义 Range。
//方法二,即从像素的250行开始到250 + .rows - 1行,200 列到 200 + .cols - 1 列imageRIO = image(Range(250, 250 + logoImage.rows), Range(200, 200 + logoImage.cols));
??下面是一个实例,演示如何利用 RIO 将一幅图加到另一幅图的指定位置。通过一个图像掩膜(mask),直接将插入处的像素设置为 logo 图像的像素值,这样效果会很逼真。
#include<opencv2/opencv.hpp>#include<iostream>using namespace std;using namespace cv;int main(int argc, char** argv){
//1.读入图像Mat srcImage = imread("dota_pa.jpg");Mat logoImage = imread("dota_logo.jpg");if (!srcImage.data || !logoImage.data){
printf("读取错误!\n");}//2.定义一个 Mat 类型并给其设定 RIO 区域Mat imageRIO = srcImage(Rect(250, 200, logoImage.cols, logoImage.rows));//3.加载掩膜(必须是灰度图)Mat mask = imread("dota_logo.jpg", 0);//4.将掩膜复制到 RIOlogoImage.copyTo(imageRIO, mask);//5.显示结果namedWindow("RIO实现图像叠加实例窗口");imshow("RIO实现图像叠加实例窗口", srcImage);waitKey(0);//system("pause");return 0;}
??测试了下 Rect 的左上角坐标设置,发现横坐标数值越大,logo 图像越往右移,而纵坐标越大,logo 图像越往下移。
2.2 线性混合操作
??线性混合操作是一种典型的二元(两个输入)的像素操作,它的理论公式如下:
????????????
??通过在范围 0 到 1 之间改变 alpha 值,来对两幅图像 (f0(x))和 (f1(x)) 或两端视频(f0(x))和 (f1(x)) 产生时间上的画面叠化效果,就像幻灯片放映和电影制作中的那样,也就是在幻灯片翻页时设置的前后页缓慢过渡叠加的效果,以及电影情节过渡时经常出现的画面叠加效果。
??实现方面,主要运用了 OpenCV 中的 addWeighted 函数。
2.3 计算数组加权和:addWeighted() 函数
??这个函数的作用是计算两个数组(图像列阵)的加权和。原型如下:
void addWeighted(InputArray src1, double alpha, InputArray src2, double bate, double gamma, OutputArray dst, int dtype = -1);
??● 第一个参数,InputArray 类型的 src1,表示需要加权的第一个数组,常常填一个 Mat。
??● 第二个参数,double 类型的 alpha,表第一个数组的权重。
??● 第三个参数,InputArray 类型的 src2,表第二个数组,它需要和第一个数组拥有相同的尺寸和通道数。
??● 第四个参数,double 类型的 bate,表示第二个数组的权重。
??● 第五个参数,double 类型的 gamma,一个加到权重总和上的标量值。
??● 第六个参数, OutputArray 类型的 dst,输出的数组,它和输入的两个数组拥有相同的尺寸和通道数。
??● 第七个参数,int 型的 dtype,输出阵列的可选深度,有默认值 -1 。当两个输入数组具有相同的深度时,这个参数的设置为 -1(默认值),即等同于 src1.depth()。
??下面的数学公式表示:用 addWeighted 函数计算以下两个数组(src1 和 src2)的加权和,得到结果输出给第四个参数,也就是 addWeighted 函数的作用和矩阵的表达式。
????????dst = src1[ I ] * alpha + src2[ I ] * beta + gamma;
??其中 I 是多维数组的索引值。而且,在遇到多通道数组时,每个通道都需要独立地进行处理。另外需要注意的是,当输出数组的深度为 CV_32S 时,这个函数就不适用了。
??接下来是一个实例,演示了如何使用 addWeighted 函数实现图像线性混合:
#include<opencv2/opencv.hpp>#include<iostream>using namespace std;using namespace cv;int main(int argc, char** argv){
//1.定义一些局部变量double alphaValue = 0.5;double betaValue;Mat srcImage1, srcImage2, dstImage;//2.读取图像(两幅图像需为同样的类型和尺寸)srcImage1 = imread("mogu.jpg");srcImage2 = imread("rain.jpg");if (!srcImage1.data || !srcImage2.data){
printf("图像读取错误!");return 0;}//3.图像混合加权操作betaValue = (1.0 - alphaValue);addWeighted(srcImage1, alphaValue, srcImage2, betaValue, 0.0, dstImage);//4.创建窗口并显示原图窗口namedWindow("原图1", 1);imshow("原图1", srcImage2);namedWindow("效果图", 1);imshow("效果图", dstImage);waitKey(0);//system("pause");return 0;}
??效果图:
??对于各自的权值 alpha 和 beta ,试了几个不同的值,感觉就像之前代码中的透明度,即权值越大,透明度越低,也就越明显。且权值是从 0 到 1 的。
3. 分离颜色通道,多通道图像混合
3.1 通道分离:spilt() 函数
??spilt() 函数用于将一个多通道数组分离成几个单通道数组。这里的 array 按语境翻译为数组或阵列。
??这个 spilt() 函数的 C++ 版本有两个原型,分别是:
??● void spilt(const Mat& src, Mat* mvbegin);
??● void spilt(InputArray m, OutputOfArrays mv);
??变量介绍如下:
??● 第一个参数,InputArray 类型的 m 或者 const Mat& 类型的 src,填我们需要进行分离的多通道数组。
??● 第二个参数,OutputOfArrays 类型的 mv 或是 Mat* 类型的 mvbegin,填函数输出数组或者输出的 vector 容器。
??spilt() 函数分割多通道数组转换成独立的单通道数组,公式如下:
??????????????
??使用示例如下:
Mat logoImage = imread("dota_logo.jpg");Mat srcImage1 = imread("dota_pa.jpg",1);Mat srcImage;Mat imageRIO;vector<Mat> channels;srcImage = cv::imread("dota.jpg");//把一个三通道图像转换成三个单通道图像split(srcImage, channels); //分离色彩通道imageRIO = channels.at(0);addWeighted(imageRIO(Rect(385, 250, logoImage.cols, logoImage.rows)), 1.0, logoImage, 0.5, 0., imageRIO(Rect(385, 250, logoImage.cols, logoImage.rows)));merge(channels, srcImage1);imshow("sample", srcImage);
3.2 通道合并
??merge() 函数是 spilt() 函数的逆向操作------将多个数组合并成一个多通道的数组。它有两个基于 C++ 的函数原型如下:
??● C++:void merge(const Mat& mv, size_tcount, OutputArray dst)
??● C++:void merge(InputArrayOfArrays mv, OutputArray dst)
??变量介绍如下:
??● 第一个参数:mv,填需要被合并的输入矩阵或 vector 容器的阵列,这个 mv 参数中所有的矩阵必须有着一样的尺寸和深度。
??● 第二个参数:count,当 mv 为一个空白的 C 数组时,代表输入矩阵的个数,这个参数显然必须大于 1。
??● 第三个参数:dst,即输出矩阵,和 mv[0] 拥有一样的尺寸和深度,并且通道的数量是矩阵阵列中的通道的总数。
??对于 merge() 函数,第 i 个输入数组的元素被视为 mv[i] 。C 一般用其中的 Mat::at() 方法对某个通道进行存取,也就是这样用: channels.at(0)。
??这里 Mat::at() 方法返回一个引用到指定的数组元素。注意是引用,相当于两者等价,也就是修改其中一个,另一个会发生改变。
??一个示例如下:
vector<Mat> channels;Mat imageBlueChannel;Mat imgaeGreenChannel;Mat imageRedChannel;srcImage4 = imread("dota.jpg");//把一个 3 通道图像转换成 3 个单通道图像spilt(srcImage4, channels); //分离色彩通道imageBlueChannel = channels.at(0);imageGreenChannel = channels.at(1);imageRedChannel = channels.at(2);
??上面的代码先做了相关的类型声明,然后把载入的 3 通道图像转换成 3 个单通道图像,放到 vector< Mat > 类型的 channels 中,接着进行引用赋值。
??另提一点,如果我们需要从多通道数组中提取特定的单通道数组,或者说实现复杂一点的多通道组合,我们可以使用 mixChannels() 函数。
3.3 示例程序
#include<opencv2/opencv.hpp>#include<iostream>using namespace std;using namespace cv;//利用感兴趣区域 ROI 实现图像叠加bool ROI_AddImage(){
//1.读入图像Mat srcImage = imread("dota_pa.jpg");Mat logoImage = imread("dota_logo.jpg");if (!srcImage.data || !logoImage.data){
printf("图像读入错误!\n");return false;}//2. 定义一个 Mat 类型并给其设定感兴趣区域 ROIMat imageROI = srcImage(Rect(200, 250, logoImage.cols, logoImage.rows));//3. 加载掩膜(必须是灰度图)Mat mask = imread("dota_logo.jpg",0);//4. 将掩膜复制到 ROI 区域logoImage.copyTo(imageROI, mask);//5. 显示结果imshow("利用掩膜实现图像叠加", srcImage);return true;}//利用 cv::addWeighted() 函数实现图像线性混合bool LinearBlending(){
//1. 定义一些局部变量double alphaValue = 0.5;double betaValue;Mat srcImage2, srcImage3, dstImage;//2. 读取图像(两幅图像大小与类型需相同)srcImage2 = imread("mogu.jpg");srcImage3 = imread("rain.jpg");if (!srcImage2.data || !srcImage3.data){
printf("图像读取错误!");return false;}//3. 进行图像混合加权操作betaValue = (1.0 - alphaValue);addWeighted(srcImage2, alphaValue, srcImage3, betaValue, 0.0, dstImage);//4. 创建并显示原图窗口namedWindow("线性混合原图", 1);imshow("线性混合原图", srcImage2);namedWindow("线性混合效果图", 1);imshow("线性混合效果图", dstImage);return true;}//利用 addWeighted 函数结合定义感兴趣区域 ROI 实现自定义区域的线性混合bool ROI_LinearBlending(){
//1. 读取图像Mat srcImage4 = imread("dota_pa.jpg", 1);Mat logoImage = imread("dota_logo.jpg");if (!logoImage.data || !srcImage4.data){
printf("图像读入错误!");return false;}//2. 定义一个感兴趣区域 ROIMat imageROI;// 方法一imageROI = srcImage4(Rect(200, 250, logoImage.cols, logoImage.rows));// 方法二//imageROI = srcImage4(Range(250, 250 + logoImage.rows), Range(200, 200 + logoImage.cols));//3. 将 logo 加到原图上addWeighted(imageROI, 0.5, logoImage, 0.3, 0., imageROI);//4. 显示效果imshow("区域线性图像混合窗口", srcImage4);return true;}int main(int argc, char** argv){
system("color 5E");if (ROI_AddImage() && LinearBlending() && ROI_LinearBlending()){
cout << "运行成功";}waitKey(0);//system("pause");return 0;}
运行结果图:
??openCV中image.copyTo()有两种形式:
??● image.copyTo(imageROI),作用是把image的内容粘贴到imageROI;
??● image.copyTo(imageROI,mask),如果在 mask 中某个像素点(i,j)的值为 1(只看第一通道,所以 mask 单通道即可),则把 image.at(i, j) 处的值直接赋给 imageROI.at(i, j),如果其值为 0 则 imageROI.at(i, j) 处保留其原始像素值(黑色)。
4. 图像的对比度、亮度值调整
4.1 理论依据
??一般的图像处理算子都是一个函数,它接受一个或多个输入图像,并产生输出图像。下面是算子的一般形式:
????????????g(x) = h( f(x) ) 或者 g(x) = h( f0(x) ······fn(x) )
??图像亮度和对比度的调整操作,其实属于图像处理变换中比较简单的一种-------点操作。点操作有一个特点:仅仅根据输入像素值(有时可能加上某些全局信息或参数),来计算相应的输出像素值。这类算子包括亮度(brightness)和对比度(contrast)调整、颜色的校正(colorcorrection)和变换(transformations)。
??两种最常用的点操作(或点算子)是乘上一个常数(对应对比度的调节)以及加上一个常数(对应亮度值的调节)。公式如下:
??????????????????g(x) = a * f(x) + b
??其中:
??● 参数 f(x) 表示源图像像素。
??● 参数 g(x) 表示输出图像像素。
??● 参数 a (需要满足 a > 0)被称为增益(gain),常常用来控制图像的对比度。
??● 参数 b 通常被称作偏置(bias),常常被用于控制图像的亮度。
??更进一步,这个式子可以写为:
??????????????g(i, j) = a * f(i, j) + b
??其中,i 和 j 表示像素位于第 i 行和第 j 列,这个式子作为我们在 OpenCV 中控制图像的对比度和亮度的理论公式。
4.2 访问图片中的像素
??为了执行如下运算:
??????????????g(i, j) = a * f(i, j) + b
??我们需要访问图像中的每一个像素。因为是对 GBR 图像进行运算,每个像素有三个值(G、B、R),所以我们必须分别访问他们。
//三个 for 循环,执行运算 new_image(i, j) = a * image(i, j) + bfor(int x = 0; x < image.rows; x++){
for(int y = 0; y < image.cols; y++){
for(int c = 0; c < 3; c++){
new_image.at<Vec3b>(x, y)[c] = saturate_cast<uchar>( (g_nContrastValue * 0.01) * (image.at<Vec3b>(x, y)[c]) + g_nBrightValue ); }}}
??● 因为浮点运算结果可能会超出像素取值范围(溢出),还可能是非整数(如果是浮点数的话),所以要用 saturate_cast 对结果进行转换,以确保它为有效值。
??● 这里的 a 也就是对比度,一般为了观察的效果,它的取值为 0.0 到 3,。0 的浮点值,但是轨迹条一般都会取整数,因此在这里我们可以将其代表对比值的 nConstrastValue 参数设置为 0 到 300 之间的整数,在最后的式子中乘以一个 0.01,这样就完成了轨迹条中 300 个不同取值的变化。这就是 g_nContrastValue * 0.01 的由来了。
4.3 实例程序:图像对比度、亮度值的调整
#include<opencv2/opencv.hpp>#include<iostream>using namespace std;using namespace cv;int g_nContrastValue; //对比度值int g_nBrightValue; //亮度值Mat g_srcImage, g_dstImage;//改变图像对比度和亮度值的回调函数static void on_ConstrastAndBringht(int, void*){
namedWindow("原始图窗口", 1);for (int x = 0; x < g_srcImage.rows; x++){
for (int y = 0; y < g_srcImage.cols; y++){
for (int c = 0; c < 3; c++){
g_dstImage.at<Vec3b>(x, y)[c] = saturate_cast<uchar>((g_nContrastValue * 0.01) * (g_srcImage.at<Vec3b>(x, y)[c]) + g_nBrightValue);}}}imshow("原始图窗口", g_srcImage);imshow("效果图窗口", g_dstImage);}int main(int argc, char** argv){
//1.读入图像g_srcImage = imread("1.jpg");if (!g_srcImage.data){
printf("图像读取错误!");return 0;}g_dstImage = Mat::zeros(g_srcImage.size(), g_srcImage.type());//2.设定对比度和亮度的初值g_nContrastValue = 80;g_nBrightValue = 80;//3.创建效果图窗口namedWindow("效果图窗口", 1);//4. 创建轨迹条createTrackbar("对比度", "效果图窗口", &g_nContrastValue, 300, on_ConstrastAndBringht);createTrackbar("亮 度", "效果图窗口", &g_nBrightValue, 200, on_ConstrastAndBringht);//5.进行回调函数初始化on_ConstrastAndBringht(g_nContrastValue, 0);on_ConstrastAndBringht(g_nBrightValue, 0);//按下 q 键时,程序退出while (char(waitKey(1) != 'q')){
}//waitKey(0);//system("pause");return 0;}
5.离散傅里叶变换
??离散傅里叶变换(Discrete Fourier Transform,DFT),是指傅里叶变换在时域和频域上都呈现离散的形式,将时域信号的采样变换为在离散时间傅里叶变换(DTFT)频域的采样。在形式上,变换两端(时域和频域上)的序列是有限长的,而实际上这两组序列都应当被认为是离散周期信号的主序列值。即使对有限长的离散信号做 DFT ,也应当对其经过周期延拓成为周期信号再进行变换。在实际应用中,通常采用快速傅里叶变换来高效计算 DFT。
5.1 离散傅里叶变换的原理
??简单来说,对一张图像使用傅里叶变换就是将它分解成正弦和余弦两部分,也就是将图像从空间域转换到频域。
??这一转换的理论基础为:任一函数都可以表示成无数个正弦和余弦函数的和的形式。傅里叶变换就是一个用来将函数分解的工具。
??二维傅里叶变换可以用以下数学公式表达:
??????????????
??式中 f 是空间域值,F是频域值,转换之后的频域值是复数,因此,显示傅里叶变换之后的结果需要用实数图像(real image)加虚数图像(complex image),或者幅度图像(magitude image)加相位图像(phase image)的形式。在实际的图像处理过程中,仅仅使用了幅度图像。
??在频域里,对于一幅图像,高频部分代表了图像的细节、纹理信息,低频部分代表了图像的轮廓信息。如果图像受到的噪声恰好位于某个特定的“频率”范围内,则可以通过滤波器来恢复原来的图像。傅里叶变换在图像处理中可以做到图像的增强与图像去噪、图像分隔之边缘检测、图像的特征提取、图像压缩等。
5.2 dft() 函数详解
??dft 的作用是对一维或二维浮点数组进行正向或反向离散傅里叶变换。
??void dft(InputArray src, OutputArray dst, int flag = 0, int nonzeroRows = 0)
??● 第一个参数:src ,输入矩阵,可以为实数或者虚数。
??● 第二个参数:dst,函数调用后的运算结果存在这里,其尺寸取决于标识符,也就是第三个参数 flags。
??● 第三个参数:flags,转换的标识符有默认值 0 ,取值可以为下表中标识符的结合。
??
??● 第四个参数:nonzeroRows,有默认值 0 ,当此参数为非零时(最好是取值为想要处理的那一行,比如 C.rows),函数会假设只有输入矩阵的第一个非零行包含非零元素(没有设置 DFT_INVERSE 标识符),或者只有输出矩阵的第一个非零行包含非零元素(设置了 DFT_INVERSE 标识符)。这样的话,函数就可以对其他进行更高效的处理,以节省时间开销。
??下面为用 dft() 函数计算两个二维实矩阵卷积的核心片段。
void convolveDFT(InputArray A, InputArray B, OutputArray C){
//1.初始化输出矩阵C.create(abs(A.rows - B.rows) + 1, abs(A.cols - B.cols) + 1, A.type());Size dftSize;//2.计算 DFT 变换的尺寸dftSize.width = getOptimalDFTSize(A.cols + B.cols - 1);dftSize.height = getOptimalDFTSize(A.rows + B.rows - 1);//3.分配临时缓冲区并初始化置零Mat tempA(dftSize, A.type(), Scalar::all(0));Mat tempB(dftSize, B.type(), Scalar::all(0));//4.分别复制 A 和 B 到 tempA 和 tempB 的左上角Mat roiA(tempA, Rect(0, 0, A.cols, A.rows));A.copyTo(roiA);Mat roiB(tempB, Rect(0, 0, B.cols, B.rows));B.copyTo(roiB);//5.就地操作(in-place),进行快速傅里叶变换,并将 nonzeroRows 参数置为非零,以进行更加快速的处理dft(tempA, tempA, 0, A.rows);dft(tempB, tempB, 0, B.rows);//6.将得到的频谱相乘,结果存放于 tempA 中mulSpectrums(tempA, tempB, tempA, DFT_COMPLEX_OUTPUT);//7.将结果变换为频域,且尽管结果行都为非零,我们只需要其中的 C.rows 的第一行,所以采用 nonzeroRows == C.rowsdft(tempA, tempA, DFT_INVERSE + DFT_SCALE, C.rows);//8.将结果复制到 C 中tempA(Rect(0, 0, C.cols, C.rows)).copyTo(C);//所有的临时缓冲区将被自动释放,所以无需首尾操作}
??上述代码中的 MulSpecturms,它的作用是计算两个傅里叶频谱的每个元素的乘法,前两个参数为输入的参加乘法运算的两个矩阵,第三个参数为得到的乘法结果矩阵。
5.3 返回 DFT 最优尺寸大小:getOptimalDFTSize() 函数
??getOptimalDFTSize() 函数返回给定向量尺寸的傅里叶最优尺寸大小。为了提高离散傅里叶变换的运行速度,需要扩充图像,而具体扩充多少,就由这个函数计算得到。
??int getOptimalDFTSize(int vcsize)
??此函数唯一的 参数为 int 型的 vecsize,向量尺寸,即图片的 rows、cols。
5.4 扩充图像边界:copyMakeBorder() 函数
??void copyMakeBorder(InputArray src, OutputArray dst, int top, int bottom, int left, int right, int borderType, const Scalar& value = Scalar() )
??● 第一个参数:src,输入图像。
??● 第二个参数:dst,输出图像,存放函数调用后的输出结果,需和源图像有一样的尺寸和类型,且 size 应该为 Size(src.cols + left + right, src.rows + top + bottom)。
??● 接下来的四个参数分别为 int 类型的 top、bottom、left、right,分别表示在源图像的四个方向上扩充多少像素。
??● 第七个参数,int 类型的 borderType,边界类型, 常见取值为 BORDER_CONSTANT。
??● 第八个参数,value,有默认值 Scalar(),可以理解为默认值为 0,当 borderType 取值为 BORDER_CONSTANT 时,这个参数表示边界值。
5.5 计算二维矢量的幅值:magnitude() 函数
??void magnitude(InputArray x, InputArray y, OutputArray magnitude)
??● 第一个参数:x,表示矢量的浮点型的 X 坐标值,也就是实部。
??● 第二个参数:y,表示矢量的浮点型的 Y 坐标值,也就是虚部。
??● 第三个参数:magnitude,输出的幅值,它和第一个参数 x 有着同样的尺寸和类型。
??下式可以表示 magnitude() 函数的原理:
????????????
计算自然对数:log() 函数
??void log(InputArray src, OutputArray dst)
??第一个参数为输入图像,第二个参数为得到的对数值,其原理如下:
????????????
5.7 矩阵归一化:normalize()
??void normalize(InputArray src, OutputArray dst, double alpha = 1, double beta = 0, int norm_type = NORM_L2, int dtype = -1, InputArray mask = noArray() )
??● 第一个参数:src,输入图像。
??● 第二个参数:dst,输出图像,和源图像有一样的大小和尺寸。
??● 第三个参数:alpha,归一化后的最大值,有默认值 1。
??● 第四个参数:beta,归一化后的最大值,有默认值 0。
??● 第五个参数:norm_type。归一化类型,有 NORM_INF、NORM_L1、NORM_L2 和 NORM_MINMAX 等参数可选,有默认值 NORM_L2。
??● 第七个参数:mask,可选的操作掩膜,有默认的 noArray()。
5.8 示例程序:离散傅里叶变换
??此示例中,将展示如何计算以及显示傅里叶变换后的幅度图像。
??● 第一步,载入原始图像
??以灰度模式读取原始图像。
//1.以灰度模式读取原始图像并显示Mat srcImage = imread("1.jpg", 0);if (!srcImage.data){
printf("图像读取失败!");return 0;}imshow("原始图像", srcImage);
??● 第二步,将图像扩大到合适的尺寸
??当图像的尺寸是 2、3、5 的整数倍时,计算速度最快。因此,为了达到快速计算的目的,经常通过添凑新的边缘像素的方法获取最佳图像尺寸。函数 getOptimalDFTSize() 用于返回最佳尺寸,而函数 copyMakeBorder() 用于填充边缘像素。
//2.将图像延扩到最佳的尺寸,边界用 0 补充int m = getOptimalDFTSize(srcImage.rows);int n = getOptimalDFTSize(srcImage.cols);//将添加的像素初始化为 0Mat padded;copyMakeBorder(srcImage, padded, 0, m - srcImage.rows, 0, n - srcImage.cols, BORDER_CONSTANT, Scalar::all(0));
??● 第三步,为傅里叶变换的结果(实部和虚部)分配存储空间
??傅里叶的变换结果是复数,这就是说,对于每个原图像值,结果会有两个图像值。此外,频域值范围远远超过空间值范围,因此至少要将频域储存在 float 格式中。所以我们将输入图像转换成浮点类型,并多加一个额外通道来存储复数部分。
//3.将傅里叶变换的结果(实部和虚部)分配存储空间//将planes 数组组合合并成一个多通道数组 complexIMat planes[] = {
Mat_<float>(padded),Mat::zeros(padded.size(),CV_32F) };Mat complexI;merge(planes, 2, complexI);
??● 第四步,进行离散傅里叶变换
??这里的离散傅里叶变换为图像就地计算模式(in-place,输入输出为同一图像)
//4.就地进行傅里叶变换dft(complexI, complexI);
??● 第五步,将复试转换为幅值
??复数包含实数部分(Re)和虚数部分(Im)。离散傅里叶变换的结果是复数,对应的幅度可以表示为:
????????????
//5.将负数转换为幅值,即 -> log(1 + sqrt(Re(DFT(I))^2 + Im(DFT(I))^2))//将多通道数组 complexI 分解成几个单通道数组,planes[0] = Re(DFT(I)),planse[1] = Im(DFT(I))split(complexI, planes); magnitude(planes[0], planes[1], planes[0]); //planes[0] = magnitudeMat magnitudeImage = planes[0];
??● 第六步,进行对数尺度缩放
??傅里叶变换的幅度值范围大到不适合在屏幕上显示,高值在屏幕上显示为白点,而低值显示为黑点,高低值的变化无法有效分辨。为了在屏幕上凸显高低变化的连续性,我们可以用对数尺度来替换线性尺度。公式如下:
????????????????
//6.进行对数尺度(logarithmic scale) 缩放magnitudeImage += Scalar::all(1);log(magnitudeImage, magnitudeImage); //求自然对数
??● 第七步,剪切和重分布图像象限
??因为在第二步中延扩了图像,因此需要将新添加的像素剔除了。为了方便显示,也可以重新分布幅度图象限位置(即将第五步得到的幅度图从中间划开,得到 4 张 1/4 的子图,将每张子图看成幅度图的一个象限,重新分布,即将四个角点重叠到图片中心),这样的话原点(0,0)就位移到图像中心了。
//7.剪切和重分布幅度图象限//若有奇数行或奇数列,进行频谱裁剪magnitudeImage = magnitudeImage(Rect(0, 0, magnitudeImage.cols & -2, magnitudeImage.rows & -2));//重新排列傅里叶图像中的象限,使得原点位于图像中心int cx = magnitudeImage.cols / 2;int cy = magnitudeImage.rows / 2;Mat q0(magnitudeImage, Rect(0, 0, cx, cy)); //ROI 区域的左上Mat q1(magnitudeImage, Rect(cx, 0, cx, cy)); //ROI 区域的右上Mat q2(magnitudeImage, Rect(0, cy, cx, cy)); //ROI 区域的左下Mat q3(magnitudeImage, Rect(cx, cy, cx, cy)); //ROI 区域的右下//交换象限(左上与右下进行交换)Mat tmp;q0.copyTo(tmp);q3.copyTo(q0);tmp.copyTo(q3);//交换象限(右上与左下进行交换)q1.copyTo(tmp);q2.copyTo(q1);tmp.copyTo(q2);
??● 第八步,归一化
??这一步仍是为了显示,现在有了重新分布后的幅度图,但是幅度值仍然超过可显示的范围[0,1],使用 normalize() 函数将幅度归一化到可显示范围。
//8.归一化,用 0 到 1 之间的浮点值将矩阵变换为可视的图像格式normalize(magnitudeImage, magnitudeImage, 0, 1, NORM_MINMAX);
??● 第九步,显示效果图
//9.显示效果图imshow("频谱幅值", magnitudeImage);
??将上述代码整合到一起,便得到了完整的程序源代码:
#include<opencv2/opencv.hpp>#include<iostream>using namespace std;using namespace cv;int main(int argc, char** argv){
//1.以灰度模式读取原始图像并显示Mat srcImage = imread("1.jpg", 0);if (!srcImage.data){
printf("图像读取失败!");return 0;}imshow("原始图像", srcImage);//2.将图像延扩到最佳的尺寸,边界用 0 补充int m = getOptimalDFTSize(srcImage.rows);int n = getOptimalDFTSize(srcImage.cols);//将添加的像素初始化为 0Mat padded;copyMakeBorder(srcImage, padded, 0, m - srcImage.rows, 0, n - srcImage.cols,BORDER_CONSTANT, Scalar::all(0));//3.将傅里叶变换的结果(实部和虚部)分配存储空间//将planes 数组组合合并成一个多通道数组 complexIMat planes[] = {
Mat_<float>(padded),Mat::zeros(padded.size(),CV_32F) };Mat complexI;merge(planes, 2, complexI);//4.就地进行傅里叶变换dft(complexI, complexI);//5.将负数转换为幅值,即 -> log(1 + sqrt(Re(DFT(I))^2 + Im(DFT(I))^2))//将多通道数组 complexI 分解成几个单通道数组,planes[0] = Re(DFT(I)),planse[1] = Im(DFT(I))split(complexI, planes); magnitude(planes[0], planes[1], planes[0]); //planes[0] = magnitudeMat magnitudeImage = planes[0];//6.进行对数尺度(logarithmic scale) 缩放magnitudeImage += Scalar::all(1);log(magnitudeImage, magnitudeImage); //求自然对数//7.剪切和重分布幅度图象限//若有奇数行或奇数列,进行频谱裁剪magnitudeImage = magnitudeImage(Rect(0, 0, magnitudeImage.cols & -2, magnitudeImage.rows & -2));//重新排列傅里叶图像中的象限,使得原点位于图像中心int cx = magnitudeImage.cols / 2;int cy = magnitudeImage.rows / 2;Mat q0(magnitudeImage, Rect(0, 0, cx, cy)); //ROI 区域的左上Mat q1(magnitudeImage, Rect(cx, 0, cx, cy)); //ROI 区域的右上Mat q2(magnitudeImage, Rect(0, cy, cx, cy)); //ROI 区域的左下Mat q3(magnitudeImage, Rect(cx, cy, cx, cy)); //ROI 区域的右下//交换象限(左上与右下进行交换)Mat tmp;q0.copyTo(tmp);q3.copyTo(q0);tmp.copyTo(q3);//交换象限(右上与左下进行交换)q1.copyTo(tmp);q2.copyTo(q1);tmp.copyTo(q2);//8.归一化,用 0 到 1 之间的浮点值将矩阵变换为可视的图像格式normalize(magnitudeImage, magnitudeImage, 0, 1, NORM_MINMAX);//9.显示效果图imshow("频谱幅值", magnitudeImage);waitKey(0);//system("pause");return 0;}
6.输入输出 XML 和 YAML 文件
6.1 XML 和 YAML 文件简介
??XML,可拓展标识语言,是一种元标记语言。
??可扩展标记语言,标准通用标记语言的子集,是一种用于标记电子文件使其具有结构性的标记语言。它可以用来标记数据、定义数据类型,是一种允许用户对自己的标记语言进行定义的源语言。
??YAML,它是类似于标准通用标记语言的子集XML的数据描述语言,语法比XML简单很多。
6.2 FileStorage 类操作文件的使用引导
??XML 和 YAML 是使用非常广泛的文件格式,可以利用 XML 或者 YAML 格式的文件存储和还原各式各样的数据结构。还可以存储和载入任意复杂的数据结构,其中就包括了 OpenCV 相关周边的数据结构,以及各种原始数据类型,如整形和浮点数字和文本字符串。
??一般使用如下过程来写入或者读入数据到 XML 或 YAML 文件中。
??1)实例化一个 FileStorage 类的对象,用默认带参数的构造函数完成初始化,或者用 FileStorage::open() 成员函数辅助初始化。
??2)使用操作符 << 进行文件写入操作,或者 >> 进行文件读取操作。
??3)使用 FileStorage::release() 函数析构掉 FileStorage 类对象,同时关闭文件。
??第一步:XML、YAML 文件的打开
??1.准备文件写操作
??构造函数 FileStorage::FileStorage,有两个重载,如下:
??● FileStorage::FileStorage()
??● FileStorage::FileStorage(const string& source, int flags, const string& encoding = string() )
??构造函数在实际使用中,方法一般有两种:
??1)对于第二种带参数的构造函数,进行写操作范例如下:
FileStorage fs("abc.xml", FileStorage::write);
??2)对弈第一种不带参数的构造函数,可以使用成员函数 FileStorage::open 进行数据的写操作,范例如下:
FileStorage fs;fs.open("abc.xml", FileStorage::write);
??2. 准备文件读操作
//第一种方式FileStorage fs("abc.xml", FileStorage::write);//第二种方式FileStorage fs;fs.open("abc.xml", FileStorage::write);
??对于 YAML 格式的文件,操作是类似的。
??第二步:进行文件的读写操作
??1.文本和数字的输入和输出
??写入文件可使用 << 运算符,如:
fs << "iterationNr" << 100;
??而读取文件,使用 >> 运算符,例如:
int itNr;fs["iterationNr"] >> itNr;itNr = (int)fs["iterationNr"];
??2.OpenCV 数据结构的输入和输出
//数据结构的初始化Mat R = Mat_<uchar>::eye(3, 3);Mat T = Mat_<double>::zeros(3, 1);//向 Mat 中写入数据fs << "R" << R;fs << "T" << T;//从 Mat 中读取数据fs["R"] >> R;fs["T"] >> T;
??第三步:vector(arrays) 和 maps 的输入和输出
??对于 vector 结构的 输入和输出,要注意在第一个元素前加上 “[”,在最后一个元素前加上 “]”。例如:
fs << "strings" << "["; //开始读入 string 文本序列fs << "image1.jpg" << "Awesomeness" << "baboon.jpg";fs << "]"; //关闭序列
??而对于 map 结构的操作,使用的是 “{” 和 “}” ,例如:
fs << "Mapping" //开始读入 Mapping 文本fs << "{" << "One" << 1;fs << "Two" << 2 << "}";
??读取这些类的时候,会用到 FileNode 和 FileNodeIterator 数据结构。对 FileStorage 类的 “[” 和 “]” 操作符会返回 FileNode 数据类型,对于一连串的 node ,可以使用 FileNodeIterator 结构,例如:
FileNode n = fs["string"]; //读取字符串序列以得到节点if(n.type() != FileNode::SEQ){
cerr << "发生错误!字符串不是一个序列" << endl;return 1; }FileNodeIterator it = n.begin(),it_end = n.end(); //遍历节点for( ; it != it_end; it++){
cout << (string)*it << endl; }
??第四步:文件关闭
??文件关闭会在 FileStorage 类销毁时自动进行,但我们也可以显示的调用其析构函数 FileStorage::release() 实现。FileStorage::release() 函数会析构掉 FileStorage 类对象,同时关闭文件。
fs.release();
6.3 示例程序:XML 和 YAML 文件的写入
#include<opencv2/opencv.hpp>#include<time.h>#include<iostream>using namespace std;using namespace cv;int main(int argc, char** argv){
//初始化FileStorage fs("test.yaml", FileStorage::WRITE);//开始文件写入fs << "frameCount" << 5;time_t rawtime;time(&rawtime);fs << "calibrationDate" << asctime(localtime(&rawtime));Mat cameraMatrix = (Mat_<double>(3, 3) << 1000, 0, 320, 0, 1000, 240, 0, 0, 1);Mat disCoeffs = (Mat_<double>(5, 1) << 0.1, 0.01, -0.001, 0, 0);fs << "cameraMatrix" << cameraMatrix << "distCoeffs" << disCoeffs;fs << "features" << "[";for (int i = 0; i < 3; i++){
int x = rand() % 640;int y = rand() % 480;uchar lbp = rand() % 256;fs << "{:" << "x" << x << "y" << y << "lbp" << "[:";for (int j = 0; j < 8; j++){
fs << ((lbp >> j) & 1);}fs << "]" << "}";}fs << "]";fs.release();printf("文件读写完毕,请在工程目录下查看生成的文件");getchar();waitKey(0);//system("pause");return 0;}
??其中,FileStorage fs(“test.yaml”, FileStorage::WRITE); 这一句代码,将其中的 “text.yaml” 的后缀换为 xml、yml、txt、甚至 doc 都行。
运行截图:
??文件目录下:
内容:
6.4 示例程序: XML 和 YAML 文件的读取
#include<opencv2/opencv.hpp>#include<time.h>#include<iostream>using namespace std;using namespace cv;int main(int argc, char** argv){
system("color 6F"); //改变 console 字体颜色//初始化FileStorage fs2("test.yaml", FileStorage::READ);//第一种方法,对 FileNode 操作,int frameCount = (int)fs2["frameCount"];std::string date;//第二种方法,使用 FileNode 运算符 >> fs2["calibrationDate"] >> date;Mat cameraMartix2, distCoeffs2;fs2["cameraMatrix"] >> cameraMartix2;fs2["distCoeffs"] >> distCoeffs2;cout << "frameCount:" << frameCount << endl<< "cailbration date:" << date << endl<< "camera matrix:" << cameraMartix2 << endl<< "distortion coeffs:" << distCoeffs2 << endl;FileNode features = fs2["features"];FileNodeIterator it = features.begin(), it_end = features.end();int idx = 0;std::vector<uchar> lbpval;//使用 FileNodeIterator 遍历序列for (; it != it_end; it++,idx++){
cout << "feature #" << idx << ": ";cout << "x=" << (int)(*it)["x"] << ",y = " << (int)(*it)["y"] << ", lbp: (";//也可以使用 filenode >> std::vector 操作符来读取数值阵列(*it)["lbp"] >> lbpval;for (int i = 0; i < (int)lbpval.size(); i++){
cout << " " << (int)lbpval[i];}cout << ")" << endl;}fs2.release();printf("\n文件读入完毕");getchar();waitKey(0);//system("pause");return 0;}
运行结果: