OpenCV中,Mat是最基础的数据处理单元,矩阵可以用Mat表示,图像可以用Mat表示,向量也可以用Mat表示,它们之间有何区别?
理解好Mat对学好OpenCV有很大帮助。相信很多人和我一样,接触OpenCV时,被Mat弄得一头雾水。我这里写一些我对Mat的理解。
平时我们看见的图像,在计算机眼里其实是个大的数字矩阵。图像涉及到通道,而且像素值是正整数,而我们印象中的矩阵是没有这些特点的。OpenCV为了把图像和一般的矩阵统一到Mat中,特地给矩阵添加了一个矩阵头。
Mat本质上是有由一个矩阵头和一个指针组成的类。矩阵头包含了矩阵的大小,存储的方法,存储的地址等,指针指向了一个包含像素值的矩阵。
矩阵头的大小是恒定的,而矩阵本身的大小因图像的不同而不同,通常是较大的数量级。OpenCV 是图像处理库,一般图像的像素值矩阵非常大,矩阵头能让我们对图像进行一些操作时变得更加快捷和方便。
我们来看看Mat的存储形式。Mat和Matlab里的数组格式有点像,但一般是二维向量,如果是灰度图,一般存放<uchar>类型;如果是RGB彩色图,存放<Vec3b>类型。
单通道灰度图数据存放格式:
多通道的图像中,每列并列存放通道数量的子列,如RGB三通道彩色图:
Mat(); //构造2维矩阵头 Mat(int rows, int cols, int type); Mat(Size size, int type); //构造n维矩阵头 Mat(int ndims, const int* sizes, int type); //创建矩阵头 void create(int rows, int cols, int type); void create(Size size, int type); void create(int ndims, const int* sizes, int type); //创建一个指定行数的矩阵头 Mat row(int y) const; //创建一个指定列数的矩阵头 Mat col(int x) const; //创建一个指定行数范围的矩阵头 Mat rowRange(int startrow, int endrow) const; Mat rowRange(const Range& r) const; //创建一个指定列数范围的矩阵头 Mat colRange(int startcol, int endcol) const; Mat colRange(const Range& r) const;
//构造2维矩阵并赋值 Mat(int rows, int cols, int type, const Scalar& s); Mat(Size size, int type, const Scalar& s); //构造n维矩阵并赋值 Mat(int ndims, const int* sizes, int type, const Scalar& s); //复制矩阵 Mat(const Mat& m); //Mat m3(m2);将m2复制到m3中,m2和m3共用同一内存位置的数据,也就是说改变m2,m3会根根变,同理 //根据data构造矩阵 Mat(int rows, int cols, int type, void* data, size_t step=AUTO_STEP); Mat(Size size, int type, void* data, size_t step=AUTO_STEP); Mat(int ndims, const int* sizes, int type, void* data, const size_t* steps=0); //从Mat中提取一个区域来构造矩阵 Mat(const Mat& m, const Range& rowRange, const Range& colRange=Range::all()); Mat(const Mat& m, const Rect& roi); Mat(const Mat& m, const Range* ranges); //根据特定对象来构造矩阵,其实是其他数据类型转Mat,false表示不包含数据,true表示包含数据 //将CvMat转换成Mat,默认不复制数据。 Mat(const CvMat* m, bool copyData=false); //将CvMatND转换成Mat,默认不复制数据。 Mat(const CvMatND* m, bool copyData=false); //将IplImage转换成Mat,默认不复制数据。 Mat(const IplImage* img, bool copyData=false); //根据std::vector构造矩阵,默认不复制数据 template<typename _Tp> explicit Mat(const vector<_Tp>& vec, bool copyData=false); //根据cv::Vec构造矩阵,默认不复制数据 template<typename _Tp, int n> explicit Mat(const Vec<_Tp, n>& vec, bool copyData=true); //根据cv::Matx构造矩阵,默认不复制数据 template<typename _Tp, int m, int n> explicit Mat(const Matx<_Tp, m, n>& mtx, bool copyData=true); //根据2维点构造矩阵 template<typename _Tp> explicit Mat(const Point_<_Tp>& pt, bool copyData=true); //根据3维点构造矩阵 template<typename _Tp> explicit Mat(const Point3_<_Tp>& pt, bool copyData=true); //根据comma initializer构造矩阵,默认不复制矩阵 template<typename _Tp> explicit Mat(const MatCommaInitializer_<_Tp>& commaInitializer); //构造特殊矩阵 static MatExpr zeros(int rows, int cols, int type); static MatExpr zeros(Size size, int type); static MatExpr zeros(int ndims, const int* sz, int type); static MatExpr ones(int rows, int cols, int type); static MatExpr ones(Size size, int type); static MatExpr ones(int ndims, const int* sz, int type); static MatExpr eye(int rows, int cols, int type); static MatExpr eye(Size size, int type); //Mat m1 = Mat::eye(5,5,CV_8UC1); //复制矩阵,包括数据 Mat clone() const; void copyTo( OutputArray m ) const; void copyTo( OutputArray m, InputArray mask ) const; void convertTo( OutputArray m, int rtype, double alpha=1, double beta=0 ) const; void assignTo( Mat& m, int type=-1 ) const; //从GpuMat下载数据 explicit Mat(const gpu::GpuMat& m);
//析构函数 ~Mat(); //同Mat::release()。 //释放矩阵 void release(); //释放矩阵数据 void deallocate();
//将矩阵所有元素设置为s Mat& operator = (const Scalar& s); //根据mask将矩阵部分元素设置为s Mat& setTo(InputArray value, InputArray mask=noArray()); //改变矩阵的通道数cn、维数dims和行数rows Mat reshape(int cn, int rows=0) const; Mat reshape(int cn, int newndims, const int* newsz) const;
//判断矩阵在内存是否是连续存放 bool isContinuous() const; //判断一个矩阵是否是另一个矩阵的子矩阵 bool isSubmatrix() const; //以字节为单位返回元素大小,比如CV_8U返回1,CV_8U3返回3,CV_16U返回2,CV_64FC3返回24 size_t elemSize() const; //以字节形式返回元素通道的尺寸,<span style="font-family: Arial, Helvetica, sans-serif;">也就是elemSize()/channels()</span> size_t elemSize1() const; //返回矩阵数据类型 int type() const; //返回矩阵数据深度,<span style="color: rgb(17, 17, 17); font-size: 13px; line-height: 16.848px; white-space: pre-wrap;">CV_8U=0, CV_8S=1, CV_16U=2, CV_16S=3, CV_32S=4, CV_32F=5, CV_64F=6</span> int depth() const; //返回矩阵通道 int channels() const; //返回step/elemSize1() size_t step1(int i=0) const; //判断矩阵是否为空,是返回true bool empty() const; //返回矩阵所有元素的和 size_t total() const; //检查Mat某通道元素矩阵是否是向量,是就返回N int checkVector(int elemChannels, int depth=-1, bool requireContinuous=true) const; //返回指向Mat的行指针 uchar* ptr(int i0=0); const uchar* ptr(int i0=0) const; //返回指向Mat像素点的指针 uchar* ptr(int i0, int i1); const uchar* ptr(int i0, int i1) const; //返回指向第#0行#1列#2通道的指针 uchar* ptr(int i0, int i1, int i2); const uchar* ptr(int i0, int i1, int i2) const; //返回指向索引为idx的某个数(不是像素点)的指针 uchar* ptr(const int* idx); //返回只读型的指向索引为idx的某个数(不是像素点)的指针 const uchar* ptr(const int* idx) const;
//返回矩阵对角线元素,d=0,对角线,d>0,对角线下移d单位的斜线上元素,d<0,对角线上移d单位的斜线上元素 Mat diag(int d=0) const; static Mat diag(const Mat& d); //计算矩阵的转置 MatExpr t() const; //计算矩阵的逆, /*DECOMP_LU:采用的是高斯消去法(Gaussian elimination),只能计算非奇异矩阵的逆,如果矩阵可逆,则函数返回非零值,否则返回0. DECOMP_SVD:采用的是奇异值分解法(singular value decomposition)。对于奇异矩阵,计算伪逆矩阵,非奇异矩阵则计算逆矩阵,如果矩阵是非奇异的,则返回最小奇异值和最大奇异值的比例,否则返回0. DECOMP_CHOLESKY:采用的是Cholesky分解*/ MatExpr inv(int method=DECOMP_LU) const; //矩阵按元素相乘或相除。比如,Mat C = A.mul(5/B); //等价于divide(A,B,C,5) MatExpr mul(InputArray m, double scale=1) const; //计算两向量的叉积(外积,向量积)。 Mat cross(InputArray m) const; //计算两向量的点积(内积,数量积)。 double dot(InputArray m) const;
//Mat转CvMat、CvMatND、IplImage,不复制数据 operator CvMat() const; operator CvMatND() const; operator IplImage() const; //CvMat、CvMatND、IplImage转换成Mat,默认不复制数据。 Mat(const CvMat* m, bool copyData=false); //将CvMatND转换成Mat,默认不复制数据。 Mat(const CvMatND* m, bool copyData=false); //将IplImage转换成Mat,默认不复制数据。 Mat(const IplImage* img, bool copyData=false);
template<int n> uchar* ptr(const Vec<int, n>& idx); template<int n> const uchar* ptr(const Vec<int, n>& idx) const; //! template version of the above method template<typename _Tp> _Tp* ptr(int i0=0); template<typename _Tp> const _Tp* ptr(int i0=0) const; template<typename _Tp> _Tp* ptr(int i0, int i1); template<typename _Tp> const _Tp* ptr(int i0, int i1) const; template<typename _Tp> _Tp* ptr(int i0, int i1, int i2); template<typename _Tp> const _Tp* ptr(int i0, int i1, int i2) const; template<typename _Tp> _Tp* ptr(const int* idx); template<typename _Tp> const _Tp* ptr(const int* idx) const; template<typename _Tp, int n> _Tp* ptr(const Vec<int, n>& idx); template<typename _Tp, int n> const _Tp* ptr(const Vec<int, n>& idx) const; //! the same as above, with the pointer dereferencing template<typename _Tp> _Tp& at(int i0=0); template<typename _Tp> const _Tp& at(int i0=0) const; template<typename _Tp> _Tp& at(int i0, int i1); template<typename _Tp> const _Tp& at(int i0, int i1) const; template<typename _Tp> _Tp& at(int i0, int i1, int i2); template<typename _Tp> const _Tp& at(int i0, int i1, int i2) const; template<typename _Tp> _Tp& at(const int* idx); template<typename _Tp> const _Tp& at(const int* idx) const; template<typename _Tp, int n> _Tp& at(const Vec<int, n>& idx); template<typename _Tp, int n> const _Tp& at(const Vec<int, n>& idx) const; //! special versions for 2D arrays (especially convenient for referencing image pixels) template<typename _Tp> _Tp& at(Point pt); template<typename _Tp> const _Tp& at(Point pt) const; //! template methods for iteration over matrix elements. // the iterators take care of skipping gaps in the end of rows (if any) template<typename _Tp> MatIterator_<_Tp> begin(); template<typename _Tp> MatIterator_<_Tp> end(); template<typename _Tp> MatConstIterator_<_Tp> begin() const; template<typename _Tp> MatConstIterator_<_Tp> end() const;
我们来看一些实际的例子,加深对Mat的理解。先读入一张图片
Mat img = imread("test.png");
来看看内存里放了哪些东西。
flags不知道是什么。。。
dims = 2. 表明img是矩阵,等于1就是向量了;
rows = 486,cols=646; 图像尺寸是646×486,也可以用img.size返回图像尺寸;
Mat的数据都存在了data中,注意到data是指针型的,所以我们访问数据都要用到指针,也就是地址;图像在内存的地址从datastart到dataend,即0X00e3d050到0X00f22f7c,那么图像大小 = 0X00f22f7c - 0X00e3d050 = 0Xe5f2c = 941868 = 486×646×3 = rows×cols×channels;
重点是step!step定义了矩阵的布局,img.step[0]=1938=646×3,所以step[0]是矩阵一行的大小。step[1]=3,step[1]表示每一列由3组元素组成,其实img.channels()也等于3,step[1]也可以看做通道数。知道了数据的起始地址、矩阵一行的大小和通道数,那么图像上(m,n)点的地址,就可以表示为
ptr = datastart + step[0]*m+step[1]*n
对应的各通道数值的地址就分别为ptr[0]、ptr[1]、ptr[2]。
还不懂的话可以参照下图进行理解
上面是一个 3 X 4 的矩阵,假设其数据类型为 CV_8UC1,则
M.dims = 2,
M.rows = 3;
M.cols = 4。
因为是二维矩阵,那么 step 数组只有两个值, step[0] 和 step[1] 分别代表一行的数据大小和一个元素的数据大小,则 M.step[0] == 4, M.step[1] == 1。点(1,2)的地址
ptr12 = addr(M1,2) = M.data +M.step[0]*1 + M.step[1]*2
假设上面的矩阵数据类型是 CV_8UC3。
M.dims == 2;
M.channels() == 3;
M.depth() == 0;
M.elemSize() == 3 (每一个元素包含3个uchar值),
M.elemSize1() == 1 (elemSize / channels),
M.step[0] == M.cols * M.elemSize() == 12,
M.step[1] == M.channels() * M.elemSize1() == M.elemSize() == 3;
点(1,2)处各通道数值的地址分别为ptr12[0],ptr12[1],ptr12[2]
再来看看3维情况
上面是一个 3 X 4 X 6 的矩阵,假设其数据类型为 CV_16SC4,也就是 short 类型,在内存中是存成3个4×6矩阵的。
M.dims = 3 ;
M.channels() = 4 ;
M.elemSize1() = sizeof(short) = 2 ;
M.elemSize() = M.elemSize1() * M.channels() = M.step[M.dims-1]= M.step[2] = 2 * 4 = 8;
M.step[0] = 4 * 6 * M.elemSize() = 192;
M.step[1] = 6 * M.elemSize() = 48;
M.step[2] = M.elemSize() = 8;
M.step1(0) = M.step[0] / M.elemSize() = 48 / 2 = 96 (第一维度(即面的元素个数) * 通道数);
M.step1(1) = M.step[1] / M.elemSize() = 12 / 2 = 24(第二维度(即行的元素个数/列宽) * 通道数);
M.step1(2) = M.step[2] / M.elemSize() = M.channels() = 4(第三维度(即元素) * 通道数);
那么点(0,1,2)的地址为
ptr = addr(M0,1,2) = M.data +M.step[0]*0 + M.step[1]*1+ M.step[2]*2
对应的各通道的地址为ptr[0]、ptr[1]、ptr[2]、ptr[3]
知道了地址,元素的值就用取值运算符“ * ”,比如上面,*ptr[0] 表示M在点(0,1,2)处第1通道的值。