OpenCV C++(二)----图像数字化

一、Mat类

MatMatrix的缩写,代表矩阵或者数组的意思。该 类的声明在头文件opencv2\core\core.hpp中, 所以使用Mat类时要引入该头文件。

1.1、Mat类的构造函数

构造Mat对象相当于构造了一个矩阵(数组),需要四个基本要素:行数(高)、列数(宽)、通道数及其数据类型。

    Mat(int rows, int cols, int type);

其中,

  • rows代表矩阵的行数
  • cols代表矩阵的列数
  • type代表类型, 包括通道数及其 数据类型, 可以设置为CV_8UC(n)CV_8SC(n)CV_16SC(n)CV_16UC(n)CV_32SC(n)CV_32FC(n)CV_64FC(n)
    其中8U8S16S16U32S32F64F前面的数字代表Mat中每一个数值所占的bit数, 而 1byte=8bit, 所以, 32F就是占4字节的float类型, 64F是占8字节的doule类型, 32S是占4字 节的int类型,8U是占1字节的uchar类型, 其他的类似;
    C(n)代表通道数,当n=1时, 即构造单通道矩阵或称二维矩阵, 当n>1时, 即构造多通道矩阵即三维矩阵, 直观上就是n个二维矩阵组成的三维矩阵。
    Mat(Size size, int type);

其中,需要注意的是, Size的第一个元素是矩阵的列数(宽),第二个元素是矩阵的行数(高),即先存 宽, 再存高。即Size(int cols,int rows)

Mat m;
m.create(2,3,CV_32FC1);
m.create(Size(3,2),CV_32FC1);

1.2、初始化Mat类

Mat o=Mat::ones(2,3,CV_32FC1);
Mat m=Mat::zeros(Size(3,2),CV_32FC1);

Mat m=(Mat_<int>(2,3)<<1,2,3,4,5,6);

1.3、获取单通道Mat的基本信息

1. 使用成员变量rows和 cols获取矩阵的行数和列数
   //构造矩阵
   Mat m=(Mat_<int>(3,2)<<1,2,3,4,5,6);
   //矩阵的行数
   cout<<"行数:"<<m.rows<<endl;
   //矩阵的列数
   cout<<"列数:"<<m.cols<<endl;
2. 使用成员函数size() 获取矩阵的尺寸
   Size size=m.size();
   cout<<"尺寸:"<<size<<endl;
3. 使用成员函数 channels() 得到矩阵的通道数
   cout<<"通道数:"<<m.channels()<<endl;
4. 使用成员函数 total()得到矩阵的行数乘以列数, 即面积。 注意和通道数无关, 返回的不是矩阵中数据的个数
   cout<<"面积:"<<m.total()<<endl;
5. 使用成员变量 dims 得到矩阵的维数。 显然对于单通道矩阵来说就是一个二维矩阵, 对于多通道矩 阵来说就是一个三维矩阵。
   cout<<"维数:"<<m.dims<<endl; 

1.4、访问单通道Mat对象中的值

1. 利用成员函数at
   //构造单通道矩阵
   Mat m=(Mat_<int>(3,2)<<11,22,33,44,55,66);
   //通过for循环打印M中的每一个值
   for(int r=0;r<m.rows;r++)
   {
    for(int c=0;c<m.cols;c++)
    {
           cout<<m.at<int>(r,c)<<",";//第r行第c列的值
           cout<<m.at<int>(Point(c,r))<<",";//等价于上面一行代码
    }
    cout<<endl'
   }
2. 利用成员函数ptr

对于Mat中的数值在内存中的存储, 每一行的值是存储在连续的内存区域中的, 通过成员函数ptr获得指向每一行首地址的指针。 仍以“利用成员函数at”部分的m存储为例, m中所有的值在内存中的存储方式如图2-1所示, 其中如果行与行之间的存储是有内存间隔的, 那么间隔也是相等的。

   for(int=0;r<m.rows;r++)
   {
       //得到矩阵m的第r行行首的地址
       const int *ptr=m.ptr<int>(r);
       //打印第r行的所有值
       for(int c=0;c<m.cols;c++)
       {
           cout<<ptr[c]<<",";
       }
       cout<<endl;
   }
