《OpenCV3编程入门 》学习笔记 第5章 core组件进阶

本章讲解core模块的一些进阶知识点,如操作图像中的像素、图像混合、分离颜色通道、调节图像的对比度和亮度、进行离散傅里叶变换,以及输入输出XML和YAML文件。

5.1 访问图像中的像素

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

在之前的章节中,我们已经了解到图像矩阵的大小取决于所用的颜色模型,确切地说,取决于所用通道数。如果是灰度图像,矩阵就会如图5.1所示。

而对多通道图像来说,矩阵中的列会包含多个子列,其子列个数与通道数相等。例如,如图5.2所示RGB颜色模型的矩阵。

可以看到,OpenCV中子列的通道顺序是反过来的——BGR而不是RGB。很多情况下,因为内存足够大,可实现连续存储,因此,图像中的各行就能一行一行地连接起来,形成一个长行。连续存储有助于提升图像扫描速度,我们可以使用isContinuous()来判断矩阵是否是连续存储的。相关示例会在接下来的内容中提供。

5.1.2 颜色空间缩减

我们知道,若矩阵元素存储的是单通道像素,使用C或C++的无符号字符类型,那么像素可有256个不同值。但若是三通道图像,这种存储格式的颜色数就太多了(确切地说,有一千六百多万种)。用如此之多的颜色来进行处理,可能会对我们的算法性能造成严重影响。

其实,仅用这些颜色中具有代表性的很小的部分,就足以达到同样的效果。

如此,颜色空间缩减(color space reduction)便可以派上用场了,它在很多应用中可以大大降低运算复杂度。颜色空间缩减的做法是:将现有颜色空间值除以某个输入值,以获得较少的颜色数。也就是“做减法”,比如颜色值0到9可取为新值0,10到19可取为10,以此类推。

如uchar类型的三通道图像,每个通道取值可以是0~255,于是就有256(256个不同的值。我们可以定义:

0~9范围的像素值为0;

10~19范围的像素值为10;

20~29范围的像素值为20。

这样的操作将颜色取值降低为26(26(26种情况。这个操作可以用一个简单的公式来实现。因为C++中int类型除法操作会自动截余。例如Iold=14;Inew=(Iold/10)*10=(14/10)*10=1*10=10;

uchar(无符号字符,即0到255之间取值的数)类型的值除以int值,结果仍是char。因为结果是char类型的,所以求出来小数也要向下取整。利用这一点,刚才提到在uchar定义域中进行的颜色缩减运算就可以表达为下面的形式:

在处理图像像素时,每个像素需要进行一遍上述计算,也需要一定的时间花销。但我们注意到其实只有0~255种像素,即只有256种情况。进一步可以把256种计算好的结果提前存在表中table中,这样每种情况不需计算,直接从table中取结果即可。

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

于是table[i]存放的是值为i的像素减小颜色空间的结果,这样也就可以理解上述方法中的操作:

p[j] = table[p[j]];

 这样,简单的颜色空间缩减算法就可由下面两步组成:

(1)遍历图像矩阵的每一个像素;

(2)对像素应用上述公式。

值得注意的是,我们这里用到了除法和乘法运算,而这两种运算又特别费时,所以,应尽可能用代价较低的加、减、赋值等运算来替换它们。此外,还应注意到,上述运算的输入仅能在某个有限范围内取值,如uchar类型可取256个值。

由此可知,对于较大的图像,有效的方法是预先计算所有可能的值,然后需要这些值的时候,利用查找表直接赋值即可。查找表是一维或多维数组,存储了不同输入值所对应的输出值,其优势在于只需读取、无须计算。

5.1.3 LUT函数:Look up table操作

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

5.1.4 计时函数

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

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

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

这两个函数组合起来使用的示例如下。

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

5.1.5 访问图像中像素的三类方法

任何图像处理算法,都是从操作每个像素开始的。即使我们不会使用OpenCV提供的各种图像处理函数,只要了解了图像处理算法的基本原理,也可以写出具有相同功能的程序。在OpenCV中,提供了三种访问每个像素的方法。

● 方法一 指针访问:C操作符[];

● 方法二 迭代器iterator;

● 方法三 动态地址计算。

这三种方法在访问速度上略有差异。debug模式下,这种差异非常明显,不过在release模式下,这种差异就不太明显了。我们通过一组程序来说明这几种方法。程序的目的是减少图像中颜色的数量,比如原来的图像是是256种颜色,我们希望将它变成64种颜色,那只需要将原来的颜色除以4(整除)以后再乘以4就可以了。

将要使用的主程序代码如下。


/*	@File				: 21_UsePointerAccessPixel.cpp
 *  @Brief				: 示例程序21
 *  @Details			: 用指针访问像素
 *  @Date				: 2015-11-01
 *  @OpenCV Version		: 4.8.0
 *  @Development Tools	: Windows 11 64bit && Visual Studio 2017
 *  @Modify				: 2024-03-31
*/

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

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



//--------------------------------------【main( )函数】---------------------------------------
//          描述:控制台应用程序的入口函数,我们的程序从这里开始执行
//-----------------------------------------------------------------------------------------------
int main( )  
{  
	//【1】创建原始图并显示
	Mat srcImage = imread("1.jpg");  
	imshow("原始图像",srcImage);  

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

	ShowHelpText();

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

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

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

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

//-----------------------------------【ShowHelpText( )函数】----------------------------------
//          描述:输出一些帮助信息
//----------------------------------------------------------------------------------------------
void ShowHelpText()
{
	//输出欢迎信息和OpenCV版本
	printf("\n\n\t\t\t此为本书OpenCV3版的第21个配套示例程序\n");
	printf("\n\n\t\t\t   当前使用的OpenCV版本为:" CV_VERSION );
	printf("\n\n  ----------------------------------------------------------------------------\n");
}


主程序中调用colorReduce函数来完成减少颜色的工作,而我们根据访问每个像素三类方法,写出了三个版本的colorReduce函数。下面,我们将分别对其进行简单地讲解。

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

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

//---------------------------------【colorReduce( )函数】---------------------------------
//          描述:使用【指针访问:C操作符[ ]】方法版的颜色空间缩减函数
//----------------------------------------------------------------------------------------------
void colorReduce(Mat& inputImage, Mat& outputImage, int div)  
{  
	//参数准备
	outputImage = inputImage.clone();  //拷贝实参到临时变量
	int rowNumber = outputImage.rows;  //行数
	int colNumber = outputImage.cols*outputImage.channels();  //列数 x 通道数=每一行元素的个数

	//双重循环,遍历所有的像素值
	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;  
			// ----------【处理结束】---------------------
		}  //行处理结束
	}  
}

运行结果:

下面对上述代码进行讲解。

Mat类有若干成员函数可以获取图像的属性。公有成员变量cols和rows给出了图像的宽和高,而成员函数channels()用于返回图像的通道数。灰度图的通道数为1,彩色图的通道数为3。

每行的像素值由以下语句得到:

int colNumber = outputImage.cols*outputImage.channels();  //列数 x 通道数=每一行元素的个数

为了简化指针运算,Mat类提供了ptr函数可以得到图像任意行的首地址。ptr是一个模板函数,它返回第i行的首地址:

uchar* data = outputImage.ptr<uchar>(i);  //获取第i行的首地址

而双层循环内部的那句处理像素的代码,我们可以等效地使用指针运算从一列移动到下一列。所以,也可以这样来写:

data[j] = data[j]/div*div + div/2; 

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

第二种方法为用迭代器操作像素,这种方法与STL库的用法类似。

在迭代法中,我们所需要做的仅仅是获得图像矩阵的begin和end,然后增加迭代直至从begin到end。将*操作符添加在迭代指针前,即可访问当前指向的内容。

相比用指针直接访问可能出现越界问题,迭代器绝对是非常安全的方法。



/*	@File				: 22_UseIteratorAccessPixel.cpp
 *  @Brief				: 示例程序22
 *  @Details			: 用迭代器访问像素
 *  @Date				: 2015-11-01
 *  @OpenCV Version		: 4.8.0
 *  @Development Tools	: Windows 11 64bit && Visual Studio 2017
 *  @Modify				: 2024-03-31
*/

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



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



//--------------------------------------【main( )函数】--------------------------------------
//		描述:控制台应用程序的入口函数,我们的程序从这里开始执行
//-----------------------------------------------------------------------------------------------
int main( )  
{  
	//【1】创建原始图并显示
	Mat srcImage = imread("1.jpg");  
	imshow("原始图像",srcImage);  

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

	ShowHelpText();

	//【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);  
}  




//-------------------------------------【colorReduce( )函数】-----------------------------
//		描述:使用【迭代器】方法版的颜色空间缩减函数
//----------------------------------------------------------------------------------------------
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;  
		// ------------------------【处理结束】----------------------------
	}  
}  



//-----------------------------------【ShowHelpText( )函数】----------------------------------
//          描述:输出一些帮助信息
//----------------------------------------------------------------------------------------------
void ShowHelpText()
{
	//输出欢迎信息和OpenCV版本
	printf("\n\n\t\t\t此为本书OpenCV3版的第22个配套示例程序\n");
	printf("\n\n\t\t\t   当前使用的OpenCV版本为:" CV_VERSION );
	printf("\n\n  ----------------------------------------------------------------------------\n");
}


实验条件下单次运行时间为0.242588。

不熟悉面向对象编程中迭代器的概念的读者,可以阅读与STL中迭代器相关的入门书籍和文字。用关键字“STL迭代器”进行搜索可以找到各种相关的博文和资料。

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

第三种方法为用动态地址计算来操作像素。下面是使用动态地址运算配合at方法的colorReduce函数的代码。这种方法简洁明了,符合大家对像素的直观认识。实验条件下单次运行时间约为0.334131。


/*	@File				: 23_UseAtAccessPixel.cpp
 *  @Brief				: 示例程序23
 *  @Details			: 用动态地址计算配合at访问像素
 *  @Date				: 2015-11-01
 *  @OpenCV Version		: 4.8.0
 *  @Development Tools	: Windows 11 64bit && Visual Studio 2017
 *  @Modify				: 2024-03-31
*/

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

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


//--------------------------------------【main( )函数】---------------------------------------
//          描述:控制台应用程序的入口函数,我们的程序从这里开始执行
//-----------------------------------------------------------------------------------------------
int main( )  
{  
	system("color 9F");
	//【1】创建原始图并显示
	Mat srcImage = imread("1.jpg");  
	imshow("原始图像",srcImage);  

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

	ShowHelpText();

	//【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);  
}  


//----------------------------------【colorReduce( )函数】-------------------------------
//          描述:使用【动态地址运算配合at】方法版本的颜色空间缩减函数
//----------------------------------------------------------------------------------------------
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;  //红是通道
			// -------------------------【处理结束】----------------------------
		}  // 行处理结束     
	}  
}  


//-------------------------------【ShowHelpText( )函数】--------------------------------
//          描述:输出一些帮助信息
//----------------------------------------------------------------------------------------------
void ShowHelpText()
{
	//输出欢迎信息和OpenCV版本
	 
	printf("\n\n\t\t\t此为本书OpenCV3版的第23个配套示例程序\n");
	printf("\n\n\t\t\t   当前使用的OpenCV版本为:" CV_VERSION );
	printf("\n\n  ----------------------------------------------------------------------------\n");
}

让我们讲解一下上述的代码。

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

对于彩色图像,每个像素由三个部分构成:蓝色通道、绿色通道和红色通道(BGR)。因此,对于一个包含彩色图像的Mat,会返回一个由三个8位数组成的向量。OpenCV将此类型的向量定义为Vec3b,即由三个unsigned char组成的向量。这也解释了为什么存取彩色图像像素的代码可以写出如下形式:

