序列化——XML和YAML文件处理
- 序列化和反序列化
- 代码实现
- XML/YAML文件的打开和关闭
- 写入或读取文本和数字
- 写入或读取OpenCV数据
- 写入或读取数组以及map
- 读取和写入自定义数据类型
- 输出结果
序列化和反序列化
如果希望永久保存某些对象,而不是每次运行程序的时候重新创建,就需要将对象以字节序列的形式保存起来,这就是序列化。反之,通过字节序列得到储存在其中的原对象就是反序列化。序列化和反序列常用在以下场景中:
- 将对象保存到一个文件或数据库中
- 在网络上传送对象
本文将介绍在OpenCV中如何将对象序列化,并保存到XML或YAML文件,然后从这些文件中读取它。
代码实现
XML/YAML文件的打开和关闭
OpenCV中对应XML/YAML数据结构的类是cv::FileStorage
。可以用open()
函数来打开文件,也可以在构造函数中创建对象:
FileStorage fs(filename, FileStorage::WRITE); //构造函数
//或者
//FileStorage fs;
//fs.open(filename, FileStorage::WRITE);
不管哪种方法,第2个参数都得要指定打开的方式:FileStorage::WRITE
(写)、FileStorage::READ
(读)或FileStorage::APPEND
(追加)。文件名中的后缀名决定了写入或读取的格式。甚至可以通过指定后缀名为".xml.gz"来压缩文件。
当cv::FileStorage
对象离开作用域的时候文件会自动关闭,也可以显式关闭文件:
fs.release();
写入或读取文本和数字
在C++中,使用运算符<<
来向cv::FileStorage
对象中写入内容。写入任何类型的数据都先要指定变量名称:
fs << "iterationNr" << 100;
从cv::FileStorage
对象中读取基本类型的变量,需要在[]
操作符中指定变量名。然后,可以用>>
运算符或者类型转换来将数据读取到变量中:
int itNr;
//fs["iterationNr"] >> itNr; //>>运算符
itNr = static_cast<int>(fs["iterationNr"]);
写入或读取OpenCV数据
Mat
对象的写入和读取与文本或数字变量相同,也需要先指定变量名:
Mat R{ mat_<uchar>::eye(3,3) },T{ Mat_<double>::zeros(3,1) };
//写入
fs << "R" << R;
fs << "T" << T;
//读取
fs["R"] >> R;
fs["T"] >> T;
写入或读取数组以及map
map和数组的区别是,在map数据中每个元素都有一个特定的名称用来获取这个元素,而在数组中则需要按顺序来获取元素。
写入数组时,需要在开头和结尾分别写入"[“和”]":
fs << "strings" << "["; //字符串数组
fs << "image1.jpg" << "Awesomeness" << "../data/babboon.jpg";
fs << "]"; //关闭数组
写入map数据时,需要在开头和结尾分别写入"{“和”}";先输入元素名称,再输入元素数据:
fs << "Mapping";
fs << "{" << "One" << 1;
fs << "Two" << 2 << "}"';
读取map和数组数据的时候需要用到cv::FileNode
和cv::FileNodeIterator
。使用[]
操作符后,cv::FileStorage
对象返回一个cv::FileNode
类型的节点数据。如果这个节点是个数组,则可以使用cv::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 << static_cast<string>(*it) << endl;
如果节点是map数据,则使用<<
运算符读取即可:
n = fs["Mapping"];
cout << "Two " << static_cast<int>(n["Two"]) << "; ";
cout << "One " << static_cast<int>(n["One"]) << endl << endl;
读取和写入自定义数据类型
如果有一个自定义的类:
class MyData
{
public:MyData() : A(0), X(0), id() {}explicit MyData(int) : A(97), X(CV_PI), id("mydata1234") {}//自定义读写函数void write(FileStorage& fs) const {fs << "{" << "A" << A << "X" << X << "id" << id << "}";}void read(const FileNode& node) {A = static_cast<int>(node["A"]);X = static_cast<double>(node["X"]);id = static_cast<string>(node["id"]);}public:int A;double X;string id;
};
注意:需要在自定的类里编写自定义的读写函数,以方便以下的静态函数调用:
static void write(FileStorage& fs, const std::string&, const MyData& x) {x.write(fs);
}static void read(const FileNode& node, MyData& x, const MyData& default_value = MyData()) {if (node.empty())x = default_value;elsex.read(node);
}
这两个静态函数是对OpenCV库中persistence.hpp定义的同名静态函数的重载,以实现对自定义的数据类型的读写。
注意:在重载的read
函数中,我们还实现了对空对象的读取,即读取为默认的MyData
对象
重载了这两个函数之后就可以直接使用>>
运算符来写入自定义类的对象,因为在persistence.hpp中已经定义了该运算符的重载。
但是<<
运算符还必须自定义重载,以读取自定义类的对象:
static ostream& operator<<(ostream& out, const MyData& m) {out << "{ id = " << m.id << ", ";out << "X = " << m.X << ", ";out << "A = " << m.A << "}";return out;
}
接下来,就可以像读写其他基本数据类型的变量一样,读写自定义类的对象了:
MyData m{ 1 };
fs << "MyData" << m; //写入自定义MyData对象fs["MyData"] >> m; //读取自定义MyData对象
cout << "MyData = " << endl << m << endl << endl;
输出结果
完整代码如下:
#include <opencv2/core.hpp>
//#include <opencv2/imgcodecs.hpp>
//#include <opencv2/highgui.hpp>import <iostream>;
import <string>;using namespace cv;
using namespace std;static void help(char** av) {cout << endl<< av[0] << "展示了OpenCV中序列化函数的用法。" << endl<< "使用说明:" << endl<< "输出文件要么是XML(xml)或YAML(yml/yaml)类型的。" << endl<< "还可以通过指定文件后缀名,比如xml.gz或yaml.gz等来压缩输出文件。" << endl<< "在FileStorage模块中可以使用<<和>>运算符来序列化对象。" << endl<< "例如:- 创建一个类并将其序列化;" << endl<< " - 读取或写入矩阵。" << endl;
}class MyData
{
public:MyData() : A(0), X(0), id() {}explicit MyData(int) : A(97), X(CV_PI), id("mydata1234") {}void write(FileStorage& fs) const {fs << "{" << "A" << A << "X" << X << "id" << id << "}";}void read(const FileNode& node) {A = static_cast<int>(node["A"]);X = static_cast<double>(node["X"]);id = static_cast<string>(node["id"]);}public:int A;double X;string id;
};static void write(FileStorage& fs, const std::string&, const MyData& x) {x.write(fs);
}static void read(const FileNode& node, MyData& x, const MyData& default_value = MyData()) {if (node.empty())x = default_value;elsex.read(node);
}static ostream& operator<<(ostream& out, const MyData& m) {out << "{ id = " << m.id << ", ";out << "X = " << m.X << ", ";out << "A = " << m.A << "}";return out;
}int main(int ac, char** av) {if (ac != 2) {help(av);return 1;}string filename = av[1];{//写Mat R{ Mat_<uchar>::eye(3, 3) },T{ Mat_<double>::zeros(3, 1) };MyData m{ 1 };FileStorage fs(filename, FileStorage::WRITE);//也可以://FileStorage fs;//fs.open(filename, FileStorage::WRITE);fs << "iterationNr" << 100;fs << "strings" << "[";fs << "image1.jpg" << "Awesomeness" << "../data/baboon.jpg";fs << "]";fs << "Mapping";fs << "{" << "One" << 1;fs << "Two" << 2 << "}";fs << "R" << R;fs << "T" << T;fs << "MyData" << m;fs.release();cout << "完成写入。" << endl;}{cout << endl << "读取:" << endl;FileStorage fs;fs.open(filename, FileStorage::READ);int itNr;//fs["iterationNr"] >> itNr;itNr = static_cast<int>(fs["iterationNr"]);cout << itNr << endl;if (!fs.isOpened()) {cerr << filename << "打开失败" << endl;help(av);return 1;}FileNode n{ fs["strings"] };if (n.type() != FileNode::SEQ) {cerr << "字符串不是一个序列!读取失败" << endl;cout << n.type() << endl;return 1;}FileNodeIterator it{ n.begin() }, it_end{ n.end() };for (; it != it_end; ++it)cout << static_cast<string>(*it) << endl;n = fs["Mapping"];cout << "Two " << static_cast<int>(n["Two"]) << "; ";cout << "One " << static_cast<int>(n["One"]) << endl << endl;MyData m;Mat R, T;fs["R"] >> R;fs["T"] >> T;fs["MyData"] >> m;cout << endl<< "R = " << R << endl;cout << "T = " << T << endl << endl;cout << "MyData = " << endl << m << endl << endl;cout << "试图读取 NonExisting (会用默认值对数据结构进行初始化)。";fs["NoExisting"] >> m;cout << endl << "NonExisting = " << endl << m << endl;}cout << endl<< "提示:在文本编辑器中打开" << filename << "以查看序列化数据。" << endl;return 0;
}
输出结果如下:
我们也可以打开XML文件,查看序列化后的储存形式: