C++版本OpenCv教程(一)Mat—基本的图像容器

英文原版链接
Mat - The Basic Image Container

目标

我们有多种方法从现实世界获取数字图像:数码相机、扫描仪、计算机断层扫描和磁共振成像等等。在以上任何情况下,我们(人类)看到的都是图像。然而,当将其转换到我们的数字设备时,我们所记录的是图像中每个点的数值。
在这里插入图片描述
例如,在上面的图像中,你可以看到汽车的镜子只不过是一个包含所有像素点的强度值的矩阵。我们获取和存储像素值的方式可能会根据我们的需要而有所不同,但最终计算机世界中的所有图像都可能被简化为数值矩阵和描述矩阵本身的其他信息。OpenCV是一个计算机视觉库,主要致力于处理和操作这些信息。因此,首先需要熟悉OpenCV如何存储和处理图像。

Mat

cv::Mat Class Reference
关于Mat,您需要知道的第一件事是,您不再需要 手动分配它的内存并在不需要时立即释放它。虽然仍可以这样做,但大多数OpenCV函数将自动分配其输出数据。如果您传递一个已经存在的Mat对象(该对象已经为矩阵分配了所需的空间),那么它将被重用。换句话说,我们在任何时候都只使用执行任务所需的内存。

Mat基本上是一个类,有两个数据部分:Mat头(包含矩阵大小、存储方法、矩阵存储地址等信息)和一个指向包含像素值矩阵的指针(采取多少维数 取决于选择的存储方法)。矩阵头的大小是恒定的,然而,矩阵本身的大小可能随着图像的不同而变化,通常会大上几个数量级。

OpenCV是一个图像处理库。它包含了大量的图像处理功能。为了解决计算难题,大多数时候您将使用库的多个函数。因此,将图像传递给函数是一种常见的做法。我们不应该忘记我们讨论的是图像处理算法,这些算法往往需要大量的计算。我们最不希望做的事情就是通过对可能很大的图像进行不必要的复制来进一步降低程序的速度。

为了解决这个问题,OpenCV使用了一个引用计数系统。其思想是每个Mat对象都有自己的矩阵头,但是当两个Mat对象之间共享一个矩阵时,可以通过让两个Mat对象的矩阵指针指向相同的地址。此外,复制操作符将只复制矩阵头和指向大矩阵的指针,而不是数据本身。

Mat A, C;                          // creates just the header parts
A = imread(argv[1], IMREAD_COLOR); // here we'll know the method used (allocate matrix)
Mat B(A);                                 // Use the copy constructor
C = A;                                    // Assignment operator

最后,上述所有对象都指向同一个数据矩阵,使用其中任何一个对象进行修改都会影响到所有其他对象。实际上,不同的对象只是为相同的底层数据提供了不同的访问方法;然而,它们的矩阵头部分是不同的。真正有趣的部分是,您可以创建只引用部分数据的矩阵头。例如,要在图像中创建感兴趣的区域(ROI),只需创建一个带有新边界的新标题:

Mat D (A, Rect(10, 10, 100, 100) ); // using a rectangle
Mat E = A(Range::all(), Range(1,3)); // using row and column boundaries

现在你可能会问-如果矩阵本身可能属于多个Mat对象:当它不再被需要时,谁负责清理它。简而言之:是最后一个使用它的对象。这是通过使用引用计数机制来处理的。每当有人复制一个Mat对象的矩阵头时,就会为矩阵增加一个计数器。每当清除矩阵头时,此计数器就会减少。当计数器达到0时,矩阵被释放。
采用引用次数来释放存储内容是C++中常见的方式,用这种方式可以避免仍有某个变量引用数据时将这个数据删除造成程序崩溃的问题,同时极大的缩减了程序运行时所占用的内存。
但是,有时候还需要复制矩阵本身,所以OpenCV提供了 cv::Mat::clone() 和 cv::Mat::copyTo() 函数。

Mat F = A.clone();
Mat G;
A.copyTo(G);

接下来我们来了解Mat类里可以存储的数据类型,根据官方给出的Mat类继承图,如图2-2所示,我们发现Mat类可以存储的数据类型包含double、float、uchar、unsigned char以及自定义的模板等。
Mat类继承关系图
我们可以通过代码清单2-2的方式声明一个存放指定类型的Mat类变量:

