美文网首页
OpenCV C++(二)----图像数字化

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

作者: 肉松饼饼 | 来源:发表于2020-03-08 17:39 被阅读0次

    一、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

    相关文章

      网友评论

          本文标题:OpenCV C++(二)----图像数字化

          本文链接:https://www.haomeiwen.com/subject/ileezctx.html