image.at<Vec3b>(j,i)[channel]=value;

其中,索引值channel标明了颜色通道号。

另外需要再次提醒大家的是,OpenCV中的彩色图像不是以RGB的顺序存放的,而是BGR,所以程序中的outputImage.at<Vec3b>(i, j)[0]代表的是该点的B分量。同理还有(*it)[0]。

5.1.6 示例程序本节至此结束。在这里说明一下本节相关的配套示例程序。本节共有四个配套的示例程序,按示例程序序号来排列为:

【21】用指针访问像素

【22】用迭代器访问像素

【23】用动态地址计算配合at访问像素

【24】遍历图像像素的14种方法其中,第21~23分别为5.1.5节中讲到三种方法的示例程序,而第24个示例程序“遍历图像像素的14种方法”为来自一本国外OpenCV2书籍的配套示例程序,其用14个函数分别封装好了14种像素存取方法。比较典型,于是将其注释后提供给大家学习。


/*	@File				: 24_14WaysToAccessPixel.cpp
 *  @Brief				: 示例程序24
 *  @Details			: 来自一本国外OpenCV2书籍的示例-遍历图像像素的14种方法
 *  @Date				: 2015-11-01
 *  @OpenCV Version		: 4.8.0
 *  @Development Tools	: Windows 11 64bit && Visual Studio 2017
 *  @Modify				: 2024-03-31
*/
/*------------------------------------------------------------------------------------------*\
   This file contains material supporting chapter 2 of the cookbook:  
   Computer Vision Programming using the OpenCV Library. 
   by Robert Laganiere, Packt Publishing, 2011.

   This program is free software; permission is hereby granted to use, copy, modify, 
   and distribute this source code, or portions thereof, for any purpose, without fee, 
   subject to the restriction that the copyright notice may not be removed 
   or altered from any source or altered source distribution. 
   The software is released on an as-is basis and without any warranties of any kind. 
   In particular, the software is not guaranteed to be fault-tolerant or free from failure. 
   The author disclaims all warranties with regard to this software, any use, 
   and any consequent failure, is purely the responsibility of the user.
 
   Copyright (C) 2010-2011 Robert Laganiere, www.laganiere.name
\*------------------------------------------------------------------------------------------*/


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



//---------------------------------【宏定义部分】---------------------------------------------
//		描述:包含程序所使用宏定义
//-------------------------------------------------------------------------------------------------
#define NTESTS 14
#define NITERATIONS 20



//----------------------------------------- 【方法一】-------------------------------------------
//		说明:利用.ptr 和 []
//-------------------------------------------------------------------------------------------------
void colorReduce0(Mat &image, int div=64) {

	  int nl= image.rows; //行数
	  int nc= image.cols * image.channels(); //每行元素的总元素数量
              
      for (int j=0; j<nl; j++) 
	  {

		  uchar* data= image.ptr<uchar>(j);

          for (int i=0; i<nc; i++) 
		  {
 
            //-------------开始处理每个像素-------------------
                 
                  data[i]= data[i]/div*div + div/2;
 
            //-------------结束像素处理------------------------
 
            } //单行处理结束                  
      }
}

//-----------------------------------【方法二】-------------------------------------------------
//		说明:利用 .ptr 和 * ++ 
//-------------------------------------------------------------------------------------------------
void colorReduce1(Mat &image, int div=64) {

	  int nl= image.rows; //行数
	  int nc= image.cols * image.channels(); //每行元素的总元素数量
              
      for (int j=0; j<nl; j++) 
	  {

		  uchar* data= image.ptr<uchar>(j);

          for (int i=0; i<nc; i++) 
		  {
 
            //-------------开始处理每个像素-------------------
                 
				 *data++= *data/div*div + div/2;
 
            //-------------结束像素处理------------------------
 
            } //单行处理结束              
      }
}

//-----------------------------------------【方法三】-------------------------------------------
//		说明:利用.ptr 和 * ++ 以及模操作
//-------------------------------------------------------------------------------------------------
void colorReduce2(Mat &image, int div=64) {

	  int nl= image.rows; //行数
	  int nc= image.cols * image.channels(); //每行元素的总元素数量
              
      for (int j=0; j<nl; j++) 
	  {

		  uchar* data= image.ptr<uchar>(j);

          for (int i=0; i<nc; i++) 
		  {
 
            //-------------开始处理每个像素-------------------
       
			      int v= *data;
                  *data++= v - v%div + div/2;
 
            //-------------结束像素处理------------------------
 
            } //单行处理结束                   
      }
}

//----------------------------------------【方法四】---------------------------------------------
//		说明:利用.ptr 和 * ++ 以及位操作
//----------------------------------------------------------------------------------------------------
void colorReduce3(Mat &image, int div=64) {

	  int nl= image.rows; //行数
	  int nc= image.cols * image.channels(); //每行元素的总元素数量
	  int n= static_cast<int>(log(static_cast<double>(div))/log(2.0));
	  //掩码值
	  uchar mask= 0xFF<<n; // e.g. 对于 div=16, mask= 0xF0
              
      for (int j=0; j<nl; j++) {

		  uchar* data= image.ptr<uchar>(j);

          for (int i=0; i<nc; i++) {
 
            //------------开始处理每个像素-------------------
                 
            *data++= *data&mask + div/2;
 
            //-------------结束像素处理------------------------
            }  //单行处理结束            
      }
}


//----------------------------------------【方法五】----------------------------------------------
//		说明:利用指针算术运算
//---------------------------------------------------------------------------------------------------
void colorReduce4(Mat &image, int div=64) {

	  int nl= image.rows; //行数
	  int nc= image.cols * image.channels(); //每行元素的总元素数量
	  int n= static_cast<int>(log(static_cast<double>(div))/log(2.0));
	  int step= image.step; //有效宽度
	  //掩码值
	  uchar mask= 0xFF<<n; // e.g. 对于 div=16, mask= 0xF0
              
      //获取指向图像缓冲区的指针
	  uchar *data= image.data;

      for (int j=0; j<nl; j++)
	  {

          for (int i=0; i<nc; i++) 
		  {
 
            //-------------开始处理每个像素-------------------
                 
            *(data+i)= *data&mask + div/2;
 
            //-------------结束像素处理------------------------
 
            } //单行处理结束              

            data+= step;  // next line
      }
}

//---------------------------------------【方法六】----------------------------------------------
//		说明:利用 .ptr 和 * ++以及位运算、image.cols * image.channels()
//-------------------------------------------------------------------------------------------------
void colorReduce5(Mat &image, int div=64) {

	  int nl= image.rows; //行数
	  int n= static_cast<int>(log(static_cast<double>(div))/log(2.0));
	  //掩码值
	  uchar mask= 0xFF<<n; // e.g. 例如div=16, mask= 0xF0
              
      for (int j=0; j<nl; j++) 
	  {

		  uchar* data= image.ptr<uchar>(j);

          for (int i=0; i<image.cols * image.channels(); i++) 
		  {
 
            //-------------开始处理每个像素-------------------
                 
            *data++= *data&mask + div/2;
 
            //-------------结束像素处理------------------------
 
            } //单行处理结束            
      }
}

// -------------------------------------【方法七】----------------------------------------------
//		说明:利用.ptr 和 * ++ 以及位运算(continuous)
//-------------------------------------------------------------------------------------------------
void colorReduce6(Mat &image, int div=64) {

	  int nl= image.rows; //行数
	  int nc= image.cols * image.channels(); //每行元素的总元素数量

	  if (image.isContinuous())  
	  {
		  //无填充像素
		  nc= nc*nl; 
		  nl= 1;  // 为一维数列
	   }

	  int n= static_cast<int>(log(static_cast<double>(div))/log(2.0));
	  //掩码值
	  uchar mask= 0xFF<<n; // e.g. 比如div=16, mask= 0xF0
              
      for (int j=0; j<nl; j++) {

		  uchar* data= image.ptr<uchar>(j);

          for (int i=0; i<nc; i++) {
 
            //-------------开始处理每个像素-------------------
                 
            *data++= *data&mask + div/2;
 
            //-------------结束像素处理------------------------
 
            } //单行处理结束                   
      }
}

//------------------------------------【方法八】------------------------------------------------
//		说明:利用 .ptr 和 * ++ 以及位运算 (continuous+channels)
//-------------------------------------------------------------------------------------------------
void colorReduce7(Mat &image, int div=64) {

	  int nl= image.rows; //行数
	  int nc= image.cols ; //列数

	  if (image.isContinuous())  
	  {
		  //无填充像素
		  nc= nc*nl; 
		  nl= 1;  // 为一维数组
	   }

	  int n= static_cast<int>(log(static_cast<double>(div))/log(2.0));
	  //掩码值
	  uchar mask= 0xFF<<n; // e.g. 比如div=16, mask= 0xF0
              
      for (int j=0; j<nl; j++) {

		  uchar* data= image.ptr<uchar>(j);

          for (int i=0; i<nc; i++) {
 
            //-------------开始处理每个像素-------------------
                 
            *data++= *data&mask + div/2;
            *data++= *data&mask + div/2;
            *data++= *data&mask + div/2;
 
            //-------------结束像素处理------------------------
 
            } //单行处理结束                    
      }
}


// -----------------------------------【方法九】 ------------------------------------------------
//		说明:利用Mat_ iterator
//-------------------------------------------------------------------------------------------------
void colorReduce8(Mat &image, int div=64) {

	  //获取迭代器
	  Mat_<Vec3b>::iterator it= image.begin<Vec3b>();
	  Mat_<Vec3b>::iterator itend= image.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;

        //-------------结束像素处理------------------------
	  }//单行处理结束  
}

//-------------------------------------【方法十】-----------------------------------------------
//		说明:利用Mat_ iterator以及位运算
//-------------------------------------------------------------------------------------------------
void colorReduce9(Mat &image, int div=64) {

	  // div必须是2的幂
	  int n= static_cast<int>(log(static_cast<double>(div))/log(2.0));
	  //掩码值
	  uchar mask= 0xFF<<n; // e.g. 比如 div=16, mask= 0xF0

	  // 获取迭代器
	  Mat_<Vec3b>::iterator it= image.begin<Vec3b>();
	  Mat_<Vec3b>::iterator itend= image.end<Vec3b>();

	  //扫描所有元素
	  for ( ; it!= itend; ++it) 
	  {
        
		//-------------开始处理每个像素-------------------

        (*it)[0]= (*it)[0]&mask + div/2;
        (*it)[1]= (*it)[1]&mask + div/2;
        (*it)[2]= (*it)[2]&mask + div/2;

        //-------------结束像素处理------------------------
	  }//单行处理结束  
}

//------------------------------------【方法十一】---------------------------------------------
//		说明:利用Mat Iterator_
//-------------------------------------------------------------------------------------------------
void colorReduce10(Mat &image, int div=64) {

	  //获取迭代器
	  Mat_<Vec3b> cimage= image;
	  Mat_<Vec3b>::iterator it=cimage.begin();
	  Mat_<Vec3b>::iterator itend=cimage.end();

	  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;

        //-------------结束像素处理------------------------
	  }
}