1. 利用成员函数 isContinuous和ptr

每一行的所有值存储在连续的内存区域中, 行与行之间可能会有间隔, 如果isContinuous返回值为true, 则代表行与行之间也是连续存储的, 即所有的值都是连续存储的。

   if(m.isContinuous())
   {
       //得到矩阵m的第一个值的地址
       int *ptr=m.ptr<int>(0);
       for(int n=0;c<m.rows*m.cols;n++)
       {
           cout<<ptr[n]<<",";
       }
   }
2. 利用成员变量step和data

对于单通道矩阵来说,step[0]代表每一行所占的字节数,而如果有间隔的话, 这个间隔也作为字节数的一部分被计算在内;step[1]代表每一个数值所占的字节数,data是指向第一个数值 的指针, 类型为uchar。 所以, 无论哪一种情况, 如访问一个int类型的单通到矩阵的第r行 第c列的值, 都可以通过以下代码来实现。

     *((int*)(m.data+m.step[0]*r+c*m.step[1])) 

1.5、向量类Vec

默认是列向量

   //构造一个长度为3,数据类型为int并且初始化为11、22、33的列向量
   Vec<int,3> vi(11,22,33);
   cout<<"向量的行数"<<vi.rows<<endl;
   cout<<"向量的列数<<vi.cols<<endl;
   cout<<"访问滴0个元素:"<<vi[0]<<endl;

OpenCV为向量类的声明取了一个别名,在matx.hpp 401行开始 例如:

   typedef Vec<uchar, 2> Vec2b;
   typedef Vec<uchar, 3> Vec3b;
   typedef Vec<uchar, 4> Vec4b;
   
   typedef Vec<int, 2> Vec2i;
   typedef Vec<int, 3> Vec3i;
   ...

单通道矩阵的每一个元素都是一个数值, 多通道矩阵的每一个元素都可以看作一个向量。

1.6、构造多通道的Mat对象

   Mat mm=(Mat_<Vec3f>(2,2)<<Vec3f(1,1,1),Vec3f(2,2,2),Vec3f(3,3,3),Vec3f(4,4,4));
   //打印第0行第0列的元素值
   int r=0;
   int c=0;
   cout<<mm.at<Vec3f>(r,c)<<endl;

其余同单通道方法,只是类型变成了向量Vec
(1)分离通道

   vector<Mat> planes;
   split(mm,planes);

