记录了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}; - std::vector<int> jpg_params = {cv::IMWRITE_JPEG_QUALITY, 80};
成功返回true;
|
Qt中将uchar
字符数组读入QByteArray
,即可调用其toBase64
方法进行编码,注意这种返回的base64
是QString
而不是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反序列化
通过QByteArray
的fromBase64
方法可以从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()); 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
函数仅支持单通道比较,而且仅支持非浮点类型,浮点类型可能出现精度差异,导致dst矩阵非0非1而是出现浮点数,错误返回false
:
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, 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
| auto isMultiMatSame = [](const cv::Mat&m1,const cv::Mat&m2){ Q_ASSERT(m1.channels()==m2.channels()); if(m1.channels()==1){ 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 <QDebug> #include <QJsonObject>
using namespace std;
int main(int argc, char*argv[]) { 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; };
auto base642Mat = [](const QString& base64){ 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); };
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);
auto isMultiMatSame = [](const cv::Mat&m1,const cv::Mat&m2){ Q_ASSERT(m1.channels()==m2.channels()); if(m1.channels()==1){ 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);
qDebug() <<"done"; cv::waitKey(0); cv::destroyAllWindows(); return 0; }
|
非CV_8U类型单通道的序列化
以上例子基于CV_8U
可以无损保存为png
编码,但是如果是浮点类型等可能导致精度损失,因此不能直接使用CV_8U,也无法通过cv::imencode
等编码,但也不复杂,只需要我们手动将Mat装入vector即可,但主要需要额外载入size结构信息,以便后续将一维vector反序列化成Mat:
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
| auto mat2json = [](const cv::Mat& m){ Q_ASSERT(m.type()==5); QJsonObject json; std::vector<float> vp(m.total(),0); int k=0; for(int i=0; i<m.rows; i++){ for(int j=0; j<m.cols; j++){ float* elem = (float*)(m.data + i*m.step[0] + j*m.step[1]); vp[k++] = elem[0]; } } QByteArray byteArray(reinterpret_cast<const char*>(vp.data()),vp.size()*sizeof(float)); json.insert("data",QString(byteArray.toBase64())); json.insert("size",QJsonArray{m.rows,m.cols}); return json; };
auto json2mat = [](const QJsonObject& json){ std::vector<float>vp; QString base64 = json["data"].toString(); QByteArray byteArray = QByteArray::fromBase64(base64.toUtf8()); vp.resize(base64.size()/sizeof(float)); memcpy(vp.data(),byteArray.constData(),byteArray.size()); int row = json["size"].toArray()[0].toInt(); int col = json["size"].toArray()[1].toInt(); return cv::Mat(row,col,CV_32FC1,vp.data()); };
|
非CV_8U类型多通道的序列化
多通道也可以使用一维vector,因为Mat仍然支持一维vector来初始化多通道:
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
| auto mat2json = [](const cv::Mat& m){ Q_ASSERT(m.type()==21); QJsonObject json; std::vector<float> vp(m.total()*m.channels(),0); int k=0; for(int i=0; i<m.rows; i++){ for(int j=0; j<m.cols; j++){ float* elem = (float*)(m.data + i*m.step[0] + j*m.step[1]); for(int c=0; c<m.channels(); c++){ vp[k++] = elem[c]; } } } QByteArray byteArray(reinterpret_cast<const char*>(vp.data()),vp.size()*sizeof(float)); json.insert("data",QString(byteArray.toBase64())); json.insert("size",QJsonArray{m.rows,m.cols}); return json; };
auto json2mat = [](const QJsonObject& json){ std::vector<float>vp; QString base64 = json["data"].toString(); QByteArray byteArray = QByteArray::fromBase64(base64.toUtf8()); vp.resize(byteArray.size()/sizeof(float)); memcpy(vp.data(),byteArray.constData(),byteArray.size()); int row = json["size"].toArray()[0].toInt(); int col = json["size"].toArray()[1].toInt(); return cv::Mat(row,col,CV_32FC3,vp.data()); };
|
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); buf.open(QIODevice::WriteOnly); pic.save(&buf,"PNG");
QString base64 = byteArray.toBase64();
|
解析时,QImage
支持通过QByteArray
直接loadData
加载图片:
1 2 3 4 5 6
| QByteArray rarray = QByteArray::fromBase64(base64.toUtf8()); QImage rpic; if(!rpic.loadFromData(rarray)){ qDebug() <<"Parse Fault!"; }
|
Qt重载了QImage
的比较运算符,直接比较即可比对前后的像素数据:
1
| qDebug()<<(rpic == pic);
|
当有一张图片数据,可能我们希望看到它的打印效果,但是QImage
的打印需要经过QPainter
等,可能今天记住了,明天不查也不会写,因此完全可以通过曲线救国的方式,在支持OpenCV的Qt环境,将序列化的base64
转到cv::Mat
结构,通过cv::imshow
直接打印,非常nice:
1 2
| cv::Mat rMat = base642Mat(base64); cv::imshow("test",rMat);
|
QImage可以直接保存:
1 2 3
| QImage pic; ... pic.save("/path/xx.png","PNG",-1);
|