//--------------------------------------【方法十二】--------------------------------------------
//		说明:利用动态地址计算配合at
//-------------------------------------------------------------------------------------------------
void colorReduce11(Mat &image, int div=64) {

	  int nl= image.rows; //行数
	  int nc= image.cols; //列数
              
      for (int j=0; j<nl; j++) 
	  {
          for (int i=0; i<nc; i++) 
		  {
 
            //-------------开始处理每个像素-------------------
                 
                  image.at<Vec3b>(j,i)[0]=	 image.at<Vec3b>(j,i)[0]/div*div + div/2;
                  image.at<Vec3b>(j,i)[1]=	 image.at<Vec3b>(j,i)[1]/div*div + div/2;
                  image.at<Vec3b>(j,i)[2]=	 image.at<Vec3b>(j,i)[2]/div*div + div/2;
 
            //-------------结束像素处理------------------------
 
            } //单行处理结束                 
      }
}

//----------------------------------【方法十三】----------------------------------------------- 
//		说明:利用图像的输入与输出
//-------------------------------------------------------------------------------------------------
void colorReduce12(const Mat &image, //输入图像
                 Mat &result,      // 输出图像
                 int div=64) {

	  int nl= image.rows; //行数
	  int nc= image.cols ; //列数

	  //准备好初始化后的Mat给输出图像
	  result.create(image.rows,image.cols,image.type());

	  //创建无像素填充的图像
	  nc= nc*nl; 
	  nl= 1;  //单维数组

	  int n= static_cast<int>(log(static_cast<double>(div))/log(2.0));
	  //掩码值
	  uchar mask= 0xFF<<n; // e.g.比如div=16, mask= 0xF0
              
      for (int j=0; j<nl; j++) {

		  uchar* data= result.ptr<uchar>(j);
		  const uchar* idata= image.ptr<uchar>(j);

          for (int i=0; i<nc; i++) {
 
            //-------------开始处理每个像素-------------------
                 
            *data++= (*idata++)&mask + div/2;
            *data++= (*idata++)&mask + div/2;
            *data++= (*idata++)&mask + div/2;
 
            //-------------结束像素处理------------------------
 
          } //单行处理结束                   
      }
}

//--------------------------------------【方法十四】------------------------------------------- 
//		说明:利用操作符重载
//-------------------------------------------------------------------------------------------------
void colorReduce13(Mat &image, int div=64) {
	
	  int n= static_cast<int>(log(static_cast<double>(div))/log(2.0));
	  //掩码值
	  uchar mask= 0xFF<<n; // e.g. 比如div=16, mask= 0xF0

	  //进行色彩还原
	  image=(image&Scalar(mask,mask,mask))+Scalar(div/2,div/2,div/2);
}




//-----------------------------------【ShowHelpText( )函数】-----------------------------
//		描述:输出一些帮助信息
//----------------------------------------------------------------------------------------------
void ShowHelpText()
{
	//输出欢迎信息和OpenCV版本
	printf("\n\n\t\t\t非常感谢购买《OpenCV3编程入门》一书!\n");
	printf("\n\n\t\t\t此为本书OpenCV3版的第24个配套示例程序\n");
	printf("\n\n\t\t\t   当前使用的OpenCV版本为:" CV_VERSION );
	printf("\n\n  ----------------------------------------------------------------------------\n");

	printf("\n\n正在进行存取操作,请稍等……\n\n");
}




//-----------------------------------【main( )函数】--------------------------------------------
//		描述:控制台应用程序的入口函数,我们的程序从这里开始
//-------------------------------------------------------------------------------------------------
int main( )
{
	int64 t[NTESTS],tinit;
	Mat image0;
	Mat image1;
	Mat image2;

	system("color 4F");

	ShowHelpText();

	image0= imread("1.png");
	if (!image0.data)
		return 0; 

	//时间值设为0
	for (int i=0; i<NTESTS; i++)
		t[i]= 0;


	// 多次重复测试
	int n=NITERATIONS;
	for (int k=0; k<n; k++)
	{
		cout << k << " of " << n << endl; 

		image1= imread("1.png");
		//【方法一】利用.ptr 和 []
	    tinit= getTickCount();
		colorReduce0(image1);
		t[0]+= getTickCount()-tinit;

		//【方法二】利用 .ptr 和 * ++ 
		image1= imread("1.png");
	    tinit= getTickCount();
		colorReduce1(image1);
		t[1]+= getTickCount()-tinit;

		//【方法三】利用.ptr 和 * ++ 以及模操作
		image1= imread("1.png");
	    tinit= getTickCount();
		colorReduce2(image1);
		t[2]+= getTickCount()-tinit;

		//【方法四】 利用.ptr 和 * ++ 以及位操作
		image1= imread("1.png");
	    tinit= getTickCount();
		colorReduce3(image1);
		t[3]+= getTickCount()-tinit;

		//【方法五】 利用指针的算术运算
		image1= imread("1.png");
	    tinit= getTickCount();
		colorReduce4(image1);
		t[4]+= getTickCount()-tinit;

		//【方法六】利用 .ptr 和 * ++以及位运算、image.cols * image.channels()
		image1= imread("1.png");
	    tinit= getTickCount();
		colorReduce5(image1);
		t[5]+= getTickCount()-tinit;

		//【方法七】利用.ptr 和 * ++ 以及位运算(continuous)
		image1= imread("1.png");
	    tinit= getTickCount();
		colorReduce6(image1);
		t[6]+= getTickCount()-tinit;

		//【方法八】利用 .ptr 和 * ++ 以及位运算 (continuous+channels)
		image1= imread("1.png");
	    tinit= getTickCount();
		colorReduce7(image1);
		t[7]+= getTickCount()-tinit;

		//【方法九】 利用Mat_ iterator
		image1= imread("1.png");
	    tinit= getTickCount();
		colorReduce8(image1);
		t[8]+= getTickCount()-tinit;

		//【方法十】 利用Mat_ iterator以及位运算
		image1= imread("1.png");
	    tinit= getTickCount();
		colorReduce9(image1);
		t[9]+= getTickCount()-tinit;

		//【方法十一】利用Mat Iterator_
		image1= imread("1.png");
	    tinit= getTickCount();
		colorReduce10(image1);
		t[10]+= getTickCount()-tinit;

		//【方法十二】 利用动态地址计算配合at
		image1= imread("1.png");
	    tinit= getTickCount();
		colorReduce11(image1);
		t[11]+= getTickCount()-tinit;

		//【方法十三】 利用图像的输入与输出
		image1= imread("1.png");
	    tinit= getTickCount();
		Mat result;
		colorReduce12(image1, result);
		t[12]+= getTickCount()-tinit;
		image2= result;
		
		//【方法十四】 利用操作符重载
		image1= imread("1.png");
	    tinit= getTickCount();
		colorReduce13(image1);
		t[13]+= getTickCount()-tinit;

		//------------------------------
	}
	 //输出图像   
	imshow("原始图像",image0);
	imshow("结果",image2);
	imshow("图像结果",image1);

	// 输出平均执行时间
	cout << endl << "-------------------------------------------" << endl << endl;
	cout << "\n【方法一】利用.ptr 和 []的方法所用时间为 " << 1000.*t[0]/getTickFrequency()/n << "ms" << endl;
	cout << "\n【方法二】利用 .ptr 和 * ++ 的方法所用时间为" << 1000.*t[1]/getTickFrequency()/n << "ms" << endl;
	cout << "\n【方法三】利用.ptr 和 * ++ 以及模操作的方法所用时间为" << 1000.*t[2]/getTickFrequency()/n << "ms" << endl;
	cout << "\n【方法四】利用.ptr 和 * ++ 以及位操作的方法所用时间为" << 1000.*t[3]/getTickFrequency()/n << "ms" << endl;
	cout << "\n【方法五】利用指针算术运算的方法所用时间为" << 1000.*t[4]/getTickFrequency()/n << "ms" << endl;
	cout << "\n【方法六】利用 .ptr 和 * ++以及位运算、channels()的方法所用时间为" << 1000.*t[5]/getTickFrequency()/n << "ms" << endl;
	cout << "\n【方法七】利用.ptr 和 * ++ 以及位运算(continuous)的方法所用时间为" << 1000.*t[6]/getTickFrequency()/n << "ms" << endl;
	cout << "\n【方法八】利用 .ptr 和 * ++ 以及位运算 (continuous+channels)的方法所用时间为" << 1000.*t[7]/getTickFrequency()/n << "ms" << endl;
	cout << "\n【方法九】利用Mat_ iterator 的方法所用时间为" << 1000.*t[8]/getTickFrequency()/n << "ms" << endl;
	cout << "\n【方法十】利用Mat_ iterator以及位运算的方法所用时间为" << 1000.*t[9]/getTickFrequency()/n << "ms" << endl;
	cout << "\n【方法十一】利用Mat Iterator_的方法所用时间为" << 1000.*t[10]/getTickFrequency()/n << "ms" << endl;	
	cout << "\n【方法十二】利用动态地址计算配合at 的方法所用时间为" << 1000.*t[11]/getTickFrequency()/n << "ms" << endl;	
	cout << "\n【方法十三】利用图像的输入与输出的方法所用时间为" << 1000.*t[12]/getTickFrequency()/n << "ms" << endl;	
	cout << "\n【方法十四】利用操作符重载的方法所用时间为" << 1000.*t[13]/getTickFrequency()/n << "ms" << endl;	
	
	waitKey();
	return 0;
}

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

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

5.2.1 感兴趣区域:ROI

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

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

其中,image为已经载入好的图片。

Mat imageROI;
//方法一
imageROI= srcImage4(Rect(200,250,logoImage.cols,logoImage.rows));

另一种定义ROI的方式是指定感兴趣行或列的范围(Range)。Range是指从起始索引到终止索引(不包括终止索引)的一连段连续序列。cRange可以用来定义Range。如果使用Range来定义ROI,那么前例中定义ROI的代码可以重写为:

//方法二
imageROI= srcImage4(Range(250,250+logoImage.rows),Range(200,200+logoImage.cols));

下面我们来看一个实例,显示如何利用ROI将一幅图加到另一幅图的指定位置。大家如果需要复制以下函数中的代码直接运行,可以自己建一个基于console的程序,然后把函数体中的内容复制到main函数中,然后找两幅大小合适的图片,加入到工程目录下,并和代码中读取的文件名一致即可。

在下面的代码中,我们通过一个图像掩膜(mask),直接将插入处的像素设置为logo图像的像素值,这样效果会很逼真。


/*	@File				: 25_ImageBlending.cpp
 *  @Brief				: 示例程序25
 *  @Details			: 初级图像混合
 *  @Date				: 2015-11-01
 *  @OpenCV Version		: 4.8.0
 *  @Development Tools	: Windows 11 64bit && Visual Studio 2017
 *  @Modify				: 2024-03-31
*/

//---------------------------------【头文件、命名空间包含部分】-------------------------------
//		描述:包含程序所使用的头文件和命名空间
//------------------------------------------------------------------------------------------------                                                                                 
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <iostream>

using namespace cv;
using namespace std;


//-----------------------------------【全局函数声明部分】--------------------------------------
//	描述:全局函数声明
//-----------------------------------------------------------------------------------------------
bool  ROI_AddImage();
bool  LinearBlending();
bool  ROI_LinearBlending();
void   ShowHelpText();

//-----------------------------------【main( )函数】--------------------------------------------
//	描述:控制台应用程序的入口函数,我们的程序从这里开始
//-----------------------------------------------------------------------------------------------
int main(   )
{
	system("color 6F");
 
	ShowHelpText();

	if(ROI_AddImage( )&& LinearBlending( )&&ROI_LinearBlending( ))
	{
		cout<<endl<<"\n运行成功,得出了需要的图像";
	}

	waitKey(0);
	return 0;
}


