矩阵的基本元素表达
对于单通道图像,其元素类型一般为 8U(即 8 位无符号整数),当然也可以是 16S、32F 等;这些类型可以直接用 uchar、short、float 等 C/C++语言中的基本数据类型表达。
如果多通道图像,如 RGB 彩色图像,需要用三个通道来表示。在这种情况下,如果依然将图像视作一个二维矩阵,那么矩阵的元素不再是基本的数据类型。
OpenCV 中有模板类 Vec,可以表示一个向量。OpenCV 中使用 Vec 类预定义了一些小向量,可以将之用于矩阵元素的表达。
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<int, 2> Vec2i;
typedef Vec<int, 3> Vec3i;
typedef Vec<int, 4> Vec4i;
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;
例如 8U 类型的 RGB 彩色图像可以使用 Vec3b,Vec3b对应的通道顺序是blue、green、red的uchar类型数据。3 通道 float 类型的矩阵可以使用 Vec3f。
对于 Vec 对象,可以使用[]符号如操作数组般读写其元素,如:
Vec3b color; //用 color 变量 述一种 RGB 颜色
color[0]=255; //B 分量
color[1]=0; //G分量
color[2]=0; //R分量
像素值的读写
1.at()函数
函数 at()来实现读去矩阵中的某个像素,或者对某个像素进行赋值操作。下
面两行代码演示了 at()函数的使用方法:
uchar value = grayim.at<uchar>(i,j);//读出第i行第j列像素值
grayim.at<uchar>(i,j)=128; //将第i行第j列像素值设置为128
对图像进行遍历,如下:
Mat colorim(600, 800, CV_8UC3);
//遍历所有像素,并设置像素值
for( int i = 0; i < colorim.rows; ++i) {
for( int j = 0; j < colorim.cols; ++j ){
Vec3b pixel;
pixel[0] = i%255; //Blue
pixel[1] = j%255; //Green
pixel[2] = 0; //Red
colorim.at<Vec3b>(i,j) = pixel;
}
}
需要注意的是,如果要遍历图像,并不推荐使用 at()函数。使用这个函数的优点是代码的可读性高,但是效率并不是很高。
2.使用迭代器
如果你熟悉 C++的 STL 库,那一定了解迭代器(iterator)的使用。迭代器可以方便地遍历所有元素。Mat 也增加了迭代器的支持,以便于矩阵元素的遍历。下面的例程功能跟上一节的例程类似,但是由于使用了迭代器,而不是使用行数和列数来遍历,所以这儿没有了 i 和 j 变量,图像的像素值设置为一个随机数。
Mat grayim(600, 800, CV_8UC1);
Mat colorim(600, 800, CV_8UC3);
//遍历所有像素,并设置像素值
MatIterator_<uchar> grayit, grayend;
for( grayit = grayim.begin<uchar>(), grayend = grayim.end<uchar>(); grayit != grayend; ++grayit) {
*grayit = rand()%255;
}
//遍历所有像素,并设置像素值
MatIterator_<Vec3b> colorit, colorend;
for( colorit = colorim.begin<Vec3b>(), colorend = colorim.end<Vec3b>(); colorit != colorend; ++colorit) {
(*colorit)[0] = rand()%255; //Blue
(*colorit)[1] = rand()%255; //Green
(*colorit)[2] = rand()%255; //Red
}
3.通过数据指针
如果你非常注重程序的运行速度,那么遍历像素时,建议使用指针。下面的例程演示如何使用指针来遍历图像中的所有像素。
Mat grayim(600, 800, CV_8UC1);
Mat colorim(600, 800, CV_8UC3);
//遍历所有像素,并设置像素值
for( int i = 0; i < grayim.rows; ++i) {
//获取第 i 行首像素指针
uchar * p = grayim.ptr<uchar>(i);
//对第 i 行的每个像素(byte)操作
for( int j = 0; j < grayim.cols; ++j ) {
p[j] = (i+j)%255;
}
}
//遍历所有像素,并设置像素值
for( int i = 0; i < colorim.rows; ++i) {
//获取第 i 行首像素指针
Vec3b * p = colorim.ptr<Vec3b>(i);
for( int j = 0; j < colorim.cols; ++j ) {
p[j][0] = i%255; //Blue
p[j][1] = j%255; //Green
p[j][2] = 0; //Red
}
}
选取图像局部区域
Mat 类 供了多种方便的方法来选择图像的局部区域。使用这些方法时需要注意,这些方法并不进行内存的复制操作。如果将局部区域赋值给新的 Mat 对象,新对象与原始对象共用相同的数据区域,不新申请内存,因此这些方法的执行速度都比较快。
1.单行或单列选择
取矩阵的一行或者一列可以使用函数 row()或 col()。函数的声明如下:
Mat Mat::row(int i) const
Mat Mat::col(int j) const
参数 i 和 j 分别是行标和列标。例如取出 A 矩阵的第 i 行可以使用如下代码:
Mat line = A.row(i);
2.用Range选择多行或多列
Range 是 OpenCV 中新增的类,该类有两个关键变量 star 和 end。Range 对象可以用来表示矩阵的多个连续的行或者多个连续的列。其表示的范围为从 start到 end,包含 start,但不包含 end。Range 类的定义如下:
class Range {
public:
...
int start, end;
};
Range 类还 供了一个静态方法 all(),这个方法的作用如同 Matlab 中的“:”,表示所有的行或者所有的列。
Mat A = Mat::eye(10, 10, CV_32S); //创建一个单位阵
Mat B = A(Range::all(), Range(1, 3)); // 取第1到3列(不包括3)
Mat C = B(Range(5, 9), Range::all()); // 取B的第5至9行(不包括9),其实等价于 C = A(Range(5, 9), Range(1, 3))
3.感兴趣区域
从图像中 取感兴趣区域(Region of interest)有两种方法,一种是使用构造
函数,如下例所示:
Mat img(Size(320,240),CV_8UC3); //创建宽度为 320,高度为 240 的 3 通道图像
Mat roi(img, Rect(10,10,100,100)); //roi 是表示 img 中 Rect(10,10,100,100)区域的对象
除了使用构造函数,还可以使用括号运算符,如下:
Mat roi2 = img(Rect(10,10,100,100));
当然也可以使用 Range 对象来定义感兴趣区域,如下:
Mat roi3 = img(Range(10,100),Range(10,100)); //使用括号运算符
Mat roi4(img, Range(10,100),Range(10,100)); //使用构造函数
4.取对角线元素
矩阵的对角线元素可以使用 Mat 类的 diag()函数获取,该函数的定义如下:
Mat Mat::diag(int d) const
参数 d=0 时,表示取主对角线;当参数 d>0 是,表示取主对角线下方的次对角线,如 d=1 时,表示取主对角线下方,且紧贴主多角线的元素;当参数 d<0 时,表示取主对角线上方的次对角线。
如同 row()和 col()函数,diag()函数也不进行内存复制操作,其复杂度也是 O(1)。
Mat 表达式
如果矩阵 A 和 B 大小相同,则可以使用如下表达式:
C = A + B + 1;
其执行结果是 A 和 B 的对应元素相加,然后再加 1,并将生成的矩阵赋给 C变量。
下面给出 Mat 表达式所支持的运算。下面的列表中使用 A 和 B 表示 Mat 类型的对象,使用 s 表示 Scalar 对象,alpha 表示 double 值。
- 加法,减法,取负:A+B,A-B,A+s,A-s,s+A,s-A,-A
- 缩放取值范围:A*alpha
- 矩阵对应元素的乘法和除法: A.mul(B),A/B,alpha/A
- 矩阵乘法:A*B (注意此处是矩阵乘法,而不是矩阵对应元素相乘)
- 矩阵转置:A.t()
- 矩阵求逆和求伪逆:A.inv()
- 矩阵比较运算:A cmpop B,A cmpop alpha,alpha cmpop A。此处 cmpop可以是>,>=,==,!=,<=,<。如果条件成立,则结果矩阵(8U 类型矩阵)的对应元素被置为 255;否则置 0。
- 矩阵位逻辑运算:A logicop B,A logicop s,s logicop A,~A,此处 logicop可以是&,|和^。
- 矩阵对应元素的最大值和最小值:min(A, B),min(A, alpha),max(A, B),max(A, alpha)。
- 矩阵中元素的绝对值:abs(A)
- 叉积和点积:A.cross(B),A.dot(B)
Mat_类
Mat_类是对 Mat 类的一个包装,其定义如下:
template<typename _Tp> class Mat_ : public Mat {
public:
//只定义了几个方法
//没有定义新的属性
};
在读取矩阵元素时,以及获取矩阵某行的地址时,需要指定数据类型。这样首先需要不停地写“<uchar>”,让人感觉很繁琐,在繁琐和烦躁中容易犯错,如上面代码中的错误,用 at()获取矩阵元素时错误的使用了 double 类型。这种错误不是语法错误,因此在编译时编译器不会醒。在程序运行时,at()函数获取到的不是期望的(i,j)位置处的元素,数据已经越界,但是运行时也未必会报错。这样的错误使得你的程序忽而看上去正常,忽而弹出“段错误”,特别是在代码规模很大时,难以查错。
Mat M(600, 800, CV_8UC1);
for(int i = 0; i < M.rows; ++i){
uchar * p = M.ptr<uchar>(i); //获取指针时需要指定类型
for(int j = 0; j < M.cols; ++j){
double d1 = (double) ((i+j)%255);
//用 at()读写像素时,需要指定类型
M.at<uchar>(i,j) = d1;
//下面代码错误,应该使用 at<uchar>()
//但编译时不会 醒错误
//运行结果不正确,d2 不等于 d1
double d2 = M.at<double>(i,j);
}
}
使用 Mat_类,可以在变量声明时确定元素的类型,访问元素时不再需要指定元素类型,即使得代码简洁,又减少了出错的可能性。如下:
Mat_<uchar> M1 = (Mat_<uchar>&)M; //在变量声明时指定矩阵元素类型
for( int i = 0; i < M1.rows; ++i) {
uchar * p = M1.ptr(i); //不需指定元素类型,语句简洁
for( int j = 0; j < M1.cols; ++j ) {
double d1 = (double) ((i+j)%255);
//直接使用 Matlab 风格的矩阵元素读写,简洁
M1(i,j) = d1;
double d2 = M1(i,j);
}
}