opencv2.2中使用FileStorage存取自定义类数据时的问题

来源:互联网 发布:c语言的memcpy 编辑:程序博客网 时间:2024/05/29 19:02

//使用FileStorage的C++格式存储自定义类时,调试了3天半,发现的问题,以官方网站例子为例,进行了修剪
//原始代码见:
https://code.ros.org/svn/opencv/trunk/opencv/samples/cpp/filestorage.cpp

 

#include "opencv2/core/core.hpp"
#include <iostream>
#include <string>
using namespace cv;
using namespace std;

struct MyData
{
  MyData() :A(0), X(0), id()  {  } //-----------------------------------(A)
  explicit MyData(int):A(97),X(CV_PI),id("mydata1234"){  }  //-------(B)
  int A;
  double X;
  string id;
 
  //Write serialization for this class
  void write(FileStorage& fs) const
  {
    fs << "{" << "A" << A << "X" << X << "id" << id << "}";  //--------(G)
  }
  //Read serialization for this class
  void read(const FileNode& node)   //-------------------------------------(C)
  {
    A = (int)node["A"];
    X = (double)node["X"];
    id = (string)node["id"];
  }
};
//These write and read functions must exist
//as per the inline functions in operations.hpp
void write(FileStorage& fs, const std::string&, const MyData& x)
{
  x.write(fs);
}
void read(const FileNode& node, MyData& x, const MyData& default_value = MyData()) //---(D)
{
  if(node.empty())
    x = default_value; //--------------------------------------------------(E)
  else
    x = (MyData)node; //--------------------------------------------------(F)
}
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)
{
 string filename="test.yml";
  //write
  {
    FileStorage fs(filename, FileStorage::WRITE);
    cout << "writing MyData struct/n";
    MyData m(1); //------------------------------------------------------(1)
    fs << "mdata" << m; //--------------------------------------------------(2)
 cout<<m<<endl;
  }
  //read
  {
    FileStorage fs(filename, FileStorage::READ);
    MyData m;  //------------------------------------------------------(3)
    fs["mdata"] >> m; //--------------------------------------------------(4)
    cout << "read mdata/n";
 cout<<m<<endl;
 
    fs["mdata_b"] >> m; //--------------------------------------------------(5)
    cout << "read mdata_b/n";
 cout<<m<<endl;
  }
  system("pause");
  return 0;
}

一:说明
(A):自定义类型MyData的默认构造函数;
(B):参数为int的单参数构造函数---显式类型转换;
(C):内部读取数据的read函数,输入参数为保存MyData类数据的map型节点;
(D):外部全局读取函数,用于opencv内部操作符>>调用,类似回调函数,其实是一个inline函数template;
(E):节点为空,设为MyData取默认值;
(F):取节点的值,转换为MyData类型-------调用FileNode类的重载操作符做转换---见下面分析;
(1):单参数构造MyData;
(2):写入;
(3):默认构造;
(4):读取;
(5):读取一个不存在的节点数据;

二:单步调试跟踪分析
写入过程,没有问题,(2)内部调用全局的write函数,再调用内部真正写入函数,实现数据写入节点mdata;
主要跟踪读取过程:

从(4)下断点开始跟踪进入operations.hpp文件中(一下的行号都是在opencv2.2该文件中的位置)
---------->L2808:可以看出此时输入的是由FileStorage的[]操作符所返回的存储MyData数据的节点----设为P-node;
template<typename _Tp> static inline void operator >> (const FileNode& n, _Tp& value)
{ FileNodeIterator it = n.begin(); it >> value; }

---------->L2782:对n.begin()的调用----经过这一步,it变为了P-node的下属第一个子节点,
设为C1-node(实际就是存储MyData.A的叶节点);
inline FileNodeIterator FileNode::begin() const
{
    return FileNodeIterator(fs, node);
}

---------->L2797:对L2808中it >> value的调用响应,真正起到读取作用的,输入参数是上面获取的C1-node,是一个叶节点;
template<typename _Tp> static inline FileNodeIterator& operator >> (FileNodeIterator& it, _Tp& value)
{ read( *it, value, _Tp()); return ++it; }

---------->(A)行:对_Tp()的实例化,其实是在(D)进行的。

---------->L2791:对*it的响应,将FileNodeIterator还原为FileNode,以作为参数传递给(D),注意是C1-node;
inline FileNode FileNodeIterator::operator *() const
{ return FileNode(fs, (const CvFileNode*)reader.ptr); }

---------->L2625:对(const CvFileNode*)reader.ptr)的调用;
inline FileNode::FileNode(const CvFileStorage* _fs, const CvFileNode* _node)
    : fs(_fs), node(_node) {}

---------->L2797:对*it和_Tp()处理完后,返回;
template<typename _Tp> static inline FileNodeIterator& operator >> (FileNodeIterator& it, _Tp& value)
{ read( *it, value, _Tp()); return ++it; }

---------->(D):对上面read( *it, value, _Tp())的真正响应,read函数在opencv中实现为inline function template,所以真正的
处理函数是(D),单步到(F);进入

---------->L2708:要转换为MyData型,发现有一个int型单参数构造函数(B),而对于FileNode有一个重载的转换操作符int(),即下面的;
所以首先调用该函数,转换为int,然后调用(B),完成了一个FileNode到MyData的转换,(如果MyData还定义了其他double或者string单参数
的构造函数,则编译出错,因为FileNode也定义(重载)了从FileNode到double和string的转换符);
【重载类型转换操作符,自定义类中定义了参数为int的构造函数的话,FileNode->int->自定义类】
inline FileNode::operator int() const
{
    int value;
    read(*this, value, 0);
    return value;
}