//-----------------------------------【ShowHelpText( )函数】----------------------------------
//		 描述:输出一些帮助信息
//----------------------------------------------------------------------------------------------
void ShowHelpText()
{
	//输出欢迎信息和OpenCV版本
	printf("\n\n\t\t\t此为本书OpenCV3版的第25个配套示例程序\n");
	printf("\n\n\t\t\t   当前使用的OpenCV版本为:" CV_VERSION );
	printf("\n\n  ----------------------------------------------------------------------------\n");
}




//----------------------------------【ROI_AddImage( )函数】----------------------------------
// 函数名:ROI_AddImage()
//	描述:利用感兴趣区域ROI实现图像叠加
//----------------------------------------------------------------------------------------------
bool  ROI_AddImage()
{

	// 【1】读入图像
	Mat srcImage1= imread("dota_pa.jpg");
	Mat logoImage= imread("dota_logo.jpg");
	if( !srcImage1.data ) { printf("读取srcImage1错误~! \n"); return false; }
	if( !logoImage.data ) { printf("读取logoImage错误~! \n"); return false; }

	// 【2】定义一个Mat类型并给其设定ROI区域
	Mat imageROI= srcImage1(Rect(200,250,logoImage.cols,logoImage.rows));

	// 【3】加载掩模(必须是灰度图)
	Mat mask= imread("dota_logo.jpg",0);

	//【4】将掩膜拷贝到ROI
	logoImage.copyTo(imageROI,mask);

	// 【5】显示结果
	namedWindow("<1>利用ROI实现图像叠加示例窗口");
	imshow("<1>利用ROI实现图像叠加示例窗口",srcImage1);

	return true;
}


//---------------------------------【LinearBlending()函数】-------------------------------------
// 函数名:LinearBlending()
// 描述:利用cv::addWeighted()函数实现图像线性混合
//--------------------------------------------------------------------------------------------
bool  LinearBlending()
{
	//【0】定义一些局部变量
	double alphaValue = 0.5; 
	double betaValue;
	Mat srcImage2, srcImage3, dstImage;

	// 【1】读取图像 ( 两幅图片需为同样的类型和尺寸 )
	srcImage2 = imread("mogu.jpg");
	srcImage3 = imread("rain.jpg");

	if( !srcImage2.data ) { printf("读取srcImage2错误! \n"); return false; }
	if( !srcImage3.data ) { printf("读取srcImage3错误! \n"); return false; }

	// 【2】进行图像混合加权操作
	betaValue = ( 1.0 - alphaValue );
	addWeighted( srcImage2, alphaValue, srcImage3, betaValue, 0.0, dstImage);

	// 【3】显示原图窗口
	imshow( "<2>线性混合示例窗口【原图】", srcImage2 );
	imshow( "<3>线性混合示例窗口【效果图】", dstImage );

	return true;

}

//---------------------------------【ROI_LinearBlending()】-------------------------------------
// 函数名:ROI_LinearBlending()
// 描述:线性混合实现函数,指定区域线性图像混合.利用cv::addWeighted()函数结合定义
//			  感兴趣区域ROI,实现自定义区域的线性混合
//--------------------------------------------------------------------------------------------
bool  ROI_LinearBlending()
{

	//【1】读取图像
	Mat srcImage4= imread("dota_pa.jpg",1);
	Mat logoImage= imread("dota_logo.jpg");

	if( !srcImage4.data ) { printf("读取srcImage4错误~! \n"); return false; }
	if( !logoImage.data ) { printf("读取logoImage错误~! \n"); return false; }

	//【2】定义一个Mat类型并给其设定ROI区域
	Mat 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("<4>区域线性图像混合示例窗口",srcImage4);

	return true;
}

这个函数首先是载入了两张jpg图片到srcImage1和logoImage中,然后定义了一个Mat类型的imageROI,并使用Rect设置其感兴趣区域为srcImage1中的一块区域,将imageROI和srcImage1关联起来。接着定义了一个Mat类型的的mask并读入dota_logo.jpg,顺势使用Mat::copyTo把mask中的内容复制到imageROI中,于是就得到了最终的效果图。namedWindow和imshow配合使用,显示出最终的结果。

运行结果如图5.3所示。

图5.3中白色的dota2 logo,就是通过操作之后加上去的图像。

5.2.2 线性混合操作

线性混合操作是一种典型的二元(两个输入)的像素操作,它的理论公式如下:

