所谓序列化,就是讲内存数据保存为磁盘数据的过程,反序列化就是反过来理解。对于图像处理程序来说,最主要的变量是图片,然后还有相关的参数或运算结果。这里区分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;
}