--------->L2649:对上面read(*this, value, 0)的响应,读取真正的数据,对此MyData来说,将取node.node->data.i的值,
如果将(G)中的存入顺序改变一下先存如X那么就取cvRound(node.node->data.f),因为当前node是C1-node即存入的第一个叶节点;
static inline void read(const FileNode& node, int& value, int default_value)
{
    value = !node.node ? default_value :
    CV_NODE_IS_INT(node.node->tag) ? node.node->data.i :
    CV_NODE_IS_REAL(node.node->tag) ? cvRound(node.node->data.f) : 0x7fffffff;
}

--------->L2708:读取之后返回;
inline FileNode::operator int() const
{
    int value;
    read(*this, value, 0);
    return value;
}

-------->(A):由FileNode->int在转换为MyData,需要调用int参数的构造;

--------->返回(F),将转换完的值赋值给x;

--------->L2797:返回it >> value的响应;
template<typename _Tp> static inline FileNodeIterator& operator >> (FileNodeIterator& it, _Tp& value)
{ read( *it, value, _Tp()); return ++it; }

---------->L2808:返回对fs["mdata"] >> m;---(4)的响应;
template<typename _Tp> static inline void operator >> (const FileNode& n, _Tp& value)
{ FileNodeIterator it = n.begin(); it >> value; }

---------->返回(4),完成单步调试;

三:分析
运行完成上面的之后
test.yml文件为:
===============================================
%YAML:1.0
mdata:
   A: 97
   X: 3.1415926535897931e+000
   id: mydata1234
================================================
运行输出:
-----------------------------------------------
writing MyData struct
{ id = mydata1234, X = 3.14159, A = 97}
read mdata
{ id = mydata1234, X = 3.14159, A = 97}
read mdata_b
{ id = , X = 0, A = 0}
-----------------------------------------------

可以看到貌似正确的读出了数据,但是修改一下。
在(1)(2)行之间加入:
  m.A = 12345;
  m.X = 2.71828;
  m.id = "second test";
然后再看:
test.yml
===================================================
%YAML:1.0
mdata:
   A: 12345
   X: 2.7182800000000000e+000
   id: second test
===================================================
以及运行输出:
---------------------------------------------------
writing MyData struct
{ id = second test, X = 2.71828, A = 12345}
read mdata
{ id = mydata1234, X = 3.14159, A = 97}
read mdata_b
{ id = , X = 0, A = 0}
---------------------------------------------------
可以看到,写入的是正确的,读取的却是错误的。
仍然是单参数构造函数的初始化值。

问题就出在(D)函数的第(F)行这里。
在第二部分的单步跟踪调试中,开始时:L2808:
template<typename _Tp> static inline void operator >> (const FileNode& n, _Tp& value)
{ FileNodeIterator it = n.begin(); it >> value; }
可见输入的参数是FileNode,对本例来说也就是通过第(4)---fs["mdata"]获取的存有MyData全部数据的节点(P-node),
看上面函数内部:
FileNodeIterator it = n.begin();
获取了其第一个子节点C1-node,也就是(G)行的第一个输入:A的数据节点,然后后面的运行都是针对这个节点C1-node进行的。
这样在(F)行中首先在内部根据MyData的int单参数构造函数调用FileNode的int()换转操作函数,转为int,然后转为调用(B)转为MyData;
所以无论实际的yml文件里存储的是什么,到头来读出来的一定会是单参数构造所初始化的值。
所以说文档给出的例子在这里是错误的。

开始时我忽略了传入(D)函数的node参数是C1-node,直接把(F)行改为x.read(node);调用内部定义的read方法,完成实际的读取任务,
但是编译通过,一运行到这一步就出现内存访问错误,调试了很久,最后又仔细查看单步跟踪时才发现,此时传入的node是C1-node,
也就是存储数据MyDAta.A的节点,在对它进行[]操作访问子节点肯定是错误的,因为本身该节点就已经是一个叶节点了,当然会出错。、

后来又仔细研究源码这一部分,发现对于自定义类型,使用FileNode的>>操作符来读取数据节点似乎是不对的(也可能是有其他方法
我不知道)觉得这貌似是OpenCV的一个bug,
对于自定义的类型,将L2808:
template<typename _Tp> static inline void operator >> (const FileNode& n, _Tp& value)
{ FileNodeIterator it = n.begin(); it >> value; }
直接改为:
template<typename _Tp> static inline void operator >> (const FileNode& n, _Tp& value)
{ read( n, value, _Tp()); }
不就得了,这样的话应该就可以正确的调用(D)当然(F)行也得随之改动为x.read(node);

想要修改源码然后编译试一下,后来一想,算了还不知道有没有,有多少个内部的函数依赖于此函数,万一改错了麻烦就大了,
还是先不改了,在读取时调用内部的read方法就是了,
将第(4)改为
m.read(fs["mdata"]);
就可以了。
运行输出:
-----------------------------------------------
writing MyData struct
{ id = second test, X = 2.71828, A = 12345}
read mdata
{ id = second test, X = 2.71828, A = 12345}
read mdata_b
{ id = , X = 0, A = 0}
-----------------------------------------------
test.yml文件没变仍然是原样子。

回头再研究一下怎样实现vector<MyData>这样的向量的存储与读取的格式,估计应该也有类似的问题。