我们通过在范围0到1之间改变alpha值,来对两幅图像(f0(x)和f1(x))或两段视频(同样为(f0(x)和f1(x))产生时间上的画面叠化(cross-dissolve)效果,就像幻灯片放映和电影制作中的那样,也就是在幻灯片翻页时设置的前后页缓慢过渡叠加效果,以及电影情节过渡时经常出现的画面叠加效果。

实现方面,主要运用了OpenCV中addWeighted函数,下面来一起全面地了解它。

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

这个函数的作用是计算两个数组(图像阵列)的加权和。原型如下:

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

● 第一个参数,InputArray类型的src1,表示需要加权的第一个数组,常常填一个Mat;

● 第二个参数,double类型的alpha,表示第一个数组的权重;

● 第三个参数,InputArray类型的src2,表示第二个数组,它需要和第一个数组拥有相同的尺寸和通道数;

● 第四个参数,double类型的beta,表示第二个数组的权重值;

● 第五个参数,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时,这个函数就不适用了,这时候就会内存溢出或者算出的结果压根不对。

理论和函数的讲解就是上面这些,接着我们来看代码实例,以融会贯通。

//---------------------------------【LinearBlending()函数】-------------------------------------
// 函数名:LinearBlending()
// 描述:利用cv::addWeighted()函数实现图像线性混合
//--------------------------------------------------------------------------------------------
bool  LinearBlending()
{
	//【0】定义一些局部变量
	double alphaValue = 0.5; 
	double betaValue;
	Mat srcImage2, srcImage3, dstImage;

	// 【1】读取图像 ( 两幅图片需为同样的类型和尺寸 )
	srcImage2 = imread("mogu.jpg");
	srcImage3 = imread("rain.jpg");

	if( !srcImage2.data ) { printf("读取srcImage2错误! \n"); return false; }
	if( !srcImage3.data ) { printf("读取srcImage3错误! \n"); return false; }

	// 【2】进行图像混合加权操作
	betaValue = ( 1.0 - alphaValue );
	addWeighted( srcImage2, alphaValue, srcImage3, betaValue, 0.0, dstImage);

	// 【3】显示原图窗口
	imshow( "<2>线性混合示例窗口【原图】", srcImage2 );
	imshow( "<3>线性混合示例窗口【效果图】", dstImage );

	return true;

}

下面对以上代码进行解析。

(0)首先当然是定义一些局部变量、alpha值、beta值,以及三个Mat类型的变量

//【0】定义一些局部变量
double alphaValue = 0.5; 
double betaValue;
Mat srcImage2, srcImage3, dstImage;

在这里我们设置alpha值为0.5。

(1)读取两幅图像并作错误处理

这步很简单,直接看代码。

    // 【1】读取图像 ( 两幅图片需为同样的类型和尺寸 )
	srcImage2 = imread("mogu.jpg");
	srcImage3 = imread("rain.jpg");

	if( !srcImage2.data ) { printf("读取srcImage2错误! \n"); return false; }
	if( !srcImage3.data ) { printf("读取srcImage3错误! \n"); return false; }

在这里需要注意的是,因为我们是对srcImage1和srcImage2求和,所以它们必须要有相同的尺寸(宽度和高度)和类型,不然多余的部分没有对应的“伴”,肯定会出问题。

(2)进行图像混合加权操作

载入图像后,我们就可以来生成混合图像,也就是之前公式中的g(x)。为此目的,使用函数addWeighted可以很方便地实现。代码其实很简单,就是这样:

    // 【2】进行图像混合加权操作
	betaValue = ( 1.0 - alphaValue );
	addWeighted( srcImage2, alphaValue, srcImage3, betaValue, 0.0, dstImage);

(3)创建显示窗口,显示图像

    // 【3】显示原图窗口
	imshow( "<2>线性混合示例窗口【原图】", srcImage2 );
	imshow( "<3>线性混合示例窗口【效果图】", dstImage );

接着来看一下运行效果图,首先是原图(图5.4)。

                                                           图5.4 原始图

然后是效果图,如图5.5所示。

                                                         图5.5 线性混合效果图

 5.2.4 综合示例:初级图像混合

在前文介绍的设定感兴趣区域ROI和使用addWeighted函数进行图像线性混合的基础上,我们还可以将二者结合起来使用,也就是先指定ROI,再用addWeighted函数对指定的ROI区域的图像进行混合操作。我们将其封装在了一个名为ROI_LinearBlending的函数中,方便大家分块学习。代码如下。

//---------------------------------【ROI_LinearBlending()】-------------------------------------
// 函数名:ROI_LinearBlending()
// 描述:线性混合实现函数,指定区域线性图像混合.利用cv::addWeighted()函数结合定义
//			  感兴趣区域ROI,实现自定义区域的线性混合
//--------------------------------------------------------------------------------------------
bool  ROI_LinearBlending()
{

	//【1】读取图像
	Mat srcImage4= imread("dota_pa.jpg",1);
	Mat logoImage= imread("dota_logo.jpg");

	if( !srcImage4.data ) { printf("读取srcImage4错误~! \n"); return false; }
	if( !logoImage.data ) { printf("读取logoImage错误~! \n"); return false; }

	//【2】定义一个Mat类型并给其设定ROI区域
	Mat 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("<4>区域线性图像混合示例窗口",srcImage4);

	return true;
}

下面放出详细注释的本节示例源代码,示例代码都将封装在了不同的函数中,具体如下。


/*	@File				: 25_ImageBlending.cpp
 *  @Brief				: 示例程序25
 *  @Details			: 初级图像混合
 *  @Date				: 2015-11-01
 *  @OpenCV Version		: 4.8.0
 *  @Development Tools	: Windows 11 64bit && Visual Studio 2017
 *  @Modify				: 2024-03-31
*/

//---------------------------------【头文件、命名空间包含部分】-------------------------------
//		描述:包含程序所使用的头文件和命名空间
//------------------------------------------------------------------------------------------------                                                                                 
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <iostream>

using namespace cv;
using namespace std;


//-----------------------------------【全局函数声明部分】--------------------------------------
//	描述:全局函数声明
//-----------------------------------------------------------------------------------------------
bool  ROI_AddImage();
bool  LinearBlending();
bool  ROI_LinearBlending();
void   ShowHelpText();

//-----------------------------------【main( )函数】--------------------------------------------
//	描述:控制台应用程序的入口函数,我们的程序从这里开始
//-----------------------------------------------------------------------------------------------
int main(   )
{
	system("color 6F");
 
	ShowHelpText();

	if(ROI_AddImage( )&& LinearBlending( )&&ROI_LinearBlending( ))
	{
		cout<<endl<<"\n运行成功,得出了需要的图像";
	}

	waitKey(0);
	return 0;
}


//-----------------------------------【ShowHelpText( )函数】----------------------------------
//		 描述:输出一些帮助信息
//----------------------------------------------------------------------------------------------
void ShowHelpText()
{
	//输出欢迎信息和OpenCV版本
	printf("\n\n\t\t\t此为本书OpenCV3版的第25个配套示例程序\n");
	printf("\n\n\t\t\t   当前使用的OpenCV版本为:" CV_VERSION );
	printf("\n\n  ----------------------------------------------------------------------------\n");
}




//----------------------------------【ROI_AddImage( )函数】----------------------------------
// 函数名:ROI_AddImage()
//	描述:利用感兴趣区域ROI实现图像叠加
//----------------------------------------------------------------------------------------------
bool  ROI_AddImage()
{

	// 【1】读入图像
	Mat srcImage1= imread("dota_pa.jpg");
	Mat logoImage= imread("dota_logo.jpg");
	if( !srcImage1.data ) { printf("读取srcImage1错误~! \n"); return false; }
	if( !logoImage.data ) { printf("读取logoImage错误~! \n"); return false; }

	// 【2】定义一个Mat类型并给其设定ROI区域
	Mat imageROI= srcImage1(Rect(200,250,logoImage.cols,logoImage.rows));

	// 【3】加载掩模(必须是灰度图)
	Mat mask= imread("dota_logo.jpg",0);

	//【4】将掩膜拷贝到ROI
	logoImage.copyTo(imageROI,mask);

	// 【5】显示结果
	namedWindow("<1>利用ROI实现图像叠加示例窗口");
	imshow("<1>利用ROI实现图像叠加示例窗口",srcImage1);

	return true;
}


//---------------------------------【LinearBlending()函数】-------------------------------------
// 函数名:LinearBlending()
// 描述:利用cv::addWeighted()函数实现图像线性混合
//--------------------------------------------------------------------------------------------
bool  LinearBlending()
{
	//【0】定义一些局部变量
	double alphaValue = 0.5; 
	double betaValue;
	Mat srcImage2, srcImage3, dstImage;

	// 【1】读取图像 ( 两幅图片需为同样的类型和尺寸 )
	srcImage2 = imread("mogu.jpg");
	srcImage3 = imread("rain.jpg");

	if( !srcImage2.data ) { printf("读取srcImage2错误! \n"); return false; }
	if( !srcImage3.data ) { printf("读取srcImage3错误! \n"); return false; }

	// 【2】进行图像混合加权操作
	betaValue = ( 1.0 - alphaValue );
	addWeighted( srcImage2, alphaValue, srcImage3, betaValue, 0.0, dstImage);

	// 【3】显示原图窗口
	imshow( "<2>线性混合示例窗口【原图】", srcImage2 );
	imshow( "<3>线性混合示例窗口【效果图】", dstImage );

	return true;

}

//---------------------------------【ROI_LinearBlending()】-------------------------------------
// 函数名:ROI_LinearBlending()
// 描述:线性混合实现函数,指定区域线性图像混合.利用cv::addWeighted()函数结合定义
//			  感兴趣区域ROI,实现自定义区域的线性混合
//--------------------------------------------------------------------------------------------
bool  ROI_LinearBlending()
{

	//【1】读取图像
	Mat srcImage4= imread("dota_pa.jpg",1);
	Mat logoImage= imread("dota_logo.jpg");

	if( !srcImage4.data ) { printf("读取srcImage4错误~! \n"); return false; }
	if( !logoImage.data ) { printf("读取logoImage错误~! \n"); return false; }

	//【2】定义一个Mat类型并给其设定ROI区域
	Mat 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("<4>区域线性图像混合示例窗口",srcImage4);

	return true;
}

最后看一下整体的运行效果,如图5.6、图5.7、图5.8、图5.9所示。

                                                            图5.6 图像叠加效果图

                                                  图5.7 区域线性混合效果图

                                                 图5.8 线性混合原始图

                                                   图5.9 线性混合效果图

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

上节中我们讲解了如何使用addWeighted函数进行图像混合操作,以及如何将ROI和addWeighted函数结合起来,对指定区域进行图像混合操作。

而为了更好地观察一些图像材料的特征,有时需要对RGB三个颜色通道的分量进行分别显示和调整。通过OpenCV的split和merge方法可以很方便地达到目的。

这一节,我们会详细介绍这两个互为“冤家”的函数。首先来看看进行通道分离的split函数。

5.3.1 通道分离:split()函数split函数

用于将一个多通道数组分离成几个单通道数组。这里的array按语境翻译为数组或者阵列。

这个split函数的C++版本有两个原型,分别是:

● C++:void split(const Mat&src, Mat*mvbegin);

● C++:void split(InputArray m, OutputArrayOfArrays mv);

变量介绍如下:

● 第一个参数,InputArray类型的m或者const Mat&类型的src,填我们需要进行分离的多通道数组。

● 第二个参数,OutputArrayOfArrays类型的mv,填函数的输出数组或者输出的vector容器。

split函数分割多通道数组转换成独立的单通道数组,公式如下:

mv[c](I)=src(I)c

最后我们一起看一个示例。

Mat srcImage;
Mat imageROI;
vector<Mat> channels;

srcImage= imread("dota.jpg");

//把一个3通道图像转换成3个单通道图像
split(srcImage,channels);//分离色彩通道
imageROI=channels.at(0);

addWeighted(imageROI(Rect(500,250,logoImage.cols,logoImage.rows)),1.0,
	logoImage,0.5,0,imageROI(Rect(500,250,logoImage.cols,logoImage.rows)));

namedWindow("sample");
imshow("sample",srcImage);

将一个多通道数组分离成几个单通道数组的split()函数的内容大概就是以上这些了,下面我们来看一下和它关系密切的merge()函数。

5.3.2 通道合并:merge()函数

merge()函数是split()函数的逆向操作——将多个数组合并成一个多通道的数组。它通过组合一些给定的单通道数组,将这些孤立的单通道数组合并成一个多通道的数组,从而创建出一个由多个单通道阵列组成的多通道阵列。它有两个基于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()方法返回一个引用到指定的数组元素。注意是引用,相当于两者等价,也就是修改其中一个,另一个也会随之改变。

依然是一个示例,如下。

上面的代码先做了相关的类型声明,然后把载入的3通道图像转换成3个单通道图像,放到vector<Mat>类型的channels中,接着进行引用赋值。

根据OpenCV的BGR色彩空间(Bule、Green、Red,蓝绿红),其中channels.at(0)就表示引用取出channels中的蓝色分量,channels.at(1)就表示引用取出channels中的绿色分量,channels.at(2)就表示引用取出channels中的红色分量。

一对做相反操作的split()函数和merge()函数的用法就是这些。另外提一点,如果我们需要从多通道数组中提取出特定的单通道数组,或者说实现一些复杂的通道组合,可以使用mixChannels()函数。

5.3.3 示例程序:多通道图像混合

在本小节展示的示例程序中,我们把多通道图像混合的实现代码封装在了名为MultiChannelBlending()的函数中。详细注释的代码如下。


/*	@File				: 26_MultiChannelImageBlending.cpp
 *  @Brief				: 示例程序26
 *  @Details			: 分离颜色通道&多通道图像混合
 *  @Date				: 2015-11-01
 *  @OpenCV Version		: 4.8.0
 *  @Development Tools	: Windows 11 64bit && Visual Studio 2017
 *  @Modify				: 2024-03-31
*/

//-----------------------------------【头文件包含部分】---------------------------------------
//	描述:包含程序所依赖的头文件
//------------------------------------------------------------------------------------------------                                                                                     
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <iostream>

//-----------------------------------【命名空间声明部分】---------------------------------------
//	描述:包含程序所使用的命名空间
//-------------------------------------------------------------------------------------------------   
using namespace cv;
using namespace std;


//-----------------------------------【全局函数声明部分】--------------------------------------
//	描述:全局函数声明
//-----------------------------------------------------------------------------------------------
bool  MultiChannelBlending();
void ShowHelpText();


//-----------------------------------【main( )函数】------------------------------------------
//	描述:控制台应用程序的入口函数,我们的程序从这里开始
//-----------------------------------------------------------------------------------------------
int main(   )
{
	system("color 9F");

	ShowHelpText( );

	if(MultiChannelBlending( ))
	{
		cout<<endl<<"\n运行成功,得出了需要的图像~! ";
	}

	waitKey(0);
	return 0;
}



//-----------------------------------【ShowHelpText( )函数】----------------------------------
//		 描述:输出一些帮助信息
//----------------------------------------------------------------------------------------------
void ShowHelpText()
{
	//输出欢迎信息和OpenCV版本
	printf("\n\n\t\t\t此为本书OpenCV3版的第26个配套示例程序\n");
	printf("\n\n\t\t\t   当前使用的OpenCV版本为:" CV_VERSION );
	printf("\n\n  ----------------------------------------------------------------------------\n");
}






//-----------------------------【MultiChannelBlending( )函数】--------------------------------
//	描述:多通道混合的实现函数
//-----------------------------------------------------------------------------------------------
bool  MultiChannelBlending()
{
	//【0】定义相关变量
	Mat srcImage;
	Mat logoImage;
	vector<Mat> channels;
	Mat  imageBlueChannel;

	//=================【蓝色通道部分】=================
	//	描述:多通道混合-蓝色分量部分
	//============================================

	// 【1】读入图片
	logoImage= imread("dota_logo.jpg",0);
	srcImage= imread("dota_jugg.jpg");

	if( !logoImage.data ) { printf("Oh,no,读取logoImage错误~! \n"); return false; }
	if( !srcImage.data ) { printf("Oh,no,读取srcImage错误~! \n"); return false; }

	//【2】把一个3通道图像转换成3个单通道图像
	split(srcImage,channels);//分离色彩通道

	//【3】将原图的蓝色通道引用返回给imageBlueChannel,注意是引用,相当于两者等价,修改其中一个另一个跟着变
	imageBlueChannel= channels.at(0);
	//【4】将原图的蓝色通道的(500,250)坐标处右下方的一块区域和logo图进行加权操作,将得到的混合结果存到imageBlueChannel中
	addWeighted(imageBlueChannel(Rect(500,250,logoImage.cols,logoImage.rows)),1.0,
		logoImage,0.5,0,imageBlueChannel(Rect(500,250,logoImage.cols,logoImage.rows)));

	//【5】将三个单通道重新合并成一个三通道
	merge(channels,srcImage);

	//【6】显示效果图
	namedWindow(" <1>游戏原画+logo蓝色通道");
	imshow(" <1>游戏原画+logo蓝色通道",srcImage);


	//=================【绿色通道部分】=================
	//	描述:多通道混合-绿色分量部分
	//============================================

	//【0】定义相关变量
	Mat  imageGreenChannel;

	//【1】重新读入图片
	logoImage= imread("dota_logo.jpg",0);
	srcImage= imread("dota_jugg.jpg");

	if( !logoImage.data ) { printf("读取logoImage错误~! \n"); return false; }
	if( !srcImage.data ) { printf("读取srcImage错误~! \n"); return false; }

	//【2】将一个三通道图像转换成三个单通道图像
	split(srcImage,channels);//分离色彩通道

	//【3】将原图的绿色通道的引用返回给imageBlueChannel,注意是引用,相当于两者等价,修改其中一个另一个跟着变
	imageGreenChannel= channels.at(1);
	//【4】将原图的绿色通道的(500,250)坐标处右下方的一块区域和logo图进行加权操作,将得到的混合结果存到imageGreenChannel中
	addWeighted(imageGreenChannel(Rect(500,250,logoImage.cols,logoImage.rows)),1.0,
		logoImage,0.5,0.,imageGreenChannel(Rect(500,250,logoImage.cols,logoImage.rows)));

	//【5】将三个独立的单通道重新合并成一个三通道
	merge(channels,srcImage);

	//【6】显示效果图
	namedWindow("<2>游戏原画+logo绿色通道");
	imshow("<2>游戏原画+logo绿色通道",srcImage);



	//=================【红色通道部分】=================
	//	描述:多通道混合-红色分量部分
	//============================================

	//【0】定义相关变量
	Mat  imageRedChannel;

	//【1】重新读入图片
	logoImage= imread("dota_logo.jpg",0);
	srcImage= imread("dota_jugg.jpg");

	if( !logoImage.data ) { printf("Oh,no,读取logoImage错误~! \n"); return false; }
	if( !srcImage.data ) { printf("Oh,no,读取srcImage错误~! \n"); return false; }

	//【2】将一个三通道图像转换成三个单通道图像
	split(srcImage,channels);//分离色彩通道

	//【3】将原图的红色通道引用返回给imageBlueChannel,注意是引用,相当于两者等价,修改其中一个另一个跟着变
	imageRedChannel= channels.at(2);
	//【4】将原图的红色通道的(500,250)坐标处右下方的一块区域和logo图进行加权操作,将得到的混合结果存到imageRedChannel中
	addWeighted(imageRedChannel(Rect(500,250,logoImage.cols,logoImage.rows)),1.0,
		logoImage,0.5,0.,imageRedChannel(Rect(500,250,logoImage.cols,logoImage.rows)));

	//【5】将三个独立的单通道重新合并成一个三通道
	merge(channels,srcImage);

	//【6】显示效果图
	namedWindow("<3>游戏原画+logo红色通道 ");
	imshow("<3>游戏原画+logo红色通道 ",srcImage);

	return true;
}


可以发现,其实多通道混合的实现函数中的代码大体可分成三部分,分别对蓝绿红三个通道进行处理,唯一不同的地方在于取通道分量时取的是channels.at(0)、channels.at(1)还是channels.at(2)。

运行截图如图5.10、图5.11、图5.12所示,具体区别表现在箭头指向处。

                                             图5.10 游戏原画+logo蓝色通道

                                           图5.11 游戏原画+logo绿色通道

                                                图5.12 游戏原画+logo红色通道

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

本节我们将学习如何用OpenCV进行图像对比度和亮度值的动态调整。

5.4.1 理论依据

首先了解一下算子的概念。一般的图像处理算子都是一个函数,它接受一个或多个输入图像,并产生输出图像。下面是算子的一般形式。

本节所讲解的图像亮度和对比度的调整操作,其实属于图像处理变换中比较简单的一种——点操作(pointoperators)。点操作有一个特点:仅仅根据输入像素值(有时可加上某些全局信息或参数),来计算相应的输出像素值。这类算子包括亮度(brightness)和对比度(contrast)调整、颜色校正(colorcorrection)和变换(transformations)。

两种最常用的点操作(或者说点算子)是乘上一个常数(对应对比度的调节)以及加上一个常数(对应亮度值的调节)。公式如下:

看到这个式子,我们关于图像亮度和对比度调整的策略就比较好理解了。

其中:

● 参数f(x)表示源图像像素。

● 参数g(x)表示输出图像像素。

● 参数a(需要满足a>0)被称为增益(gain),常常被用来控制图像的对比度。

● 参数b通常被称为偏置(bias),常常被用来控制图像的亮度。

而更近一步,我们这样改写这个式子:

其中,i和j表示像素位于第i行和第j列,这个式子可以用来作为我们在OpenCV中控制图像的亮度和对比度的理论公式。

5.4.2 访问图片中的像素

访问图片中的像素有很多种方式,在本书5.1节“访问图像中的像素”中已有过比较系统的讲解。

而为了执行如下运算:

我们需要访问图像的每一个像素。因为是对GBR图像进行运算,每个像素有三个值(G、B、R),所以我们必须分别访问它们(OpenCV中的图像存储模式为GBR)。以下是访问像素的代码片段,使用了三个for循环。

// 三个for循环,执行运算 g_dstImage(i,j) = a*g_srcImage(i,j) + b
	for( int y = 0; y < g_srcImage.rows; y++ )
	{
		for( int x = 0; x < g_srcImage.cols; x++ )
		{
			for( int c = 0; c < 3; c++ )
			{
				g_dstImage.at<Vec3b>(y,x)[c] = saturate_cast<uchar>( (g_nContrastValue*0.01)*( g_srcImage.at<Vec3b>(y,x)[c] ) + g_nBrightValue );
			}
		}
	}

让我们分三个方面进行讲解。

● 为了访问图像的每一个像素,使用这样的语法:image.at<Vec3b>(y, x)[c]。其中,y是像素所在的行,x是像素所在的列,c是R、G、B(对应0、1、2)其中之一。

● 因为运算结果可能会超出像素取值范围(溢出),还可能是非整数(如果是浮点数的话),所以要用saturate_cast对结果进行转换,以确保它为有效值。

● 这里的a也就是对比度,一般为了观察的效果,它的取值为0.0到3.0的浮点值,但是轨迹条一般取值都会取整数,因此在这里我们可以将其代表对比度值的nContrastValue参数设为0到300之间的整型,在最后的式子中乘以一个0.01,这样就完成了轨迹条中300个不同取值的变化。这就是为什么在式子中,会有saturate_cast<uchar>((g_nContrastValue*0.01)*(image.at<Vec3b>(y, x)[c])+g_nBrightValue)中的g_nContrastValue*0.01。

5.4.3 示例程序:图像对比度、亮度值调整

本小节依然是一个详细注释的配套示例程序,把本节前文介绍的知识点以代码为载体,展现给大家。

此示例程序用两个轨迹条分别控制对比度和亮度值,详细注释的示例程序代码如下。


/*	@File				: 27_ChangeContrastAndBright.cpp
 *  @Brief				: 示例程序27
 *  @Details			: 图像对比度、亮度值调整
 *  @Date				: 2015-11-01
 *  @OpenCV Version		: 4.8.0
 *  @Development Tools	: Windows 11 64bit && Visual Studio 2017
 *  @Modify				: 2024-03-31
*/

//-----------------------------------【头文件包含部分】---------------------------------------
//	描述:包含程序所依赖的头文件
//---------------------------------------------------------------------------------------------- 
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include "opencv2/imgproc.hpp"
#include <iostream>

//-----------------------------------【命名空间声明部分】---------------------------------------
//	描述:包含程序所使用的命名空间
//-----------------------------------------------------------------------------------------------   
using namespace std;
using namespace cv;


//-----------------------------------【全局函数声明部分】--------------------------------------
//	描述:全局函数声明
//-----------------------------------------------------------------------------------------------
static void ContrastAndBright(int, void *);
void   ShowHelpText();

//-----------------------------------【全局变量声明部分】--------------------------------------
//	描述:全局变量声明
//-----------------------------------------------------------------------------------------------
int g_nContrastValue; //对比度值
int g_nBrightValue;  //亮度值
Mat g_srcImage,g_dstImage;
//-----------------------------------【main( )函数】--------------------------------------------
//	描述:控制台应用程序的入口函数,我们的程序从这里开始
//-----------------------------------------------------------------------------------------------
int main(   )
{
	//改变控制台前景色和背景色
	system("color 2F");  

	ShowHelpText();
	// 读入用户提供的图像
	g_srcImage = imread( "1.jpg");
	if( !g_srcImage.data ) { printf("读取g_srcImage图片错误~! \n"); return false; }
	g_dstImage = Mat::zeros( g_srcImage.size(), g_srcImage.type() );

	//设定对比度和亮度的初值
	g_nContrastValue=80;
	g_nBrightValue=80;

	//创建窗口
	namedWindow("【效果图窗口】", 1);

	//创建轨迹条
	createTrackbar("对比度:", "【效果图窗口】",&g_nContrastValue, 300,ContrastAndBright );
	createTrackbar("亮   度:", "【效果图窗口】",&g_nBrightValue, 200,ContrastAndBright );

	//调用回调函数
	ContrastAndBright(g_nContrastValue,0);
	ContrastAndBright(g_nBrightValue,0);

	//输出一些帮助信息
	cout<<endl<<"\t运行成功,请调整滚动条观察图像效果\n\n"
		<<"\t按下“q”键时,程序退出\n";

	//按下“q”键时,程序退出
	while(char(waitKey(1)) != 'q') {}
	return 0;
}




//-----------------------------------【ShowHelpText( )函数】----------------------------------
//		 描述:输出一些帮助信息
//----------------------------------------------------------------------------------------------
void ShowHelpText()
{
	//输出欢迎信息和OpenCV版本
	printf("\n\n\t\t\t此为本书OpenCV3版的第27个配套示例程序\n");
	printf("\n\n\t\t\t   当前使用的OpenCV版本为:" CV_VERSION );
	printf("\n\n  ----------------------------------------------------------------------------\n");
}


//-----------------------------【ContrastAndBright( )函数】------------------------------------
//	描述:改变图像对比度和亮度值的回调函数
//-----------------------------------------------------------------------------------------------
static void ContrastAndBright(int, void *)
{

	// 创建窗口
	namedWindow("【原始图窗口】", 1);

	// 三个for循环,执行运算 g_dstImage(i,j) = a*g_srcImage(i,j) + b
	for( int y = 0; y < g_srcImage.rows; y++ )
	{
		for( int x = 0; x < g_srcImage.cols; x++ )
		{
			for( int c = 0; c < 3; c++ )
			{
				g_dstImage.at<Vec3b>(y,x)[c] = saturate_cast<uchar>( (g_nContrastValue*0.01)*( g_srcImage.at<Vec3b>(y,x)[c] ) + g_nBrightValue );
			}
		}
	}

	// 显示图像
	imshow("【原始图窗口】", g_srcImage);
	imshow("【效果图窗口】", g_dstImage);
}


讲解一下上述代码中的saturate_cast模板函数,其用于溢出保护,大致原理如下。

if (data < 0)
	data = 0;
else if (data > 255)
    data = 255;

最后看一下程序的运行截图。运行这个程序会得到两个图片显示窗口:第一个为原图窗口(图5.13),第二个为效果图窗口(图5.14)。在效果图窗口中可以调节两个轨迹条,来改变当前图片的对比度和亮度。

                                            图5.13 原始图

                                       图5.14 可调节对比度和亮度的效果图

5.5 离散傅里叶变换

离散傅里叶变换(Discrete Fourier Transform,缩写为DFT),是指傅里叶变换在时域和频域上都呈现离散的形式,将时域信号的采样变换为在离散时间傅里叶变换(DTFT)频域的采样。在形式上,变换两端(时域和频域上)的序列是有限长的,而实际上这两组序列都应当被认为是离散周期信号的主值序列。即使对有限长的离散信号做DFT,也应当对其经过周期延拓成为周期信号再进行变换。在实际应用中,通常采用快速傅里叶变换来高效计算DFT。

5.5.1 离散傅里叶变换的原理

简单来说,对一张图像使用傅里叶变换就是将它分解成正弦和余弦两部分,也就是将图像从空间域(spatial domain)转换到频域(frequency domain)。

这一转换的理论基础为:任一函数都可以表示成无数个正弦和余弦函数的和的形式。傅里叶变换就是一个用来将函数分解的工具。

二维图像的傅里叶变换可以用以下数学公式表达。

式中f是空间域(spatial domain)值,F是频域(frequency domain)值。转换之后的频域值是复数,因此,显示傅里叶变换之后的结果需要使用实数图像(real image)加虚数图像(complex image),或者幅度图像(magitude image)加相位图像(phase image)的形式。在实际的图像处理过程中,仅仅使用了幅度图像,因为幅度图像包含了原图像的几乎所有我们需要的几何信息。然而,如果想通过修改幅度图像或者相位图像的方法来间接修改原空间图像,需要使用逆傅里叶变换得到修改后的空间图像,这样就必须同时保留幅度图像和相位图像了。

在此示例中,我们将展示如何计算以及显示傅里叶变换后的幅度图像。由于数字图像的离散性,像素值的取值范围也是有限的。比如在一张灰度图像中,像素灰度值一般在0到255之间。因此,我们这里讨论的也仅仅是离散傅里叶变换(DFT)。如果需要得到图像中的几何结构信息,那你就要用到它了。

在频域里面,对于一幅图像,高频部分代表了图像的细节、纹理信息;低频部分代表了图像的轮廓信息。如果对一幅精细的图像使用低通滤波器,那么滤波后的结果就只剩下轮廓了。这与信号处理的基本思想是相通的。如果图像受到的噪声恰好位于某个特定的“频率”范围内,则可以通过滤波器来恢复原来的图像。傅里叶变换在图像处理中可以做到图像增强与图像去噪、图像分割之边缘检测、图像特征提取、图像压缩等。

5.5.2 dft()函数详解

dft函数的作用是对一维或二维浮点数数组进行正向或反向离散傅里叶变换。

c++: void dft(InputArray src, OutputArray dst, int flags = 0, int nonzeroRows = 0);

● 第一个参数,InputArray类型的src。输入矩阵,可以为实数或者虚数。

● 第二个参数,OutputArray类型的dst。函数调用后的运算结果存在这里,其尺寸和类型取决于标识符,也就是第三个参数flags。

● 第三个参数,int类型的flags。转换的标识符,有默认值0,取值可以为表6.1中标识符的结合。

                                           表6.1 dft标识符取值列表

● 第四个参数,int类型的nonzeroRows,有默认值0。当此参数设为非零时(最好是取值为想要处理的那一行的值,比如C.rows),函数会假设只有输入矩阵的第一个非零行包含非零元素(没有设置DFT_INVERSE标识符),或只有输出矩阵的第一个非零行包含非零元素(设置了DFT_INVERSE标识符)。这样的话,函数就可对其他行进行更高效的处理,以节省时间开销。这项技术尤其是在采用DFT计算矩阵卷积时非常有效。

 讲解完函数的参数含义,下面我们看一个用dft函数计算两个二维实矩阵卷积的示例核心片段。

此示例中的注释已经十分详尽。其中出现了新的函数MulSpectrums,它的作用是计算两个傅里叶频谱的每个元素的乘法,前两个参数为输入的参加乘法运算的两个矩阵,第三个参数为得到的乘法结果矩阵。

由于5.5.8节会放出一个使用了不少新函数的示例程序,下面我们先将这些“新面孔”各个击破。

5.5.3 返回DFT最优尺寸大小:getOptimalDFTSize()函数

getOptimalDFTSize函数返回给定向量尺寸的傅里叶最优尺寸大小。为了提高离散傅里叶变换的运行速度,需要扩充图像,而具体扩充多少,就由这个函数来计算得到。

C++: int getOptimalDFTSize(int vecsize);

此函数的唯一一个参数为int类型的vecsize,向量尺寸,即图片的rows、cols。

5.5.4 扩充图像边界:copyMakeBorder()函数

copyMakeBorder函数的作用是扩充图像边界。

//C++:
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+left+right, src.rows+top+bottom)。

