• OpenCV4(C++) —— Mat类



    前言

    在python的opencv中,也就是导入:import cv2。使用的是numpy和内置ndarray来存储和处理图像数据。而对于C++的opencv是没有ndarray的,也没有numpy库,而是使用cv::Mat类来表示图像数据与进行处理的。

    python是一种动态类型语言,无需显示的指定变量的类型。直接使用imread返回的就是ndarray对象,后续使用numpy库可以直接对其进行相关图像操作。

    import  cv2
    import  numpy as np
    image = cv2.imread("lena.jpg")
    print(type(image)) # 
    print(image.shape) # h,w,c
    
    • 1
    • 2
    • 3
    • 4
    • 5

    C++是一种静态类型语言,任何变量都需要显示地指定一个数据类型。而对于图像这种多维数组来说,就定义了Mat类来管理。

    #include   
    #include  
    using namespace std;
    
    int main()
    {
        cv::Mat image = cv::imread("lena.jpg");
        int h = image.rows;  // row为高度
        int w = image.cols;  // col为宽度
        int ch = image.channels();  // 通道
        cout << h << " " << w <<" "<< ch << endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    一、初识Mat类

      Mat类分为矩阵头指向存储数据的矩阵指针。矩阵头中包含矩阵的尺寸、存储方法、地址和引用次数等,只存放这几个固定的数据类型,所以矩阵头所占空间是固定的。图像复制和传递过程中主要的开销是存放矩阵数据。
      一般的赋值操作,只是复制矩阵头和存放矩阵数据的指针,两者还是指向同一矩阵数据(浅拷贝),若需克隆一份新的矩阵数据,则要深拷贝。(在python中也是如此)

    int main()
    {
        cv::Mat image1   // 矩阵头
        image1 = cv::imread("lena.jpg");  // 矩阵指针将指向该矩阵像素
        cv::Mat image2 = image1;  // 浅拷贝
        cv::Mat image3 = image1.clone();  // 深拷贝
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    注:Mat类利用自动内存管理技术解决了内存自动释放的问题,当变量不再需要时立即释放内存。
      当发生A浅拷贝B时,两种指向同一矩阵数据。只删除A,B仍然指向该矩阵数据;当A、B都删除时,才会释放该内存。能够这么做的原因是上面说到的矩阵头中的引用次数,复制一次,引用次数+1,删除一次,引用次数-1,当引用次数为0时才会释放内存(发现了吗,和智能指针shared_ptr一样的原理)。

    (2)图像数据类型
      C++中,内置的数据类型有int,float,char等。我们指定,不同的编译平台,位数会发生变化。所以OpenCV中根据数值变量存储位数长度重新命名了新的数据类型,如下表:

    数据类型所代表类型与取值范围
    CV_8U8位无符号整数(0-——255)
    CV_8S8位符号整数(-128——127)
    CV_16U16位无符号整数(0——65535)
    CV_16S16位符号整数(-32768——32767)
    CV_32S32位无符号整数
    CV_32F32位浮点整数

      图像的单个灰度像素值为(0-255),所以CV_8U是最常用的。但图像还有RGB通道之分,所以OpenCV还定义了通道标识符:C1、C2、C3、C4,来分别表示单通道、双通道、三通道和四通道。为此,完整的图像数据类型为:CV_8UC3,表示8位的三通道数据

    二、Mat类的创建(构造)与赋值

    1、类的常规三种构造(默认、有参、拷贝)

    (1)默认构造

      Mat可以进行默认构造,这种方式不需要输入任何的参数,在后续给变量赋值的时候会自动判断矩阵的类型与大小,实现灵活的存储,常用于存储读取的图像数据或某个函数运算的输出结果

    cv::Mat image;
    image = cv::imread("lena.jpg"); 
    
    cv::Mat zero_roi;
    cv::inRange(mask2, 0, 0, zero_roi); //cv::inRange(输入图像,下界,上界,输出图像);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    (2)输入矩阵尺寸和数据类型——有参构造

    常规语法:

    //cv::Mat image(rows, cols, type)
    cv::Mat image(600, 400, CV_8UC3);  // 创建一个高600,宽400,3通道的8位无符号矩阵数据
    
    • 1
    • 2

    还有一种方式是采用cv::Size()进行赋值
    cv::Size 类可以方便地指定图像的尺寸,例如创建一个具有特定宽度和高度的图像,或者在图像处理过程中获取图像的尺寸信息。但要注意,Size()里的高、宽与常规语法是相反的,宽在前,高在后。

    //cv::Size size(cols, rows);
    cv::Size size(400, 600);
    cv::Mat image(size, CV_8UC3);  // 创建一个高600,宽400,3通道的8位无符号矩阵数据
    
    • 1
    • 2
    • 3

    (3)利用已有矩阵构造——拷贝构造

      默认的拷贝是浅拷贝,若想创建一份不会影响到原数据的,需要用clone()来进行深拷贝,常用在函数参数上;如何还想指定尺寸,可以用矩阵指针的方式,常用于调整图像尺寸

    cv::Size size(400, 600);
    cv::Mat image1(size, CV_8UC3);
    cv::Mat image2 = image1;  //浅拷贝
    cv::Mat image3 = image1.clone(); // 深拷贝
    //cv::Mat image3(image1.clone) 拷贝构造的另一种写法,隐式构造
    
    //当要对图片传入函数中进行处理,而又不会影响到原图
    cv::Mat func(cv::Mat& images)
    {
    	cv::Mat img_input;
    	ima_input = images.clone();
    	......
    	return xxx;
    }
    
    // 当要调整图像尺寸为520×520时(同样为深拷贝)
    cv::Mat image4(520, 520, CV_8UC3, image1.data)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    利用已有矩阵构造,还可以进行类似截图的操作,可以采用cv::Range或cv::Rect。注意cv::Rect里的高和宽与默认相反,先定义宽,再定义高。

    // 1、Range指定行(高)和列(宽)的范围。下例是0到299行,共300行;0到399列,共400列
    cv::Mat image2 = image1(cv::Range(0, 300), cv::Range(0, 400)); 
    // cv::Mat image2(image1, (cv::Range(0, 300), cv::Range(0, 400)); 隐式构造
    
    //2、Rect指定左上角坐标和矩形的宽度和高度来定义区域
    cv::Mat image2(image1, cv::Rect(0, 0, 400, 300));//左上角左边(0,0),宽400,高300
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    2、赋值方式

    上面讲述了多种构造方式,但只是创建了对象,还没有数据值赋给它。OpenCV4给予了多种赋值方式。

    (1)构造时给每个通道赋值

      在构造时,加上从cv::Scalar()直接赋值。这种方式是赋给一个通道的所有相同数据,如单通道时cv::Scalar(0),表示全部赋值0;三通道时,cv::Scalar(0,0,0),表示三个通道全都赋值为0。另外一提,彩色图片在OpenCV中默认三通道的顺序是B、G、R。但是,在用这种方式赋值时,实际应用中大多数都是为了得到一个全黑或全白的矩阵,来用作后续处理。

    cv::Mat image(600, 400, CV_8UC3, cv::Scalar(0,0,0));  // 全黑的3通道
    cv::Mat image(600, 400, CV_8UC3, cv::Scalar(255, 255, 255));   // 全白的3通道
    
    • 1
    • 2

    (2)给通道内每个元素赋值

       (1)中的方式是赋给一个通道(矩阵)相同的数据,也可以给矩阵内每一个元素进行赋值,如枚举、循环、数组。枚举的个数要与矩阵元素个数相同,所以此方法一般用在矩阵数据比较少的情况。但一般图像数据都较大,所以在实际应用中很少使用。

    (3)使用成员函数赋值

       Mat类中,自定义了可以初始化的矩阵,如eys,ones,zeros,diag,来生成单位矩阵、对角矩阵等。

    前面说过,实际应用中大多数都是为了得到一个全黑或全白的矩阵,来用作后续处理。所以常用的是ones

    cv::Mat mask = cv::Mat::zeros(cv::Size(400, 600), CV_8UC3); // 注意Size里的顺序(宽,高)
    
    • 1

    全黑的mask的作用:(1)因为像素都是0,所以跟其它任何像素作加法运算,得到的都是其它像素的原值,所以可用于移植其他图像
    例:假设原始图片过大,先作了切割,再处理,最后要把处理结果图拼接起来。image是一个部分结果图,可以设定一个和原图大小一样的全黑mask作为底板,来依次放进部分结果图,下面是一个简单示例:

        cv::Mat image(100, 100, CV_8UC3, cv::Scalar(255, 255, 255));  // 用全白image来当作部分结果图
        cv::Mat mask = cv::Mat::ones(cv::Size(640, 400), CV_8UC3);
        cv::Mat roi = mask(cv::Rect(0, 0, 100, 100));  // 使用Rect获得指定区域roi(左上角(0,0),宽高(100,100))
        image.copyTo(roi);  // 复制image到roi中(深拷贝)
        cv::imshow("原图", mask);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在这里插入图片描述
    (2)像素为0的另一个好处是方便进行逻辑运算。如求与运算(只有两个数都为1,结果才为1。所以当存在像素为0时,结果一定为0),可以将其他像素全置为0。这种操作在处理二值mask时很常见。

    // 逻辑运算的参数都一样:前两个参数是要进行运算的两个图像矩阵,第三个参数是结果矩阵,第4个参数是设置范围,一般默认即可
     void cv::bitwise_and(src1, src2, dst)  //像素求与运算
     void cv::bitwise_or(src1, src2, dst)  //像素求或运算
     void cv::bitwise_xor(src1, src2, dst)  //像素求异或运算
     void cv::bitwise_not(src1, src2, dst)  //像素求非运算
     
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    此外,深度学习中使用torch张量也可以创建该类型的矩阵。如: auto zero_tensor = torch::zeros_like(index); //创建一个与index大小相同的全零张量zero_tensor。

    三、Mat类的常用属性

    属性含义
    cols矩阵列数(图像宽度)
    rows矩阵行数(图像高度)
    channels()通道数
    total()矩阵中元素的个数
    step以字节为单位的矩阵的有效宽度
    elemSize()每个元素的字节数 (默认情况下像素值是char类型,故灰度图=1字节,彩色图=3字节)
    data指向矩阵数据起始位置的指针(通常用于复制、传递图像数据)
    ptr()data是指向整个矩阵数据的指针,ptr()可以选择特定行的数据 ,常用于有映射关系的复制操作

    上面的“元素”并不是完全等于像素。在单通道图片中,一个元素等于一个像素;但若在多通道(如三通道中),一个元素=B、G、R三个像素,如下图。(注:Mat类的矩阵是二维的,所以计算机系统会把三通道的图片压缩成二维形式,如图所示,存储完一个元素的BGR三个像素值后,才会接着存放下一个元素
    在这里插入图片描述
    值得注意:灰度图的总像素=total();彩色图像的总像素 = total()×channels;灰度图step=cols,彩色图step=cols×3。
    对于cols 和 row,应该是系统做过处理,无论什么通道的图像,这两个就是图片的宽和高。

    3.1 如何访问Mat类的元素

    常用访问方式有:(1)at方法;(2)指针;(3)迭代器;(4)矩阵元素的地址定位

    (2) 指针Ptr

       ptr< uchar> () 是一个成员函数,属于OpenCV库的cv::Mat类。该函数返回一个指向图像某一行首元素的指针,可以方便地访问和修改图像中的每个像素。

    uchar* cv::Mat::ptr<uchar>(int y) // y表示的是行的索引。返回值是一个指向第y行的uchar型指针
    
    • 1

    使用Ptr确定到每个像素

    const uchar* dst = dst1.ptr<uchar>(0, 2); // 第0行第2个像素(从0开始数)
    
    // 遍历每一个像素
        for (int i = 0; i < img.rows; ++i)
        {
            uchar* row_ptr = img.ptr<uchar>(i);
            for (int j = 0; j < img.cols; ++j)
            {
                uchar pixel = row_ptr[j];
                cout << (int)pixel<< " ";
                cout << "j=" << j << endl;
            }
            cout << endl;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

       上面这种遍历方式只适用于单通道CV_8UC1的图片,因为在遍历cols时,没有考虑通道,而前面说过计算机存储彩色图是是按照BGR像素连续存储的,所以若按照 j < img.cols ,实际上是没有遍历完的。下图展示了一张图片的首行遍历结果,可以看到,会依次遍历BGR三个像素。
    在这里插入图片描述
    在这里插入图片描述
       想要遍历多通道图片的所有像素,最简单的方法就是将 j < img.cols 改为 j < img.cols *img.channels() 。但是通常情况下,不希望循环框架发生改变,所以一般是从内部处理。常见的(彩色图)逐元素复制拷贝操作参考如下:

    const uchar* dst = dst1.ptr<uchar>(0, 2); // 第0行第2个像素(从0开始数)
    
    // 遍历每一个像素
        for (int i = 0; i < img.rows; ++i)
        {
            uchar* row_ptr = img.ptr<uchar>(i);
            uchar* dst_ptr =  dstImg.ptr<uchar>(i);
            for (int j = 0; j < img.cols; ++j)
            {
    			std::memcpy(dst_ptr + col * img_input.channels(), src_ptr + col * img_input.channels(), sizeof(uchar) * img_input.channels());
            }
    
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
  • 相关阅读:
    CSS 入门指南(二)CSS 常用样式及注册页面案例
    java 性能分析:如何提高 Java 程序的性能
    力扣刷题day44|309最佳买卖股票时机含冷冻期、714买卖股票的最佳时机含手续费
    一个.NET开源的功能丰富、灵活易用的 Windows 窗口增强神器
    设计模式-10组合模式(组合模式设计模式)详解
    数学建模(一):2022年国赛a题
    GitLab项目组相关操作(创建项目组Group、创建项目组的项目、为项目添加成员并赋予权限)
    pytorch中的矩阵乘法
    怎么把pdf转换成jpg图片?
    DeepMind 利用无监督学习开发 AlphaMissense,预测 7100 万种基因突变
  • 原文地址:https://blog.csdn.net/qq_43199575/article/details/133275537