OpenCV学习之路(三)——Mat对象基本操作

矩阵的基本元素表达

对于单通道图像,其元素类型一般为 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);
  }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,179评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,229评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,032评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,533评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,531评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,539评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,916评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,813评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,568评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,654评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,354评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,937评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,918评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,152评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,852评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,378评论 2 342

推荐阅读更多精彩内容