● 接下来的4个参数分别为int类型的top、bottom、left、right,分别表示在源图像的四个方向上扩充多少像素,例如top=2,bottom=2,left=2,right=2就意味着在源图像的上下左右各扩充两个像素宽度的边界。

● 第七个参数,borderType类型的,边界类型,常见取值为BORDER_CONSTANT,可参考borderInterpolate()得到更多的细节。

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

5.5.5 计算二维矢量的幅值:magnitude()函数

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

C++:void magnitude(InputArray x, InputArray y, OutputArray magnitude);

● 第一个参数,InputArray类型的x,表示矢量的浮点型X坐标值,也就是实部。

● 第一个参数,InputArray类型的y,表示矢量的浮点型Y坐标值,也就是虚部。

● 第三次参数,OutputArray类型的magnitude,输出的幅值,它和第一个参数x有着同样的尺寸和类型。

下式可以表示magnitude()函数的原理:

5.5.6 计算自然对数:log()函数

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

C++:void log(InputArray src, OutputArray dst);

第一个参数为输入图像,第二个参数为得到的对数值。其原理如下。

5.5.7 矩阵归一化:normalize()函数

normalize()的作用是进行矩阵归一化。

//C++:
void normalize( InputArray src, InputOutputArray dst, double alpha = 1, double beta = 0,
                             int norm_type = NORM_L2, int dtype = -1, InputArray mask = noArray());