(2)合并通道

   //三个单通道矩阵
   Mat plane0=(Mat_<int>(2,2)(1,2,3,4);
   Mat plane1=(Mat_<int>(2,2)(11,12,13,14);
   Mat plane2=(Mat_<int>(2,2)(21,22,23,24);
   //用三个单通道矩阵初始化一个数组
   Mat plane[]={plane0,plane1,plane2};
   Mat mat;
   merge(plane,3,mat);
   //将三个单通道矩阵一次放入vector容器中
   vector<Mat> plane;
   plane.push_back(plane0);
   plane.push_back(plane1);
   plane.push_back(plane2);
   Mat mat;
   merge(plane,mat);

1.7、获得Mat中某一区域的值

1. 使用成员函数row(i) 或 col(j) 得到矩阵的第i行或者第 j列
2. 使用成员函数rowRange或 colRange得到矩阵的连续行或者连续列
          Range(int _start,int _end);

这是一个左闭右开的序列[_start, _end),比如Range(2, 5) 其实产生的是2、 3、 4 的序列,不包括5, 常用作rowRangecolRange的输入参数,从而访问矩阵中的连续行或者连续列

          Mat r_range=mm.rowRange(Range(2,4));
          //Mat r_range=mm.rowRange(2,4);等价于上面
          for(int r=0;r<r_range.rows;r++)
          {
              for(int c=0;c<r_range.cols;c++)
              {
                  cout<<r_range.at<int>(r,c)<<",";
              }
              cout<<endl;
          }

需要特别注意的是, 成员函数rowcolrowRangecolRange返回的矩阵其实是指向原矩阵的;有时候, 我们只访问原矩阵的某些行或列, 但是不改变原矩阵的值,需要使用复制的方法

3. 使用成员函数 clone和copy To
      Mat r_range=mm.rowRange(2,4).clone();
      Mat c_range=mm.colRange(1,3).copyTo(c_range);
4. 使用 Rect类
  Rect的构造函数
      Rect_(_Tp _x, _Tp _y, _Tp _width, _Tp _height);
      Rect_(const Rect_& r);
      Rect_(const Point_<_Tp>& org, const Size_<_Tp>& sz);
      Rect_(const Point_<_Tp>& pt1, const Point_<_Tp>& pt2);
      Mat ROI1=mm(Rect(2,1,2,2));
      Mat roi2=mm(Rect(Point(2,1),Size(2,2)));
      Mat roi3=mm(Rect(Point(2,1),Point(3,2)));

但是与使用colRangerowRange类似, 这样得到的矩形区域是指向原矩阵的, 要改变roi中的值, matrix也会发生变化, 如果不想这样, 则仍然可以使用clone或者copyTo

二、矩阵的运算

2.1、加法运算

矩阵的加法就是两个矩阵对应位置的数值相加

Mat src1=(Mat_<uchar>(2,2)<<11,22,33,60);
Mat src2=(Mat_<uchar>(2,2)<<191,192,193,204);
Mat dst=src1+src2;

注意:

  • 60+204=264,但是,实际打印出来的值是255,因为两个矩阵的数据类 型都是uchar, 所以用“+”运算符计算出来的和也是uchar类型的, 但是uchar类型范围的最大值是255, 所以只好将264截断为255。
  • 两个Mat的数据类型必须是一 样的,否则会报错,也就是用“+”求和是比较严格的 。
  • 一个数值与一个Mat对象相 加, 也可以使用“+”运算符, 但是无论这个数值是什么数据类型, 返回的Mat的数据类型 都与输入的Mat相同 。

为了弥补“+”运算符的这两个缺点, 我们可以使用OpenCV提供的另一 个函数

void add(InputArray src1, InputArray src2, OutputArray dst,InputArray mask = noArray(), int dtype = -1);

使用add函数时, 输入矩阵的数据类型可以不同, 而输出矩阵的数据类型 可以根据情况自行指定。 需要特别注意的是, 如果给dtype赋值为-1, 则表示dst的数据类型和src1src2是相同的, 也就是只有当src1src2的数据类型相同时,才有可能令 dty pe=-1,否则仍然会报错。

Mat dst;
add(src1,src2,dst,Mat(),CV_64FC1);

2.2、减法运算

矩阵的减法与加法类似

Mat dst=src1-src2;

注意:

  • 输出值不会最小为0,这是 因为src1src2均是uchar类型的, 所以返回的dst也是uchar类型的;而uchar类型的最小范围是0, 所以会将小于0的数值截断为0。
  • Mat对象与一个数值相减, 也可以使用“-”运算符。

当然, 也存在与“+”运算符 一样的不足, OpenCV提供的函数:

void subtract(InputArray src1, InputArray src2, OutputArray dst,
                           InputArray mask = noArray(), int dtype = -1);

可以实现不同的数据类型的Mat之间做减法运算, 其与add函数类似。

2.3、点乘运算

矩阵的点乘即两个矩阵对应位置的数值相乘。

Mat dst=src1.mul(src2);

注意

从打印结果就可以看出,也是对大于255的数值做了截断处理。 所以为了不损失精度,可以将两个矩阵设置为intfloat等数值范围更大的数据类型。

对于Mat的点乘, 也可以利用OpenCV提供的函数:

void multiply(InputArray src1, InputArray src2,
                           OutputArray dst, double scale = 1, int dtype = -1);

这里的dst=sclae*src1*src2, 即在点乘结果的基础上还可以再乘以系数scale

2.4、点除运算

点除运算与点乘运算类似, 是两个矩阵对应位置的数值相除。

Mat dst=src2/src1;

注意

  • 除数为0没有意义,但是OpenCV在 处理这种分母为0的除法运算时,默认得到的值为0。
  • 用一个数值与Mat对象相除也可以使用“/”运算符, 且返回的Mat的数据类型与输入的Mat的数据类型相同, 与输入数值 的数据类型是没有关系的。

对于Mat的点除, 也可以利用OpenCV提供的函数:

divide(InputArray src1, InputArray src2, OutputArray dst,
                         double scale = 1, int dtype = -1);

2.5、乘法运算

相当于卷积

Mat dst=src1*src2;

注意:

  • 对于Mat对象的乘法, 需要注意两个Mat只能同时是float或者double类型, 对于其他数据类型的矩阵做乘法会报错。
  • 两个双通道矩阵也可以相乘,这里是把Mat对象当做了复数矩阵,其中第一个通道存放的是所有值的实部,第二个通道存放的是对应的每一个虚部,也就是将Vec2f看作一个复数, 比如Vec2f(1, 2) 可以看作1+2i

对于Mat的乘法, 还可以使用OpenCV提供的gemm函数来实现。

void gemm(InputArray src1, InputArray src2, double alpha,
                       InputArray src3, double beta, OutputArray dst, int flags = 0);

注意:gemm也只能接受CV_32FC1CV_64FC1CV_32FC2CV_64FC2数据类型的Mat

该函数通过flags控制src1src2src3是否转置来实现矩阵之间不同的运算, 当将flags设置为不同的参数时, 输出矩阵为:
当然,flags可以组合使用, 比如需要src2src3都进行转置, 则令flags=GEMM_2_T+GEMM_3_T

2.6、其他运算

开平方运算

void sqrt(InputArray src, OutputArray dst);

注意:sqrt的输入矩阵的数据类型只能是 CV_32F或者CV_64F

幂指数运算

void pow(InputArray src, double power, OutputArray dst);

三、灰度图像数字化

Mat imread( const String& filename, int flags = IMREAD_COLOR );   
void imshow(const String& winname, InputArray mat);

四、彩色图像数字化

灰度图像的每一个像素都是由一个数字量化的, 而彩色图像的每一个像素都是由三个数字组成的向量量化的。 最常用的是由RGB三个分量来量化的, RGB模型使用加 性色彩混合以获知需要发出什么样的光来产生给定的色彩, 源于使用阴极射线管(CRT) 的彩色电视, 具体色彩的值用三个元素的向量来表示, 这三个元素的数值分别代表三种基色: Red、Green、Blue的亮度。 假设每种基色的数值量化成m=2^n个数, 如同8位灰度 图像一样, 将灰度量化成28=256个数。 RGB图像的红、绿、蓝三个通道的图像都是一张8 位图, 因此颜色的总数为2563 =16777216, 如(0, 0, 0) 代表黑色,(255, 255, 255) 代表白色, (255, 0, 0) 代表红色。

对于彩色图像的每一个方格, 我们可以理解为一个Vec3b。 需要注意的是, 每一个像素的向量不是按照RGB分量排列的, 而是按照BGR顺序排列的, 所以通过split函数分离通道后, 先后得到的是BGR通道。

Mat img=imread("apple.jpg",CV_LOAD_IMAGE_GRAYSCALE);
if(img.empty())
{
    return -1;
}
imshow("BGR",img);
vector<Mat> planes;
split(img,planes);
imshow("B",planes[0]);
imshow("G",planes[1]);
imshow("R",planes[2]);
waitKey(0);

在OpenCV中实现将彩色像素(一个向量) 转化为灰度像素(一个数值) 的公式如 下:

image.png

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

推荐阅读更多精彩内容