代码清单2-2 声明一个指定类型的Mat类
cv::Mat A = Mat_<double>(3,3);//创建一个3*3的矩阵用于存放double类型数据

由于OpenCV提出Mat类主要用于存储图像,而像素值的最大值又决定了图像的质量,如果用8位无符号整数去存储16位图像,会造成严重的图像颜色失真或造成数据错误。而由于不同位数的编译器对数据长度定义不同,为了避免在不同环境下因变量位数长度不同而造成程序执行问题,OpenCV根据数值变量存储位数长度定义了数据类型,表2-1中列出了OpenCV中的数据类型与取值范围。
在这里插入图片描述
仅有数据类型是不够的,还需要定义图像数据的通道(Channel)数,例如灰度图像数据是单通道数据,彩色图像数据是3通道或者4通道数据。因此针对这个情况,OpenCV还定义了通道数标识,C1、C2、C3、C4分别表示单通道、双通道、3通道和4通道。每一种数据类型都存在多个通道的情况,所以将数据类型与通道数表示结合便得到了OpenCV中对图像数据类型的完整定义,例如CV_8UC1表示的就是8位单通道数据,用于表示8位灰度图,而CV_8UC3表示的是8位3通道数据,用于表示8位彩色图。我们可以通过代码清单2-3的方式创建一个声明通道数和数据类型的Mat类:

代码清单2-3 通过OpenCV数据类型创建Mat类
cv::Mat a(640,480,CV_8UC3) //创建一个640*480的3通道矩阵用于存放彩色图像
cv::Mat a(3,3,CV_8UC1) //创建一个3*3的8位无符号整数的单通道矩阵
cv::Mat a(3,3,CV_8U) //创建单通道矩阵C1标识可以省略

虽然在64位编辑器里,uchar和CV_8U都表示8位无符号整数,但是两者有严格的定义,CV_8U只能用在Mat类内部的方法。例如用Mat_<CV_8U>(3,3)和Mat a(3,3,uchar)会提示创建错误。

现在修改F或G将不会影响A的Mat头指向的矩阵。你需要记住的是:

  • OpenCV函数的输出图像内存分配是自动的(除非另有指定)。
  • 您不需要考虑使用OpenCV的c++接口进行内存管理。
  • 赋值操作符和复制构造函数只复制Mat头。
  • 可以使用cv::Mat::clone()和cv::Mat::copyTo()函数复制图像的底层(数据)矩阵。

存储方法

这是关于如何存储像素值的。您可以选择颜色空间和使用的数据类型。颜色空间指的是我们如何结合颜色组件来编码一个给定的颜色。最简单的一种是灰度,我们可以使用黑色和白色来制造灰度。这些组合使我们能够创建许多灰度级。

对于创建色彩的方式,我们有更多的方式可供选择。它们每一种都将其分解为三个或四个基本组件,我们可以使用这些组件的组合来创建其他组件。最流行的一种是RGB,主要是因为这也是我们的眼睛构建颜色的方式。它的基色是红、绿、蓝。为了编码一种颜色的透明度,有时会添加第四个元素:alpha (透明度)(A)。

然而, 不同颜色系统各有其优点:

  • RGB是最常见的,我们的眼睛使用类似的模式。但是请记住,OpenCV标准显示系统使用BGR颜色空间组成颜色(红色和蓝色通道互换位置)。
  • HSV和HLS将颜色分解为色调、饱和度和亮度值组成部分,这是我们描述颜色更自然的方式。例如,您可能会忽略最后一个组件,使您的算法对输入图像的光线条件不太敏感。
  • YCrCb被流行的JPEG图像格式所使用。
  • CIE Lab*是一个感知上一致的颜色空间,如果你需要测量一种给定颜色到另一种颜色的距离,它很方便。