● 第一个参数,InputArray类型的src。输入图像,即源图像,填Mat类的对象即可。

● 第二个参数,OutputArray类型的dst。函数调用后的运算结果存在这里,和源图片有一样的尺寸和类型。

● 第三个参数,double类型的alpha。归一化后的最大值,有默认值1。

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

● 第五个参数,int类型的norm_type。归一化类型,有NORM_INF、NORM_L1、NORM_L2和NORM_MINMAX等参数可选,有默认值NORM_L2。

● 第六个参数,int类型的dtype,有默认值-1。当此参数取负值时,输出矩阵和src有同样的类型,否则,它和src有同样的通道数,且此时图像深度为CV_MAT_DEPTH(dtype)。

● 第七个参数,InputArray类型的mask,可选的操作掩膜,有默认值noArray()。

5.5.8 示例程序:离散傅里叶变换

这节中我们将学习一个以dft()函数为核心,对图像求傅里叶变换的,有详细注释的示例程序。

在此示例中,将展示如何计算以及显示傅里叶变换后的幅度图像。由于数字图像的离散性,像素值的取值范围也是有限的。比如在一张灰度图像中,像素灰度值一般在0到255之间。因此,我们这里讨论的也仅仅是离散傅里叶变换(DFT)。如果需要得到图像中的几何结构信息,那么就要用到离散傅里叶变换了。下面的步骤将以输入图像为单通道的灰度图像I为例,进行分步说明。

1.【第一步】载入原始图像

我们在这一步以灰度模式读取原始图像,进行是否读取成功的检测,并显示出读取到的图像。代码如下。

//【1】以灰度模式读取原始图像并显示
	Mat srcImage = imread("1.jpg", 0);
	if(!srcImage.data ) { printf("读取图片错误,请确定目录下是否有imread函数指定图片存在~! \n"); return false; } 
	imshow("原始图像" , srcImage);   

2.【第二步】将图像扩大到合适的尺寸

离散傅里叶变换的运行速度与图片的尺寸有很大关系。当图像的尺寸是2、3、5的整数倍时,计算速度最快。因此,为了达到快速计算的目的,经常通过添凑新的边缘像素的方法获取最佳图像尺寸。函数getOptimalDFTSize()用于返回最佳尺寸,而函数copyMakeBorder()用于填充边缘像素,这一步代码如下。

//【2】将输入图像延扩到最佳的尺寸,边界用0补充
	int m = getOptimalDFTSize( srcImage.rows );
	int n = getOptimalDFTSize( srcImage.cols ); 
	//将添加的像素初始化为0.
	Mat padded;  
	copyMakeBorder(srcImage, padded, 0, m - srcImage.rows, 0, n - srcImage.cols, BORDER_CONSTANT, Scalar::all(0));

3.【第三步】为傅里叶变换的结果(实部和虚部)分配存储空间

傅里叶变换的结果是复数,这就是说对于每个原图像值,结果会有两个图像值。此外,频域值范围远远超过空间值范围,因此至少要将频域储存在float格式中。所以我们将输入图像转换成浮点类型,并多加一个额外通道来储存复数部分。

    //【3】为傅立叶变换的结果(实部和虚部)分配存储空间。
	//将planes数组组合合并成一个多通道的数组complexI
	Mat planes[] = {Mat_<float>(padded), Mat::zeros(padded.size(), CV_32F)};
	Mat complexI;
	merge(planes, 2, complexI);   

4.【第四步】进行离散傅里叶变换

这里的离散傅里叶变换为图像就地计算模式(in-place,输入输出为同一图像)。

//【4】进行就地离散傅里叶变换
dft(complexI, complexI);  

5.【第五步】将复数转换为幅值

复数包含实数部分(Re)和虚数部分(imaginary-Im)。离散傅里叶变换的结果是复数,对应的幅度可以表示为:

那么,转化为OpenCV代码,就是下面这样的。

//【5】将复数转换为幅值,即=> log(1 + sqrt(Re(DFT(I))^2 + Im(DFT(I))^2))
split(complexI, planes); // 将多通道数组complexI分离成几个单通道数组,planes[0] = Re(DFT(I), planes[1] = Im(DFT(I))
magnitude(planes[0], planes[1], planes[0]);// planes[0] = magnitude  
Mat magnitudeImage = planes[0];

6.【第六步】进行对数尺度(logarithmic scale)缩放

傅里叶变换的幅度值范围大到不适合在屏幕上显示。高值在屏幕上显示为白点,而低值为黑点,高低值的变化无法有效分辨。为了在屏幕上凸显出高低变化的连续性,我们可以用对数尺度来替换线性尺度,公式如下。

而写成OpenCV代码,就是下面这样的。

//【6】进行对数尺度(logarithmic scale)缩放
magnitudeImage += Scalar::all(1);
log(magnitudeImage, magnitudeImage);//求自然对数

7.【第七步】剪切和重分布幅度图象限

因为在第二步中延扩了图像,那现在是时候将新添加的像素剔除了。为了方便显示,也可以重新分布幅度图象限位置(注:将第五步得到的幅度图从中间划开,得到4张1/4子图像,将每张子图像看成幅度图的一个象限,重新分布,即将4个角点重叠到图片中心)。这样的话原点(0, 0)就位移到图像中心了。OpenCV代码如下。

//【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()函数将幅度归一化到可显示范围。

//【8】归一化,用0到1之间的浮点值将矩阵变换为可视的图像格式
normalize(magnitudeImage, magnitudeImage, 0, 1, NORM_MINMAX);

9.【第九步】显示效果图

处理完成,最后进行显示即可。

//【9】显示效果图
imshow("频谱幅值", magnitudeImage); 

将上述代码组合到一起,便得到了程序的完整源代码。


/*	@File				: 28_DFT.cpp
 *  @Brief				: 示例程序28
 *  @Details			: 离散傅里叶变换
 *  @Date				: 2015-11-01
 *  @OpenCV Version		: 4.8.0
 *  @Development Tools	: Windows 11 64bit && Visual Studio 2017
 *  @Modify				: 2024-03-31
*/

//---------------------------------【头文件、命名空间包含部分】-----------------------------
//		描述:包含程序所使用的头文件和命名空间
//-------------------------------------------------------------------------------------------------
#include "opencv2/core/core.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <iostream>
using namespace cv;


//-----------------------------------【ShowHelpText( )函数】----------------------------------
//		 描述:输出一些帮助信息
//----------------------------------------------------------------------------------------------
void ShowHelpText()
{
	//输出欢迎信息和OpenCV版本
	printf("\n\n\t\t\t此为本书OpenCV3版的第28个配套示例程序\n");
	printf("\n\n\t\t\t   当前使用的OpenCV版本为:" CV_VERSION );
	printf("\n\n  ----------------------------------------------------------------------------\n");
}



