一、Mat对象的-访问
我们在对图像操作时,往往不是对图像的整体的操作,而是对图像的像素进行操作,所以高效的遍历图像就尤为重要。
我们以CV_8UC3类型图像降色彩为例,来分析四种遍历方式。
at与重载()
- at
<typename>
(i,j):
Mat类提供一个at的方法取得图像上的像素值,它是一个模板函数,可以取到任意类型的图像上的点.它的源码在mat.inl.hpp中,
bool colorReduce(Mat &_src, Mat &_dst, int div)
{
if (_src.empty())
{
cout << "the input image is empty..." << endl;
return false;
}
//指定_dst的属性
_src.copyTo(_dst);
for (int i = 0; i < _src.rows; i++)
{
for (int j = 0; j < _src.cols; j++)
{
_dst.at<Vec3b>(i, j)[0] = _src.at<Vec3b>(i, j)[0] / div*div + div / 2;
_dst.at<Vec3b>(i, j)[1] = _src.at<Vec3b>(i, j)[1] / div*div + div / 2;
_dst.at<Vec3b>(i, j)[2] = _src.at<Vec3b>(i, j)[2] / div*div + div / 2;
}
}
return true;
}
注释:
1. _src.at(i, j)[0]是取出B通道的第i行第j列的像素点,[1]是G通道,[2]是R通道。
2. Vec3b是像素值类型,它在matx.hpp中是通过typedef Vec< T, N> 来定义的,T代表类型,但它对单通道的数据时用uchar、short、int、float、double。例如:
* 取CV_8UC1类型的点则用uchar
* 取CV_8UC3类型的点则用Vec3b
at对下面类型的数据均支持
/** @name Shorter aliases for the most popular specializations of Vec<T,n>
@{
*/
typedef Vec<uchar, 2> Vec2b;
typedef Vec<uchar, 3> Vec3b;
typedef Vec<uchar, 4> Vec4b;
typedef Vec<short, 2> Vec2s;
typedef Vec<short, 3> Vec3s;
typedef Vec<short, 4> Vec4s;
typedef Vec<ushort, 2> Vec2w;
typedef Vec<ushort, 3> Vec3w;
typedef Vec<ushort, 4> Vec4w;
typedef Vec<int, 2> Vec2i;
typedef Vec<int, 3> Vec3i;
typedef Vec<int, 4> Vec4i;
typedef Vec<int, 6> Vec6i;
typedef Vec<int, 8> Vec8i;
typedef Vec<float, 2> Vec2f;
typedef Vec<float, 3> Vec3f;
typedef Vec<float, 4> Vec4f;
typedef Vec<float, 6> Vec6f;
typedef Vec<double, 2> Vec2d;
typedef Vec<double, 3> Vec3d;
typedef Vec<double, 4> Vec4d;
typedef Vec<double, 6> Vec6d;
/** @} */
当然OpenCV还定义了一个更加简单的方式,
Mat_<uchar> im = image;
im(i,j)=im(i,j)/ div*div + div / 2;
效果与上一种方法相同,花费时间相差不多,但是操作看起来更加简单
bool colorReduce1(Mat &_src, Mat &_dst, int div)
{
if (_src.empty())
{
cout << "the input image is empty..." << endl;
return false;
}
//指定_dst的属性
_src.copyTo(_dst);
Mat_<Vec3b> im_dst = _dst;
Mat_<Vec3b> im_src = _src;
for (int i = 0; i < _src.rows; i++)
{
for (int j = 0; j < _src.cols; j++)
{
im_dst(i, j)[0] = im_src(i, j)[0] / div*div + div / 2;
im_dst(i, j)[1] = im_src(i, j)[1] / div*div + div / 2;
im_dst(i, j)[2] = im_src(i, j)[2] / div*div + div / 2;
}
}
return true;
}
对比这这两种方法:
- at这里是由cv::Vec3b &cv::Mat::at < cv::Vec3b >(int i0,int i1)定义,当然它也有重载一边支持其他形式的数据的访问。
- 另一种方式是对()进行重载,由cv::Vec3b &cv::Mat_< cv::Vec >::operator()(int idx0, int idx1).
用数组来遍历图像
**Mat.ptr< uchar >(i).
bool colorReduce2(Mat &_src, Mat &_dst, int div)
{
if (_src.empty())
{
cout << "the input image is empty..." << endl;
return false;
}
//指定_dst的属性
_src.copyTo(_dst);
for (int i = 0; i < _src.rows; i++)
{
const uchar * _src_data = _src.ptr<uchar>(i);
uchar *_dst_data = _dst.ptr<uchar>(i);
for (int j = 0; j < (_src.cols*_src.channels()); j++)
{
_dst_data[j] = _src_data[j] / div*div + div / 2;
}
}
return true;
}
程序中把三个通道的变成了一个通道、这是建立在每一行数据元素之间在内存里是连续存储的前提下,每个像素的三通道像素按顺序存储。也就是一幅图像数据最开始的三个值,是最左上角的那像素的三个通道的值。
但是这种用法不能够用在行与行之间,因为图像在OpenCV里的存储机制问题,行与行之间可能有空白单元,这些空白单元对图像来说是没有意思的,只是为了在某些架构上更有效率,比如Inter MMX可以更有效的处理那种个数是4或者是8倍数的行。但是我们可以申请一个连续的空间来存储图像,前面提到由create开辟的空间是连续的。
- 这种方法所需时间几乎是前两种方法的近二十分之一
- 而且操作也较为简单
与at对比:
at不需要指定行首指针,ptr需要指定行首指针。
用连续指针来遍历图像
如果图像行与行之间有空白单元,那么指针连续访问方式就没法使用了。但是有些图像是连续的,对这些连续的图像进行来进行这种操作可以提高效率,其实这跟数组那个方式有些类似,但是数组那个支持行与行之间有空白单元的,这个不支持含有空白单元的。
bool colorReduce3(Mat &_src, Mat &_dst, int div)
{
if (_src.empty())
{
cout << "the input image is empty..." << endl;
return false;
}
//指定_dst的属性
_src.copyTo(_dst);
if (_src.isContinuous())
{
const uchar * _src_data = _src.ptr<uchar>(0);
uchar *_dst_data = _dst.ptr<uchar>(0);
for (int j = 0; j < (_src.cols*_src.rows*_src.channels()); j++)
{
*_dst_data++ = *_src_data++ / div*div + div / 2;
}
}
else
{
return false;
}
return true;
}
用指针除了用行列通道数的方法得到图像总的长度外,还可以用指针来索引固定位置的像素:
还可以用下面Mat的两个属性
* Mat.step 返回图像一行像素元素的个数(包括空白元素)。
* Mat.elemSize() 返回一个图像像素的大小。
Mat.at<uchar>(i,j) = Mat.data+i*Mat.step+j*Mat.elemSize();
测试代码:
bool colorReduce4(Mat &_src, Mat &_dst, int div)
{
if (_src.empty())
{
cout << "the input image is empty..." << endl;
return false;
}
//指定_dst的属性
_src.copyTo(_dst);
for (int i = 0; i < _src.rows; i++)
{
for (int j = 0; j < (_src.cols); j++)
{
*(_dst.data + i*_dst.step + j*_dst.elemSize()) = *(_src.data + i*_src.step + j*_src.elemSize()) / div*div + div / 2;
*(_dst.data + i*_dst.step + j*_dst.elemSize()) = *(_src.data + i*_src.step + (j+1)*_src.elemSize()) / div*div + div / 2;
*(_dst.data + i*_dst.step + j*_dst.elemSize()) = *(_src.data + i*_src.step + (j+2)*_src.elemSize()) / div*div + div / 2;
}
}
return true;
}
但是这样的效率并不高,应该它的乘法运算变多了。
迭代器来遍历
可以用下面两种方式来让我们来声明一个迭代器:
MatIterator_<Vec3b> it;
Mat_<Vec3b>::iterator it;
如果迭代器是指向一幅const图像,则可以用下面来两种方式声明:
MatConstIterator_<Vec3b> it;
Mat_<Vec3b>::const_iterator it;
详见代码
//迭代器方式
bool colorReduce5(Mat &_src, Mat &_dst, int div)
{
if (_src.empty())
{
cout << "the input image is empty..." << endl;
return false;
}
//指定_dst的属性
_src.copyTo(_dst);
MatConstIterator_<Vec3b> it_in_data = _src.begin<Vec3b>();
MatConstIterator_<Vec3b> it_in_end = _src.end<Vec3b>();
MatIterator_<Vec3b> it_out_data = _dst.begin<Vec3b>();
MatIterator_<Vec3b> it_out_end = _dst.end<Vec3b>();
while (it_in_data != it_in_end)
{
(*it_out_data)[0] = (*it_in_data)[0] / div*div + div / 2;
(*it_out_data)[1] = (*it_in_data)[1] / div*div + div / 2;
(*it_out_data)[2] = (*it_in_data)[2] / div*div + div / 2;
it_in_data++;
it_out_data++;
}
return true;
}
第二种方法
//迭代器方式
bool colorReduce5(Mat &_src, Mat &_dst, int div)
{
if (_src.empty())
{
cout << "the input image is empty..." << endl;
return false;
}
//指定_dst的属性
_src.copyTo(_dst);
MatConstIterator_<Vec3b> it_in_data = _src.begin<Vec3b>();
//MatConstIterator_<Vec3b> it_in_end = _src.end<Vec3b>();
Mat_<Vec3b>::const_iterator it_in_end = _src.end<Vec3b>();
MatIterator_<Vec3b> it_out_data = _dst.begin<Vec3b>();
//MatIterator_<Vec3b> it_out_end = _dst.end<Vec3b>();
Mat_<Vec3b>::iterator it_out_end = _dst.end<Vec3b>();
while (it_in_data != it_in_end)
{
(*it_out_data)[0] = (*it_in_data)[0] / div*div + div / 2;
(*it_out_data)[1] = (*it_in_data)[1] / div*div + div / 2;
(*it_out_data)[2] = (*it_in_data)[2] / div*div + div / 2;
it_in_data++;
it_out_data++;
}
return true;
}
如果想从第二行开始,直接在起始地址上加上行数
_src.begin()+src.ptr(2);
对比效率
毫无疑问,指针访问连续存储的用时最少,但是这种方式的有缺点。
迭代器的方式:指示某个元素,操作后,指示下一个元素的位置。
应该也是连续访问的其他的方式大径相同,都是得到数据的行首地址与列数量来访问每个像素。应该是其中的取值赋值花费了较多的事件。下面是以上集中方法运行的时间
顺序是从上到下
测试程序文件
二、Mat对象的-创建
使用Mat的构造函数创建矩阵
OpenCV提供了丰富的Mat的构造方法,定义在Mat.hpp文件中,下面例举了部分的函数
Mat();
Mat(int rows, int cols, int type);
Mat(Size size, int type);
Mat(int rows, int cols, int type, const Scalar& s);
Mat(Size size, int type, const Scalar& s);
Mat(int ndims, const int* sizes, int type);
Mat(int ndims, const int* sizes, int type, const Scalar& s);
Mat(const Mat& m);
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(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(int rows, int cols, int type, const Scalar& s);
为例
Mat M(3, 3, CV_8UC3, Scalar(0, 0, 255));
cout << "M=:" << endl << M << endl;
只要指定出相应的类型,均可正确创建图像,就不详细叙述。但是有些函数只是创建了一幅空图像。图像中并没有值,所以需要给图像赋值。这将在下面提到;
它还重载了()、=
,这样它的操作就会变的方便了许多。
A=B; Mat B(A);
这样的把A复制给B;
创建多维数组
//Mat(int ndims, const int* sizes, int type, const Scalar& s);
int sz[3]={2,2,2};
Mat L(3, sz,CV_8UC1,Scalar::all(0));
还需要注意一些其他的:
Mat B(A)
只是复制了A的信息头给B,没有复制A的数据;还有一种赋值操作也是只复制信息头,只复制信息头的操作称为浅复制,Rect()也是浅复制。要把A完全复制给B的话可以用Mat的成员函数B=A.clone()
和A.copyTo(B)
,完全复制的称为深复制。
实用技巧
当测试一个算法是最好先输入一个小矩阵,看下运算结果,如果正确的话,再输入要处理的矩阵。所以要会定义小矩阵的方法
Mat C = (Mat_<double>(3, 3)<<0, -1, 0, -1, 5, -1, 0, -1, 0);
和Mat_<Vec3b> C1(3, 3, Vec3b(0, 255, 0));
输出是
为已经存在的IplImage指针创建信息头
//这种方式适合OpenCV2,在OpenCV3.1上就报错。
IplImage * src = cvLoadImage("F:/Opencv/myimage/51CTO/4.jpg", CV_LOAD_IMAGE_COLOR);
Mat mtx(src);
//OpenCV3上的转换方式看下
IplImage * src = cvLoadImage("F:/Opencv/myimage/51CTO/4.jpg", CV_LOAD_IMAGE_COLOR);
Mat mtx=cvarrToMat(src);//opencv3上的操作
使用create()对Mat类对象初始化
注意,这种方法不能为矩阵设置初值,只是在改变尺寸是为矩阵数据重新分配内存
Mat M;
cout << "M=:" << endl << M << endl;
M.create(4, 4, CV_8UC2);
cout << "M=:" << endl << M << endl;
M = Scalar(0, 255);//赋值
cout << "M=:" << endl << M << endl;
Matlab类型的函数
zeros ones eye
Mat E = Mat::eye(4, 4, CV_8UC1);
cout << "E=:" << endl << E << endl;
Mat Z = Mat::zeros(4, 4, CV_8UC1);
cout << "Z=:" << endl << Z << endl;
Mat O = Mat::ones(4, 4, CV_8UC1);
cout << "O=:" << endl << O << endl;
后记
其是OpenCV中关于Mat对象的访问与创建不止这些,想到Mat支持高维矩阵的创建,和矩阵的多角度的访问,那么它的操作可想而知。这里只是简单的叙述一些,以便在以后的学习过程中使用,等到对C++与OpenCV有一个新的认识后还会回来再看Mat的。