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)) # <class 'numpy.ndarray'>
print(image.shape) # h,w,c

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

#include <opencv2/opencv.hpp>  
#include<iostream>  
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;
}

一、初识Mat类

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

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

注: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(输入图像,下界,上界,输出图像);

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

常规语法:

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

还有一种方式是采用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位无符号矩阵数据

(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)

利用已有矩阵构造,还可以进行类似截图的操作,可以采用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

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通道

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

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

(3)使用成员函数赋值

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

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

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

全黑的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);

在这里插入图片描述
(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)  //像素求非运算
 

此外,深度学习中使用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型指针

使用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;
    }

   上面这种遍历方式只适用于单通道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());
        }

    }

  • 0
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

想要躺平的一枚

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值