记录了cv::Mat的json序列化和反序列化过程,以及比对序列化反序列化前后的两个Mat矩阵是否对应。

Mat to json序列化

Mat是一种特殊的数据类型,如果使用vector再进行JsonArray序列化略微繁琐,且类型不同通道数通用性较差,一般习惯使用base64编码图像信息,首先通过cv::imencode函数进行编码(因为大多数图像是8位位深,故该函数仅支持CV_8U类型的编码);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bool cv::imencode(const std::string& ext, 
InputArray img,
std::vector<uchar>& buf,
const std::vector<int>& params = std::vector<int>()
)

//参数:
ext:png或者jpg,前者无损,后者有损;
img:输入图像;
buf:编码数组;
params:png/jpg模式具体参数(可缺省),如:
- std::vector<int> png_params = {cv::IMWRITE_PNG_COMPRESSION, 3}; //压缩级别0-9,值越大压缩越高,但编码越慢;
- std::vector<int> jpg_params = {cv::IMWRITE_JPEG_QUALITY, 80}; //压缩质量0-100,值越大,质量越好损失越少;

//返回值:
成功返回true
Qt中将uchar字符数组读入QByteArray,即可调用其toBase64方法进行编码,注意这种返回的base64QString而不是std::string,示例:
1
2
3
4
5
6
7
8
9
auto mat2Base64 = [](const cv::Mat& m){
if(m.empty())
return QString();
std::vector<uchar> m_vector;
cv::imencode(".png",m,m_vector);
QByteArray byteArray(reinterpret_cast<const char*>(m_vector.data()),m_vector.size());
QString base64 = byteArray.toBase64();
return base64;
};

json to Mat反序列化

通过QByteArrayfromBase64方法可以从base64字符串解出编码字符,再调用cv::imdecode方法将其解到Mat即可,标志位同imread读取方法,只是二者从不同对象读取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
cv::Mat cv::imdecode(InputArray buf, 
int flags
)

//参数:
buf:输入的Mat编码数组
flags:读取数组方式,可为标志位或数字标识,例如:
- cv::IMREAD_UNCHANGED[-1]:原始读取,保留一切信息(颜色通道/透明通道/位深/通道数等)
- cv::IMREAD_GRAYSCALE[0]:灰图读取
- cv::IMREAD_COLOR[1]:彩图读取,会丢失透明通道(仅PNG格式图片支持透明色)
- cv::IMREAD_ANYDEPTH[2]:保留原始位深信息(其余有可能将16/32位深强制到8位深)
- cv::IMREAD_ANYCOLOR[4]:保留原始RGB信息,顺序可能变成BGR;
- cv::IMREAD_REDUCED_GRAYSCALE_2:灰图读取并缩小二分一
- cv::IMREAD_REDUCED_COLOR_2:彩图读取并缩小二分一
- cv::IMREAD_REDUCED_GRAYSCALE_4:....缩小四分一
......

//返回值:
从imencode编码解码出的Mat类型对象;

示例:

1
2
3
4
5
6
7
auto base642Mat = [](const QString& base64){
if(base64.isEmpty())
return cv::Mat();
QByteArray byteArray = QByteArray::fromBase64(base64.toUtf8()); //base64转编码字符
std::vector<uchar>m_vector(byteArray.begin(),byteArray.end());
return cv::imdecode(m_vector,cv::IMREAD_UNCHANGED);
};

Mat类型校对

为了验证序列化前后的Mat是否具有相同的数据,可以通过cv::compare函数进行比较,其支持六种大小关系的元素比较(相等/不等/小于/大于/小于等于/大于等于),其输出掩码对象255为真,0为假),但注意cv::compare函数仅支持单通道比较:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void cv::compare( //仅支持单通道比较
InputArray src1, // 第一个输入矩阵/标量
InputArray src2, // 第二个输入矩阵/标量
OutputArray dst, // 输出掩码矩阵(CV_8U 类型)
int cmpop // 比较操作符
);

//参数:
src1/src2:要比对的两个输入对象;
dst:输出掩码矩阵,对应元素255为真,0为假
cmpop:六种关系标识符:
- cv::CMP_EQ:==,两个输入对应位置相等,置255
- cv::CMP_NE:!=,两个输入对应位置不等,置255,相等置0
- cv::CMP_GT:>
- cv::CMP_GE:>=
- cv::CMP_LT: <
- cv::CMP_LE: <=

