所谓序列化,就是讲内存数据保存为磁盘数据的过程,反序列化就是反过来理解。对于图像处理程序来说,最主要的变量是图片,然后还有相关的参数或运算结果。这里区分4个部分、由简单到复杂,分享一下自己的研究成果,希望能够给需要的工程师提供一些帮助。
一、基本操作
OpenCV本身提供了FileStorage的序列化保存方法,这对于保存参数来说非常适合;但是如果用来保存图片,会将原始图片的体积多倍增大,速度也比较慢。Mfc本身也提供了序列化的操作,但是使用起来的话,需要注意的地方比较多,比不上OpenCV来的直接。
我们最终想要通过保存得到,并且能够被图像处理程序读取的,是一个单一的文件。这个文件不仅包含了图片数据,而且包括相关的参数和运算结果,同时这个文件不能太大。所以我想到采用zip压缩/解压的方式来打包原始图片和运算结果。实验证明,效果是能够符合要求的。
在打包代码的选择上,找到了比较好的实现。zip.c++/unzip.c++中提供了稳定并且便于使用的压缩解压过程(具体使用参考对应的.h文件,压缩文件可以设定密码)。实际使用中,保存的时候参数保存为.xml文件,图片保存为.jpg图片,而后统一压缩成.go文件;读取的时候反过来操作。
为了说明问题,编写例程。现在把使用说明一下,具体细节可以参考代码。
1、点击读取图片,可以读入jpg或bmp图片,同时手工设置参数一到三
2、点击保存,保存为.go文件
3、点击打开,打开相应的.go文件,同时解压缩后,图片和参数分别显示出来。
本例程主要展现的是“图像处理程序的序列化和反序列化”,而后结合实际使用过程中发现的问题进行衍生。希望能够有类似需求的工程师提供一些帮助。
主要代码: //保存序列化结果
void CGOsaveView : : OnButtonSave() { CString str1;string s1; CString str2;string s2; CString str3;string s3; CString szFilters = _T( "go(*.go)|*.go|*(*.*)|*.*||" ); CString FilePathName = "" ; CFileDialog dlg(FALSE,NULL,NULL, 0 ,szFilters, this ); if (dlg.DoModal() == IDOK){ FilePathName = dlg.GetPathName(); } if (m_fimage.rows < = 0 ) { AfxMessageBox( "m_fimage为空!" ); return ; } GetDlgItemText(IDC_EDIT1,str1); GetDlgItemText(IDC_EDIT2,str2); GetDlgItemText(IDC_EDIT3,str3); s1 = str1.GetBuffer( 0 ); s2 = str2.GetBuffer( 0 ); s3 = str3.GetBuffer( 0 ); string filename = "params.xml" ; FileStorage fs(filename, FileStorage : : WRITE); fs << "str1" << s1; fs << "str2" << s2; fs << "str3" << s3; fs.release(); imwrite( "m_fimage.jpg" ,m_fimage); AfxMessageBox( "数据保存成功!" ); HZIP hz = CreateZip(FilePathName, "GreenOpen" ); //可以设定密码 ZipAdd(hz, "params.xml" , "params.xml" ); ZipAdd(hz, "m_fimage.jpg" , "m_fimage.jpg" ); CloseZip(hz); AfxMessageBox( "数据压缩成功!" ); } //打开序列化结果 void CGOsaveView : : OnButtonOpen() { string s1; string s2; string s3; CString szFilters = _T( "*(*.*)|*.*|go(*.go)|*.go||" ); CString FilePathName = "" ; CFileDialog dlg(TRUE,NULL,NULL, 0 ,szFilters, this ); if (dlg.DoModal() == IDOK){ FilePathName = dlg.GetPathName(); } HZIP hz = OpenZip(FilePathName, "GreenOpen" ); ZIPENTRY ze; GetZipItem(hz, - 1 , & ze); int numitems = ze.index; if (numitems < = 0 ) { AfxMessageBox( "文件读取错误!" ); return ; } for ( int i = 0 ; i < numitems; i ++ ) { GetZipItem(hz,i, & ze); UnzipItem(hz,i,ze.name); } CloseZip(hz); AfxMessageBox( "数据解压缩成功" ); m_fimage = imread( "m_fimage.jpg" ); if (m_fimage.rows < = 0 ) { AfxMessageBox( "文件读取错误!" ); return ; } string filename = "params.xml" ; FileStorage fs(filename, FileStorage : : READ); fs[ "str1" ] >> s1; fs[ "str2" ] >> s2; fs[ "str3" ] >> s3; SetDlgItemText(IDC_EDIT1,s1.c_str()); SetDlgItemText(IDC_EDIT2,s2.c_str()); SetDlgItemText(IDC_EDIT3,s3.c_str()); AfxMessageBox( "数据反序列成功" ); SOURSHOW; }
我们需要注意到的是这里的Mat是可以直接序列化的,这种方法对于存储OpenCV一类的变量来说,非常方便。但是如果是自己设定的结构体了?
二、存储自己的结构体
这里给出一个新的例子,值得参考:
//另存当前模板数据 BOOL CGOImageShopDoc : :OutPutElementItems(string filename) { FileStorage fs(filename, FileStorage : :WRITE); 具体写下内容,注意OpenCV支持Rect等基础结构的序列化 int iElementStruct = m_rctTracker.size(); //数量 fs << "iElementStruct" << iElementStruct; //按照openCV推荐的方法来写入和读取数据。 fs << "ElementContent" << "["; for ( int i = 0; i < iElementStruct; i ++) { string strName(CW2A(m_rctTracker[i].name.GetString())); string strTypeName(CW2A(m_rctTracker[i].typeName.GetString())); int iLeft = m_rctTracker[i].AreaTracker.m_rect.left; int iTop = m_rctTracker[i].AreaTracker.m_rect.top; int iWidth = m_rctTracker[i].AreaTracker.m_rect.Width(); int iHeight = m_rctTracker[i].AreaTracker.m_rect.Height(); fs << "{:" << "strName" <<strName << "strTypeName" <<strTypeName << "rectLeft" <<iLeft << "rectTop" <<iTop << "rectWidth" <<iWidth << "rectHeight" <<iHeight << "}"; } fs << "]"; 书写内容结束 fs.release(); return TRUE; }
//读取模板书 BOOL CGOImageShopDoc : :ReadElementsItems(string filename) { //读取数据 FileStorage fs(filename, FileStorage : :READ); if (fs.isOpened()) { //清空现有数据 m_rctTracker.clear(); //具体业务 int iElementStruct = - 1; Rect rect; fs[ "iElementStruct"] >> iElementStruct; cv : :FileNode features = fs[ "ElementContent"]; cv : :FileNodeIterator it = features.begin(), it_end = features.end(); int idx = 0; for (; it != it_end; ++it, idx ++) { string strName;string strTypeName; int iLeft; int iTop; int iWidth; int iHeight; strName = (string)( *it)[ "strName"]; //获得strName strTypeName =(string)( *it)[ "strTypeName"]; iLeft = ( int)( *it)[ "rectLeft"]; iTop = ( int)( *it)[ "rectTop"]; iWidth = ( int)( *it)[ "rectWidth"]; iHeight = ( int)( *it)[ "rectHeight"]; CRect rect = CRect(iLeft, iTop, iLeft +iWidth, iTop +iHeight); //获得rect //生成识别区域 Mat matROI = m_imageRaw(Rect(iLeft,iTop,iWidth,iHeight)); vector <CRect > vecFindRect ; if (strTypeName == "定位") { vecFindRect = findRect(matROI); } …… } } fs.release(); return TRUE; }
如果我们打开这里保存的文件,可以发现这种模式:
%YAML : 1. 0 -- - iElementStruct : 15 ElementContent : - { strName : "定位", rectLeft : 37, rectTop : 73, rectWidth : 241, rectHeight : 120 } - { strName : "定位", rectLeft : 1556, rectTop : 107, rectWidth : 130, rectHeight : 70 } - { strName : "定位", rectLeft : 3127, rectTop : 99, rectWidth : 93, rectHeight : 70 } - { strName : "定位", rectLeft : 19, rectTop : 2187, rectWidth : 95, rectHeight : 77 } - { strName : "定位", rectLeft : 1592, rectTop : 2203, rectWidth : 95, rectHeight : 44 } - { strName : "定位", rectLeft : 3151, rectTop : 2184, rectWidth : 84, rectHeight : 68 } - { strName : "考号", rectLeft : 1042, rectTop : 419, rectWidth : 300, rectHeight : 121 } - { strName : "主观分数", rectLeft : 161, rectTop : 678, rectWidth : 929, rectHeight : 63 } - { strName : "主观分数", rectLeft : 1789, rectTop : 203, rectWidth : 869, rectHeight : 76 } - { strName : "主观分数", rectLeft : 1777, rectTop : 717, rectWidth : 868, rectHeight : 64 } - { strName : "主观分数", rectLeft : 1785, rectTop : 1713, rectWidth : 388, rectHeight : 66 } - { strName : "主观题", rectLeft : 76, rectTop : 825, rectWidth : 1450, rectHeight : 1246 } - { strName : "主观题", rectLeft : 1692, rectTop : 367, rectWidth : 1524, rectHeight : 323 } - { strName : "主观题", rectLeft : 1696, rectTop : 864, rectWidth : 1518, rectHeight : 749 } - { strName : "主观题", rectLeft : 1696, rectTop : 1787, rectWidth : 1534, rectHeight : 307 }
那么,这种方式是OpenCV支持的结构保存方式,每一个
- { strName : "主观题", rectLeft : 1696, rectTop : 1787, rectWidth : 1534, rectHeight : 307 }
是一个可以存储读取的结构。
三、FileNode支持哪些结构
在这个例子中,我们非常丑陋地使用了4个int值来定义一个Rect。为什么不能直接定义?
比如编写代码
string filename = "序列化.yml" ;
FileStorage fs(filename, FileStorage : :WRITE); fs << "str1" << 1; cv : :Rect cvRect( 10, 10, 10, 10); fs << "cvRect" <<cvRect; fs.release(); return 0;
生成这样的结果:
%YAML : 1. 0 -- - str1 : 1 cvRect : [ 10, 10, 10, 10 ]
但是,如果我们读取这个Rect,并且编写这样的代码
则会报错:
为了进一步解析这个问题,翻看OpenCV的代码:
class CV_EXPORTS_W_SIMPLE FileNode { public : //! type of the file storage node enum Type { NONE = 0, //!< empty node INT = 1, //!< an integer REAL = 2, //!< floating-point number FLOAT = REAL, //!< synonym or REAL STR = 3, //!< text string in UTF-8 encoding STRING = STR, //!< synonym for STR REF = 4, //!< integer of size size_t. Typically used for storing complex dynamic structures where some elements reference the others SEQ = 5, //!< sequence MAP = 6, //!< mapping TYPE_MASK = 7, FLOW = 8, //!< compact representation of a sequence or mapping. Used only by YAML writer USER = 16, //!< a registered object (e.g. a matrix) EMPTY = 32, //!< empty structure (sequence or mapping) NAMED = 64 //!< the node has a name (i.e. it is element of a mapping) };
那么的确是不可能直接转换为所有的OpenCV类型,这里只是保存为了其他节点的序列,通过代码测试也的确是这样。
在这种情况下,我们可以首先将序列读入vector中,非常有用。
而后再根据实际情况进行封装。
四、更进一步,进行类封装
如果想更进一步,自然需要采用类的方法,这里是一个很好的例子。
# include <opencv2\opencv.hpp > # include "opencv2/imgproc/imgproc.hpp" # include "opencv2/highgui/highgui.hpp" # include "opencv2/core/core.hpp" # include <iostream > # include <fstream > using namespace std; using namespace cv; class ColletorMat { private : int indexFrame; bool found; Mat frame; public : ColletorMat( int index, bool found, Mat frame) { this - >indexFrame = index; this - >found = found; this - >frame = frame; } ~ColletorMat() { } // settors void set_indexFrame( int index) { this - >indexFrame = index; } void set_found( bool found) { this - >found = found; } void set_frame(Mat frame) { this - >frame = frame; } // accessors int get_indexFrame() { return this - >indexFrame; } bool get_found() { return this - >found; } Mat get_frame() { return this - >frame; } }; void matwrite(ofstream & fs, const Mat & mat, int index, bool checking) { // Data Object int indexFrame = index; bool found = checking; fs.write(( char *) &indexFrame, sizeof( int)); // indexFrame fs.write(( char *) &found, sizeof( bool)); // bool checking // Header int type = mat.type(); int channels = mat.channels(); fs.write(( char *) &mat.rows, sizeof( int)); // rows fs.write(( char *) &mat.cols, sizeof( int)); // cols fs.write(( char *) &type, sizeof( int)); // type fs.write(( char *) &channels, sizeof( int)); // channels // Data if (mat.isContinuous()) { fs.write(mat.ptr < char >( 0), (mat.dataend - mat.datastart)); } else { int rowsz = CV_ELEM_SIZE(type) * mat.cols; for ( int r = 0; r < mat.rows; ++r) { fs.write(mat.ptr < char >(r), rowsz); } } } ColletorMat matread(ifstream & fs) { // Data Object int indexFrame; bool found; fs.read(( char *) &indexFrame, sizeof( int)); // fs.read(( char *) &found, sizeof( bool)); // // Header int rows, cols, type, channels; fs.read(( char *) &rows, sizeof( int)); // rows fs.read(( char *) &cols, sizeof( int)); // cols fs.read(( char *) &type, sizeof( int)); // type fs.read(( char *) &channels, sizeof( int)); // channels // Data Mat mat(rows, cols, type); fs.read(( char *)mat.data, CV_ELEM_SIZE(type) * rows * cols); ColletorMat ojbectMat(indexFrame, found, mat); return ojbectMat; } int main() { // Save the random generated data { Mat image1, image2, image3; image1 = imread( "C:\\opencvVid\\data_seq\\Human3\\0001.jpg"); image2 = imread( "C:\\opencvVid\\data_seq\\Human3\\0002.jpg"); image3 = imread( "C:\\opencvVid\\data_seq\\Human3\\0003.jpg"); if (image1.empty() || image2.empty() || image3.empty()) { std : :cout << "error: image not readed from file\n"; return( 0); } imshow( "M1",image1); imshow( "M2",image2); imshow( "M3",image3); ( char)cvWaitKey( 0); ofstream fs( "azdoudYoussef.bin", fstream : :binary); matwrite(fs, image1, 100, true); matwrite(fs, image2, 200, true); matwrite(fs, image3, 300, true); fs.close(); double tic = double(getTickCount()); ifstream loadFs( "azdoudYoussef.bin", ios : :binary); if( !loadFs.is_open()){ cout << "error while opening the binary file" << endl; } ColletorMat lcolletorMat1 = matread(loadFs); ColletorMat lcolletorMat2 = matread(loadFs); ColletorMat lcolletorMat3 = matread(loadFs); cout << "frames loaded up " << endl; vector <ColletorMat > setFrames; setFrames.push_back(lcolletorMat1); setFrames.push_back(lcolletorMat2); setFrames.push_back(lcolletorMat3); imshow( "1", lcolletorMat1.get_frame()); imshow( "2", lcolletorMat2.get_frame()); imshow( "3", lcolletorMat3.get_frame()); ( char)cvWaitKey( 0); cout << "indexFrame" <<lcolletorMat1.get_indexFrame() << "found" << lcolletorMat1.get_found(); double toc = ( double(getTickCount()) - tic) * 1000. / getTickFrequency(); cout << "Using Raw: " << toc << endl; loadFs.close(); } return 0; }