数字图像在计算机中是以矩阵的形式存储的。
在c++中,字符串在程序中以string类型保存,整数以int的类型保存,同样OpenCV提供了一个Mat类来保存矩阵数据。
OpenCV引入了C++的接口,提供Mat类用来存储数据,同时利用自动内存管理技术很好的解决了内存自动释放的问题(好像是用的shared_ptr?有懂的大哥欢迎指正交流),当变量不再需要时,立即释放内存。
Mat类分为矩阵头和指向存储数据的矩阵指针两部分。矩阵头中包含了矩阵的尺寸、存储方法、地址和引用次数等。矩阵头的大小是一个常数,不会随着矩阵尺寸的大小而改变(类似指针的意思?)
在绝大多数情况下,矩阵头的大小远远小于矩阵中数据量的大小,因此图像复制和传递的过程中主要的开销是存放矩阵数据。
为了解决存储的问题,在OpenCV中复制和传递图像时,只是复制了矩阵头和指向存储数据的指针,因此创建Mat类时,可以先创建矩阵头后赋值数据:
//创建Mat类
cv::Mat a; //创建一个名为a的矩阵头
a = cv::imread("test.jpg"); //向a中赋值图像数据,矩阵指针指向像素数据
cv::Mat b = a; //复制矩阵头,并命名为b(我个人理解为多引用了一次,触发了矩阵头中的shared_ptr引用次数的增加)
该代码首先创建了一个名为a的矩阵头,然后读入一张图像并将a中的矩阵指针指向该图像的像素数据,最后将a矩阵头中的内容复制到b矩阵头中。通过任意一个矩阵头修改图像数据,另一个矩阵头指向的数据也会跟着改变。但是删除a变量时,b变量并不会指向一个空数据,只有当两个变量都删除时,才会释放矩阵数据。
Mat类可以存储的数据有double,float,uchar,unsigned char,以及自定义的模板。
我们可以通过以下代码来声明一个存放指定类型的Mat类变量:
//声明一个指定类型的Mat类
cv::Mat A = Mat_<double>(3,3); //创建一个3*3的矩阵用于存放double数据
我们了解了可以定义的数据类型后,还需要定义图像数据的通道数(channel),例如灰度图像是单通道数据,彩色图像是3通道或者4通道数据。OpenCV还定义了通道数标识,C1,C2,C3,C4分别表示单通道,双通道,3通道和4通道。例如,CV_8U表示的是8位无符号整数,取值范围为0-255,CV_8UC3表示的则是三通道的彩色图像,CV_8UC1表示的是单通道的灰度图像。代码如下:
//通过OpenCV数据类型创建Mat类
cv::Mat a (640,480,CV_8UC3) // 创建一个640*480的3通道矩阵用于存放彩色图像
cv::Mat a (3, 3, CV_8UC1) // 创建一个3*3的8位无符号整数的单通道矩阵
cv::Mat a (3, 3, CV_8U) // 创建一个3*3的单通道矩阵,c1标识符可以省略
OpenCV提供imwrite()函数用于将Mat类矩阵保存为图像文件,该函数的原型代码为:
bool cv::imwrite(
const String& filename, 图像保存到本地的文件名
InputArray img, imwrite接收的mat矩阵变量,即我们需要保存的mat类矩阵
Const std::vector<int> & params = std::vector<int>() 保存图像的格式以及相关的参数
)
imwrite函数的返回值是bool类型,当保存成功时返回true, 保存失败时返回false。
其中,imwrite函数的第三个参数的设置方式如下:
imwrite()函数中第三个参数的设置方式:
vector<int> compression_params;
compression_params.push_back(IMWRITE_PNG_COMPRESSION); 保存为PNG格式的图像,图像质量0 - 9,值越大表示图像的尺寸越小,压缩时间越长,默认值为1
compression_params.push_back(9);
下面用一段简单的代码来示范一下imwrite函数的具体实现方式:
#include
#include
#include
#include
using namespace std;
using namespace cv;
//bool cv::imwrite(
// const String& filename, 图像保存到本地的文件名
// InputArray img, imwrite接收的mat矩阵变量,即我们需要保存的mat类矩阵
// Const std::vector & params = std::vector() 保存图像的格式以及相关的参数
//)
//imwrite函数的返回值是bool类型,当保存成功时返回true, 保存失败时返回false。
//imwrite()函数中第三个参数的设置方式:
//vector compression_params;
//compression_params.push_back(IMWRITE_PNG_COMPRESSION); 保存为PNG格式的图像,图像质量0 - 9,值越大表示图像的尺寸越小,压缩时间越长,默认值为1
//compression_params.push_back(9); 设置的压缩参数为9
void AlphaMat(Mat & mat)
{
CV_Assert(mat.channels() == 4);
for (int i = 0; i < mat.rows; i++)
{
for (int j = 0; j < mat.cols; j++)
{
Vec4b& bgra = mat.at<Vec4b>(i, j);
bgra[0] = UCHAR_MAX; //蓝色通道
bgra[1] = saturate_cast<uchar>((float(mat.cols - j)) / ((float)mat.cols) * UCHAR_MAX); //绿色通道 saturate_cast函数是为了防止对像素操作时的颜色溢出
bgra[2] = saturate_cast<uchar>((float(mat.cols - i)) / ((float)mat.rows) * UCHAR_MAX); //红色通道
bgra[3] = saturate_cast<uchar>(0.5 * (bgra[1] + bgra[2])); //alpha通道
}
}
}
int main()
{
//create mat with alpha channel
Mat mat(480, 640, CV_8UC4); //使用Mat类生成一个mat对象,矩阵的尺寸为480 * 640, 并且是四通道的矩阵形式
AlphaMat(mat); //将实例化的mat作为变量传入自定义的 AlphaMat方法中
vector<int> compression_params;//设置imwrite函数中的第三个参数,这里我们将图片保存为PNG格式,图像压缩质量为9,如果想设置成jpg或者其他格式,可以在官网中检索此API
compression_params.push_back(IMWRITE_PNG_COMPRESSION);
compression_params.push_back(9);
bool result = imwrite("alpha.png", mat, compression_params); //接收imwrite函数的返回值作为下面输出的判断依据
if (!result)
{
cout << "保存PNG图像失败" << endl;
return -1;
}
else
{
cout << "保存PNG图像成功" << endl;
return 0;
}
}
我们在工作和学习中,有时需要将多幅图像生成视频或者直接将摄像头读取的数据保存成视频文件。OpenCV中提供了VideoWriter()类来实现多张图像保存成视频文件的功能,该类的构造函数如下:
VideoWriter的构造函数
cv::VideoWriter::VideoWriter(); //默认构造函数,后续通过open()函数来设置保存文件的名称、编解码器、帧数等一系列参数。
cv::VideoWriter::VideoWriter( //直接构造,在实例化视频流的同时,直接设置参数。
const String & filename, //保存的文件名
int fourcc, //设置编解码器,参数设置可以在官网查询
double fps, //设置保存视频的帧率
Size framesize, //设置保存的视频的尺寸
bool isColor = true //保存的视频是否为彩色视频
)
通过一段代码来看看如何通过调用摄像头并保存摄像头记录的视频数据:
#include
#include
using namespace std;
using namespace cv;
//VideoWriter的构造函数
//cv::VideoWriter::VideoWriter(); //默认构造函数,后续通过open()函数来设置保存文件的名称、编解码器、帧数等一系列参数。
//cv::VideoWriter::VideoWriter( //直接构造,在实例化视频流的同时,直接设置参数。
// const String & filename, //保存的文件名
// int fourcc, //设置编解码器,参数设置可以在官网查询
// double fps, //设置保存视频的帧率
// Size framesize, //设置保存的视频的尺寸
// bool isColor = true //保存的视频是否为彩色视频
//)
int main()
{
Mat img;
VideoCapture video(0); //使用某个摄像头
//VideoCapture video; //读取某一个视频文件
//video.open("xxx.mp4");
if (!video.isOpened())
{
cout << "打开摄像头失败,请确认摄像头是否安装成功" << endl;
return -1;
}
video >> img; //获取图像
//判断是否成功获取图像
if (img.empty()) //判断读取图形是否成功
{
cout << "没有获取到图像" << endl;
return -1;
}
bool isColor = (img.type() == CV_8UC3); //判断相机(视频)类型是否为彩色
VideoWriter writer; //实例化保存视频的视频流对象,这一点与之前的保存图像数据不同
int codec = VideoWriter::fourcc('M', 'J', 'P', 'G'); //选择编码格式
double fps = 25.0; //设置视频帧率
string filename = "live.avi"; //保存的视频文件名称
writer.open(filename, codec, fps, img.size(), isColor); //创建保存视频文件的视频流
if (!writer.isOpened()) //判断视频流是否创建成功
{
cout << "打开视频文件失败,请确认是否为合法输入" << endl;
return -1;
}
while (1)
{
//检查是否执行完成
if (!video.read(img)) //判断能否继续从摄像头或者视频文件中读取下一帧图像
{
cout << "摄像头断开连接或者视频读取完成" << endl;
break;
}
writer.write(img); //把图像写入视频流
//writer << img;
imshow("Live", img); //显示图像
char c = waitKey(50);
if (c == 27)
{
break;
}
}
video.release(); //退出程序时关闭视频流
writer.release();
return 0;
}
运行之后可以看到我们调用了电脑的摄像头,此时已经开始不断的存储摄像头读取的数据了:
如果我们想退出读取视频,可以摁两下esc键,退出读取。这时我们打开我们项目所在的路径中我们就能发现,刚才我们所读取的视频文件啦!
在学习如何在OpenCV中操作xml文件和yaml文件之前,我们先来了解一下什么是xml文件和yaml文件。
在保存数据时,除了图像数据这类较大的数据文件之外,通常我们也会保存一些尺寸较小的Mat类矩阵,字符串、数组等数据,这些数据通常保存为xml文件或者yaml文件。
xml文件是一种元标记语言。所谓元标记语言就是使用者可以根据自身需求自定义自己的标记:
<age>24</age> 来表示age的数值为24
<color> 来表示color数据中含有两个名为red和blue的数据,两者的数值分别是100和150
<red>100</red>
<blue>150</blue>
</color>
通过标记的方式,无论以什么形式保存数据,只要文件满足XML格式,取出来的数据就不会有歧义和混淆。XML文件的扩展名为“.xml”。
OpenCV4中提供了用于生成和读取XML文件和YAML文件的FileStorage类,类中定义了初始化类、写入数据和读取数据等方法。我们在使用FileStorage类时需要首先对其进行初始化,初始化可以理解为声明需要操作的文件和操作类型。OpenCV4提供了两种初始化的方法,分别是不输入任何参数的初始化,即先实例化一个对象,然后用FileStorage中的open方法来初始化。或者我们可以用他的构造函数的方式来初始化:
FileStorage构造函数原型:
cv::FileStorage::FileStorage(
const String & filename, //打开的文件名称
int flags, //对文件进行的操作类型标志
const String & encoding = String() //编码格式
)
该函数是FileStorage的构造函数,用于声明打开的文件名称和操作的类型。
打开文件后,可以通过FileStorage类中的isOpened()函数来判断是否成功打开文件。如果成功打开文件,就返回true,如果没有成功打开文件就返回false。
打开文件后,类似c++中的创建数据流,可以通过<<操作符将数据写入文件中,通过>>操作符从文件中读取数据。除此之外,我们还可以用FileStorage类中的write()函数将数据写入文件中,代码如下:
void cv::FileStorage::write(
const String & name, //写入文件中的变量名称
int val //变量的值
)
使用操作符向文件中写入数据时,同样需要声明变量名和变量值,例如变量名为“age”,变量值为“24”,可以通过“file<<“age”<<24”来实现。
file<<"age"<<24;
file<<"age"<<"["<<24<<25<<"]";
file<<"age"<<"{"<<"xiaoming"<<24<<"wanghua"<<25<<"}"; //表示从属关系
//读取变量
最常见的是:
file["x"] >> xRead; 读取变量名为x的变量值
如果某个变量中含有多个数据或者含有子变量时,需要通过FileNode节点类型和迭代器FileNodeIterator进行读取:
例如,写入一个数组数据到file中:
file<<"age"<<"["<<24<<25<<"]";
读取file中的age变量名的第2个变量值:
FileNode fn = file["age"];
cout << fn[1];
例如,写入一个含有子变量的变量进入file中:
file<<"age"<<"{"<<"xiaoming"<<24<<"wanghua"<<25<<"}"; //表示从属关系
读取file中的age变量名下的xiaoming子变量的变量值:
FileNode fn = file["age"];
cout<< fn["xiaoming"];
更具体的,我们通过一段完整的代码来演示如何读取和写入xml和yaml文件。该程序中使用了write函数和操作符<<两种方式向文件中写入数据,使用迭代器和[](地址)两种方式从文件中读取数据:
#include
#include
#include
using namespace std;
using namespace cv;
//FileStorage构造函数原型:
//cv::FileStorage::FileStorage(
// const String & filename, //打开的文件名称
// int flags, //对文件进行的操作类型标志
// const String & encoding = String() //编码格式
//)
int main()
{
system(" "); //修改运行程序背景和文字颜色
string filename = "datas.yaml"; //自定义文件的名称
//string filename = "datas.xml";
FileStorage fwrite(filename, FileStorage::WRITE); //以写入的模式打开文件
Mat mat = Mat::eye(3, 3, CV_8U); //创建一个单通道的mat矩阵来存放数据
fwrite.write("mat", mat); //使用write函数来写入数据
float x = 100; //存入浮点型数据,节点名称为x
fwrite << "x" << x; //写入
String str = "Learn opencv 4"; //存入字符串型数据,节点名称为str
fwrite << "str" << str;
fwrite << "number_array" << "[" << 4 << 5 << 6 << "]"; //存入数组,节点名称为number_array
//存入多node节点,主节点名称为multi_nodes
fwrite << "multi_nodes" << "{" << "month" << 8 << "day" << 28 << "year" << 2019 << "time" << "[" << 0 << 1 << 2 << 3 << "]" << "}";
fwrite.release(); //关闭文件
//==========================================================================================
//以读取的模式打开文件
FileStorage fread(filename, FileStorage::READ);
if (!fread.isOpened()) //判断是否打开文件
{
cout << "文件打开失败,请确认文件名称是否正确" << endl;
return -1;
}
float xRead;
fread["x"] >> xRead; //读取浮点型数据
cout << "x=" << xRead << endl;
string strRead; //读取字符串数据
fread["str"] >> strRead;
cout << "str=" << strRead << endl;
FileNode filenode = fread["number_array"]; //读取含多个数据的number_array节点
cout << "number_array=" << "[";
for (FileNodeIterator i = filenode.begin(); i != filenode.end(); i++) //循环遍历每个数据
{
float a;
*i >> a;
cout << a << " ";
}
cout << "]" << endl;
Mat matRead; //读取mat类型数据
fread["mat="] >> matRead; //这里读取的时候记得带上=号
cout << "mat=" << mat << endl;
FileNode filenode_1 = fread["multi_nodes"];
int month = (int)filenode_1["month"];
int day = (int)filenode_1["day"];
int year = (int)filenode_1["year"];
cout << "multi_nodes=" << endl;
cout << " month=" << month << " day=" << day << " year=" << year;
cout << " time=[";
for (int i = 0; i < 4; i++)
{
int a = (int)filenode_1["time"][i];
cout << a << " ";
}
cout << "]" << endl;
fread.release();
return 0;
}
下面是代码运行后的结果:
以及代码生成的yaml文件(这里我用的pycharm打开的):
到这里为止,在OpenCV中的数据保存告一个段落了,本人水平有限,若文中出现错误,希望看官大佬们能够谅解我的才疏学浅,并评论帮忙指正,万分感谢~