只需要使用split拓展一下,就可以适用于多通道比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//多通道Mat:逐通道比较是否相等
auto isMultiMatSame = [](const cv::Mat&m1,const cv::Mat&m2){
Q_ASSERT(m1.channels()==m2.channels());
if(m1.channels()==1){ //单通道无需split
cv::Mat diff;
compare(m1,m2,diff,cv::CMP_NE);
return (cv::countNonZero(diff)==0);
}
else{ //多通道
vector<cv::Mat> m1_split,m2_split;
cv::split(m1,m1_split);
cv::split(m2,m2_split);
for(int i=0; i<m1.channels(); i++){
cv::Mat diff;
cv::compare(m1_split[i],m2_split[i],diff,cv::CMP_NE);
if(cv::countNonZero(diff)!=0)
return false;
}
return true;
}
};

本文完整测试代码(Qt5+OpenCV)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include "opencv2/opencv.hpp"
//#include <QApplication>
#include <QDebug>
#include <QJsonObject>

using namespace std;

int main(int argc, char*argv[])
{
auto mat2Base64 = [](const cv::Mat& m){ //Mat to base64
if(m.empty())
return QString();
std::vector<uchar> m_vector;
cv::imencode(".png",m,m_vector);
QByteArray byteArray(reinterpret_cast<const char*>(m_vector.data()),m_vector.size());
QString base64 = byteArray.toBase64();
return base64;
};


auto base642Mat = [](const QString& base64){ //base64 to Mat
if(base64.isEmpty())
return cv::Mat();
QByteArray byteArray = QByteArray::fromBase64(base64.toUtf8());
std::vector<uchar>m_vector(byteArray.begin(),byteArray.end());
return cv::imdecode(m_vector,cv::IMREAD_UNCHANGED);
};

//QApplication app(argc,argv);
cv::Mat m = cv::imread("D:/Documents/Desktop/note/chess.jpg"); //任意读入一张图像
cv::imshow("test",m);

QString base64 = mat2Base64(m);

cv::Mat m1 = base642Mat(base64);
cv::imshow("test1",m1);

//多通道Mat:逐通道比较是否相等
auto isMultiMatSame = [](const cv::Mat&m1,const cv::Mat&m2){
Q_ASSERT(m1.channels()==m2.channels());
if(m1.channels()==1){ //单通道无需split
cv::Mat diff;
compare(m1,m2,diff,cv::CMP_NE);
return (cv::countNonZero(diff)==0);
}
else{ //多通道
vector<cv::Mat> m1_split,m2_split;
cv::split(m1,m1_split);
cv::split(m2,m2_split);
for(int i=0; i<m1.channels(); i++){
cv::Mat diff;
cv::compare(m1_split[i],m2_split[i],diff,cv::CMP_NE);
if(cv::countNonZero(diff)!=0)
return false;
}
return true;
}
};
qDebug()<<isMultiMatSame(m,m1); //输出true,序列化/反序列化无损

qDebug() <<"done";
cv::waitKey(0);
cv::destroyAllWindows();
return 0;
//return app.exec();
}

QImage

除了cv::Mat,Qt中会使用QImage打开和存储图像,其Json序列化仍然可以通过base64解决,可以将QImage装入QBuffer,而QBuffer本身可以直接绑定到二进制序列结构QByteArray

QImage to base64

1
2
3
4
5
6
7
8
9
10
11
12
13
QImage pic;
pic.load("D:/Documents/Desktop/note/jLena.jpeg");
if(pic.isNull()){
qDebug() << "Empty Pic!";
return 0;
}

QByteArray byteArray;
QBuffer buf(&byteArray); //绑定QByteArray
buf.open(QIODevice::WriteOnly);
pic.save(&buf,"PNG"); //存入图片数据

QString base64 = byteArray.toBase64();

解析时,QImage支持通过QByteArray直接loadData加载图片:

1
2
3
4
5
6
////从base64解析到QImage
QByteArray rarray = QByteArray::fromBase64(base64.toUtf8());
QImage rpic;
if(!rpic.loadFromData(rarray)){
qDebug() <<"Parse Fault!";
}
Qt重载了QImage比较运算符,直接比较即可比对前后的像素数据:
1
qDebug()<<(rpic == pic); //true
当有一张图片数据,可能我们希望看到它的打印效果,但是QImage的打印需要经过QPainter等,可能今天记住了,明天不查也不会写,因此完全可以通过曲线救国的方式,在支持OpenCV的Qt环境,将序列化的base64转到cv::Mat结构,通过cv::imshow直接打印,非常nice:
1
2
cv::Mat rMat = base642Mat(base64); //该函数源码见上文
cv::imshow("test",rMat);