每个构建组件都有自己的有效域。他们直接影响所使用的数据类型。存储组件的方式定义了我们对其域的控制。最小的数据类型可能是char,即一个字节或8位。它可以是无符号的(因此可以存储0到255的值)或有符号的(从-127到+127的值)。虽然在三个组件的情况下,这已经给出了1600万种可能的颜色来表示(像在RGB的情况下),但是,我们仍可以通过使用float(4字节= 32位)或double(8字节= 64位)数据类型为每个组件获得更精细的控制。不过,请记住,增加组件的大小也会增加内存中整个图片的大小。

显式创建一个Mat对象

但是,出于调试的目的,查看实际值要方便得多。您可以使用Mat的<<运算符来完成此操作。注意,这只适用于二维矩阵。

虽然Mat作为图像容器工作得很好,但它也是一个通用的矩阵类。因此,创建和操作多维矩阵是可能的。你可以用多种方式创建一个Mat对象:
- cv::Mat::Mat 构造函数

#include<iostream>
//#include <stdio.h>
#include <opencv2/opencv.hpp>
#include "opencv/highgui.h"

using namespace std;
using namespace cv;

int main(int argc,char** argv) {
    Mat M(2,2,CV_8UC3,Scalar(0,0,255));
    cout<<"M = "<<endl;
    cout<<M<<endl;
    return 0;
}

结果:

M = 
[  0,   0, 255,   0,   0, 255;
   0,   0, 255,   0,   0, 255]

对于二维和多通道图像,我们首先定义它们的大小,行和列计数。

然后,我们需要指定用于存储元素的数据类型和每个矩阵点的通道数量。为了做到这一点,我们根据下面的约定构造了多个定义:

CV_[The number of bits per item][Signed or Unsigned][Type Prefix]C[The channel number]
CV_[单元素的bits数][Signed or Unsigned][数据类型前缀]C[通道数]

例如,CV_8UC3意味着我们使用8位长的无符号char类型,每个像素有三个这样的类型来形成三个通道。为最多4个通道预定义了类型。cv::Scalar是四个元素的 short vector。指定它,您就可以用自定义值初始化所有矩阵点。如果需要更多,可以使用上面的宏创建类型,在括号中设置通道编号,如下所示。

- 使用C/ C++数组并通过构造函数进行初始化

int sz[3] = {2,2,2};
Mat L(3,sz, CV_8UC(1), Scalar::all(0));

上面的例子展示了如何创建一个二维以上的矩阵。指定其维度,然后传递一个包含每个维度大小的指针,剩下的保持不变。

- cv::Mat::create 函数

M.create(4,4, CV_8UC(2));
cout << "M = "<< endl << " "  << M << endl << endl;

您不能用这种结构初始化矩阵值。它只会重新分配它的矩阵数据内存,如果新的大小不能适应旧的。

- MATLAB风格的初始化器:cv::Mat:: zero, cv::Mat:: ones,cv::Mat::eye。指定要使用的大小和数据类型:

Mat E = Mat::eye(4, 4, CV_64F);
cout << "E = " << endl << " " << E << endl << endl;
Mat O = Mat::ones(2, 2, CV_32F);
cout << "O = " << endl << " " << O << endl << endl;
Mat Z = Mat::zeros(3,3, CV_8UC1);
cout << "Z = " << endl << " " << Z << endl << endl;

结果:

E = 
[1, 0, 0, 0;
 0, 1, 0, 0;
 0, 0, 1, 0;
 0, 0, 0, 1]
O = 
[1, 1;
 1, 1]
Z = 
[  0,   0,   0;
   0,   0,   0;
   0,   0,   0]

- 对于小矩阵,你可以使用逗号分隔的初始化器或初始化器列表(最后一种情况需要c++ 11支持):

Mat C = (Mat_<double>(3,3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);
cout << "C = " << endl << " " << C << endl << endl;
C = (Mat_<double>({0, -1, 0, -1, 5, -1, 0, -1, 0})).reshape(3);
cout << "C = " << endl << " " << C << endl << endl;

- 您可以使用cv::randu()函数用随机值填充一个矩阵。你需要为随机值给出一个下限和上限:

Mat R = Mat(3, 2, CV_8UC3);
randu(R, Scalar::all(0), Scalar::all(255));
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

yhwang-hub

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

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

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

打赏作者

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

抵扣说明:

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

余额充值