//--------------------------------------【main( )函数】-----------------------------------------
//          描述:控制台应用程序的入口函数,我们的程序从这里开始执行
//-------------------------------------------------------------------------------------------------
int main( )
{

	//【1】以灰度模式读取原始图像并显示
	Mat srcImage = imread("1.jpg", 0);
	if(!srcImage.data ) { printf("读取图片错误,请确定目录下是否有imread函数指定图片存在~! \n"); return false; } 
	imshow("原始图像" , srcImage);   

	ShowHelpText();

	//【2】将输入图像延扩到最佳的尺寸,边界用0补充
	int m = getOptimalDFTSize( srcImage.rows );
	int n = getOptimalDFTSize( srcImage.cols ); 
	//将添加的像素初始化为0.
	Mat padded;  
	copyMakeBorder(srcImage, padded, 0, m - srcImage.rows, 0, n - srcImage.cols, BORDER_CONSTANT, Scalar::all(0));

	//【3】为傅立叶变换的结果(实部和虚部)分配存储空间。
	//将planes数组组合合并成一个多通道的数组complexI
	Mat 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))
	split(complexI, planes); // 将多通道数组complexI分离成几个单通道数组,planes[0] = Re(DFT(I), planes[1] = Im(DFT(I))
	magnitude(planes[0], planes[1], planes[0]);// planes[0] = magnitude  
	Mat 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之间的浮点值将矩阵变换为可视的图像格式
	//此句代码的OpenCV2版为:
	//normalize(magnitudeImage, magnitudeImage, 0, 1, CV_MINMAX); 
	//此句代码的OpenCV3版为:
	normalize(magnitudeImage, magnitudeImage, 0, 1, NORM_MINMAX); 

	//【9】显示效果图
	imshow("频谱幅值", magnitudeImage);    
	waitKey();

	return 0;
}

程序注释已经足够详尽,其中新出现的函数都在上文中有过介绍,所以在这里就不花篇幅进一步展开讲解了。让我们一起看一下运行截图(图5.15、图5.16),以结束本节的学习。

                                                                        图5.15 原始图

                                         图5.16 离散傅里叶变换效果图

5.6 输入输出XML和YAML文件

5.6.1 XML和YAML文件简介

本节我们将一起认识XML和YAML这两种文件类型。

所谓XML,即eXtensible Markup Language,翻译成中文为“可扩展标识语言”。首先,XML是一种元标记语言。所谓“元标记”,就是开发者可以根据自身需要定义自己的标记,比如可以定义标记<book>、<name>。任何满足XML命名规则的名称都可以标记,这就向不同的应用程序打开了的大门。此外,XML是一种语义/结构化语言,它描述了文档的结构和语义。

YAML是“YAML Ain't a Markup Language”(译为“YAML不是一种置标语言”)的递回缩写。在开发的这种语言时,YAML的原意是:“Yet Another Markup Language”(仍是一种置标语言),但为了强调这种语言以数据为中心,而不是以置标语言为重点,而用返璞词进行重新命名。YAML是一个可读性高,用来表达资料序列的格式。它参考了其他多种语言,包括:XML、C语言、Python、Perl,以及电子邮件格式RFC2822。

.yml和.yaml同为YAML格式的后缀名

总之,YAML试图用一种比XML更敏捷的方式,来完成XML所完成的任务。

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

XML和YAML是使用非常广泛的文件格式,可以利用XML或者YAML格式的文件存储和还原各式各样的数据结构。当然,它们还可以存储和载入任意复杂的数据结构,其中就包括了OpenCV相关周边的数据结构,以及各种原始数据类型,如整数和浮点数字和文本字符串。

我们一般使用如下过程来写入或者读取数据到XML或YAML文件中。

(1)实例化一个FileStorage类的对象,用默认带参数的构造函数完成初始化,或者用FileStorage::open()成员函数辅助初始化。

(2)使用流操作符<<进行文件写入操作,或者>>进行文件读取操作,类似C++中的文件输入输出流。

(3)使用FileStorage::release()函数析构掉FileStorage类对象,同时关闭文件。

下面分别对这三个步骤进行实例讲解。

1.【第一步】XML、YAML文件的打开

(1)准备文件写操作

FileStorage是OpenCV中XML和YAML文件的存储类,封装了所有相关的信息。它是OpenCV从文件中读数据或向文件中写数据时必须要使用的一个类。

此类的构造函数为FileStorage::FileStorage,有两个重载,如下。

C++:  CV_WRAP FileStorage();
C++:  CV_WRAP FileStorage(const String& filename, int flags, const String& encoding=String());

构造函数在实际使用中,方法一般有两种。

1)对于第二种带参数的构造函数,进行写操作范例如下。

FileStorage fs("test.yaml", FileStorage::WRITE);  

2)对于第一种不带参数的构造函数,可以使用其成员函数FileStorage::open进行数据的写操作,范例如下。

FileStorage fs;
fs.open("test.yaml", FileStorage::WRITE);  

(2)准备文件读操作

上面讲到的都是以FileStorage::WRITE为标识符的写操作,而读操作,采用FileStorage::READ标识符即可,相关示例代码如下。

1)第一种方式

FileStorage fs("abc.yaml", FileStorage::READ);  

2)第二种方式 

FileStorage fs;
fs.open("abc.yaml", FileStorage::READ);  

另外需要注意的是,上面的这些操作示例是对XML文件为例子作演示的,而对YAML文件,操作方法是类似的,就是将XML文件换为YAML文件即可。

2.【第二步】进行文件读写操作

(1)文本和数字的输入和输出

定义好FileStorage类对象之后,写入文件可以使用“<<”运算符,例如:

fs << "iterationNr" << 100;  

而读取文件,使用“>>”运算符,例如:

(2)OpenCV数据结构的输入和输出

关于OpenCV数据结构的输入和输出,和基本的C++形式相同,范例如下。

3.【第三步】vector(arrays)和maps的输入和输出对于vector结构的输入和输出,要注意在第一个元素前加上“[”,在最后一个元素前加上“]”。例如:

而对于map结构的操作,使用的符号是“{”和“}”,例如:

读取这些结构的时候,会用到FileNode和FileNodeIterator数据结构。对FileStorage类的“[”、“]”操作符会返回FileNode数据类型;对于一连串的node,可以使用FileNodeIterator结构,例如:

4.【第四步】文件关闭

需要注意的是,文件关闭操作会在FileStorage类销毁时自动进行,但我们也可显式调用其析构函数FileStorage::release()实现。FileStorage::release()函数会析构掉FileStorage类对象,同时关闭文件。

调用过程非常简单,如下。

fs2.release(); 

下面将通过实例来帮助大家将以上知识融会贯通。

5.6.3 示例程序:XML和YAML文件的写入

让我们先看一个关于XML或YAML文件的写入实例,示例代码如下:


/*	@File				: 29_Write_XML_and_YAML_File.cpp
 *  @Brief				: 示例程序29
 *  @Details			: XML和YAML文件的写入
 *  @Date				: 2015-11-01
 *  @OpenCV Version		: 4.8.0
 *  @Development Tools	: Windows 11 64bit && Visual Studio 2017
 *  @Modify				: 2024-03-31
*/

//---------------------------------【头文件、命名空间包含部分】-------------------------------
//		描述:包含程序所使用的头文件和命名空间
//------------------------------------------------------------------------------------------------
#include "opencv2/opencv.hpp"  
#include <time.h>  
#include <stdio.h>
using namespace cv;  


//-----------------------------------【ShowHelpText( )函数】----------------------------------
//		 描述:输出一些帮助信息
//----------------------------------------------------------------------------------------------
void ShowHelpText()
{
	//输出欢迎信息和OpenCV版本
	printf("\n\n\t\t\t非常感谢购买《OpenCV3编程入门》一书!\n");
	printf("\n\n\t\t\t此为本书OpenCV3版的第29个配套示例程序\n");
	printf("\n\n\t\t\t   当前使用的OpenCV版本为:" CV_VERSION );
	printf("\n\n  ----------------------------------------------------------------------------\n");
}


//-----------------------------------【main( )函数】--------------------------------------------
//	描述:控制台应用程序的入口函数,我们的程序从这里开始
//-----------------------------------------------------------------------------------------------
struct tm newtime;
__time32_t aclock;

int main( )  
{  
	//改变console字体颜色
	system("color 5F"); 

	ShowHelpText();

	//初始化
	FileStorage fs("test.yaml", FileStorage::WRITE);  

	//开始文件写入
	fs << "frameCount" << 5;  
	time_t rawtime; time(&rawtime);  
	//fs << "calibrationDate" << asctime(localtime(&rawtime));  

	char buffer[32];
	errno_t errNum;
	_time32(&aclock);   // Get time in seconds.
	_localtime32_s(&newtime, &aclock);   // Convert time to struct tm form.

	// Print local time as a string.
	errNum = asctime_s(buffer, 32, &newtime);
	fs << "calibrationDate" << buffer;


	Mat cameraMatrix = (Mat_<double>(3,3) << 1000, 0, 320, 0, 1000, 240, 0, 0, 1);  
	Mat distCoeffs = (Mat_<double>(5,1) << 0.1, 0.01, -0.001, 0, 0);  
	fs << "cameraMatrix" << cameraMatrix << "distCoeffs" << distCoeffs;  
	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("\n文件读写完毕,请在工程目录下查看生成的文件~");
	getchar();

	return 0;  
}  

运行此程序,会在工程目录下程序一个名为“test.yaml”的文件,用记事本或者其他文本编辑器比如notepad++打开它,可以发现写入的YAML文档内容如下。

如果我们修改上面代码的FileStorage fs(“test.yaml”, FileStorage: : WRITE)这一句代码,将其中的“test.yaml”的后缀换为xml、yml、txt甚至doc,都是可以得到运行结果的,大家不妨尝试一下。

5.6.4 示例程序:XML和YAML文件的读取

接着,我们一起看读操作应该如何进行,完整的源代码如下。


/*	@File				: 30_Read_XML_and_YAML_File.cpp
 *  @Brief				: 示例程序30
 *  @Details			: XML和YAML文件的读取
 *  @Date				: 2015-11-01
 *  @OpenCV Version		: 4.8.0
 *  @Development Tools	: Windows 11 64bit && Visual Studio 2017
 *  @Modify				: 2024-03-31
*/

//---------------------------------【头文件、命名空间包含部分】-------------------------------
//		描述:包含程序所使用的头文件和命名空间
//------------------------------------------------------------------------------------------------       
#include "opencv2/opencv.hpp"  
#include <time.h>  
using namespace cv;  
using namespace std;  


//-----------------------------------【ShowHelpText( )函数】----------------------------------
//		 描述:输出一些帮助信息
//----------------------------------------------------------------------------------------------
void ShowHelpText()
{
	//输出欢迎信息和OpenCV版本
	printf("\n\n\t\t\t此为本书OpenCV3版的第30个配套示例程序\n");
	printf("\n\n\t\t\t   当前使用的OpenCV版本为:" CV_VERSION );
	printf("\n\n  ----------------------------------------------------------------------------\n\n");
}




int main( )  
{  
	//改变console字体颜色
	system("color 6F"); 

	ShowHelpText();

	//初始化
	FileStorage fs2("test.yaml", FileStorage::READ);  

	// 第一种方法,对FileNode操作
	int frameCount = (int)fs2["frameCount"];  

	std::string date;  
	// 第二种方法,使用FileNode运算符> > 
	fs2["calibrationDate"] >> date;  

	Mat cameraMatrix2, distCoeffs2;  
	fs2["cameraMatrix"] >> cameraMatrix2;  
	fs2["distCoeffs"] >> distCoeffs2;  

	cout << "frameCount: " << frameCount << endl  
		<< "calibration date: " << date << endl  
		<< "camera matrix: " << cameraMatrix2 << 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();

	return 0;  
}  

此程序需要工程目录下有指定的文件存在,如我们将前文刚刚生成的test.yaml复制到工程目录下,运行此程序,便可以得出正确的结果。运行截图如图5.17所示。

                                               图5.17 运行输出截图

5.7 本章小结

本节中,我们学习了core模块的一些进阶知识点——操作图像中的像素、图像混合、分离颜色通道、调节图像的对比度和亮度、进行离散傅里叶变换,以及输入输出XML和YAML文件。

本章核心函数清单

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值