OpenCV学习笔记-Mat

Mat - 基本的图像容器

兼容:> OpenCV 2.0
作者:Bernát Gábor

前言

我们有多种方法从现实世界中获取数字图像:数码相机、扫描仪、计算机断层扫描和磁共振成像等。在每种情况下,我们(人类)看到的都是图像。然而,当我们把这个转换为数字设备时,我们记录的是图像的每一个点的数值。
这里写图片描述

例如,在上面的图像中,你看到汽车的镜子只不过是一个包含所有像素点强度值的矩阵。我们如何获取和存储像素值可能会根据我们的需要而变化,但最终,计算机世界中的所有图像可能会被简化为数字矩阵和描述矩阵本身的其他信息。OpenCV是一个计算机视觉库,它的主要焦点是加工和修改这些信息。因此,你需要熟悉的第一件事是OpenCV如何存储和处理图像。

Mat

OpenCV诞生于2001。那时,库是围绕C接口构建的,使用叫做 IplImage的C结构将图像存储在内存中。这是你在大部分的旧教程和教育材料中看到的。因而它把C语言的所有不足都带来了。其中最大的问题是手动内存管理。它建立在用户负责处理内存分配和回收的假设之上。虽然对于小程序来说不是问题,但是一旦你的代码变多了,你就会花更多的精力地去处理所有这些问题,而不是专注于你的开发目标。

幸运的是,C++出现了,并引入了类的概念,通过自动内存管理(或多或少)使用户编码更容易。而且C++与C完全兼容,不可能产生兼容性问题。因此,OpenCV 2.0引入了一个新的C++接口,它提供了一种新的处理方式,这意味着你不需要处理内存管理,使你的编码简洁(编写更少,实现更多)。

C++接口的主要缺点是,目前许多嵌入式开发系统只支持c。因此,除非你针对的是嵌入式平台,否则使用旧方法是没有意义的(除非您是一个masochist程序员,并且您正在自找麻烦)。

首先当你不需要某内存的时候,你不需要手动的处理或者释放它。虽然有时候还是需要手动的,但是大部分OpenCV会自动为输出数据分配内存。

还有一个优点,如果你传递一个已经存在的Mat对象,这个Mat对象矩阵已经分配了所需内存,这内存将被重用。这样我们在任何时候执行需要的任务时,我们都可以尽可能多的获得内存(在矩阵数据上花的内存少了,其他就可以获得多内存)。

Mat是一个基本类,包含了两个数据部分:矩阵头和矩阵数据本身。矩阵头大小是固定的,它包含了矩阵大小、存储方法、矩阵存储地址等信息。但矩阵本身会因图像和图像不同而不同,通常图像大的矩阵数据大。

OpenCV是一个图像处理库。它包含了大量的图像处理函数。为了解决计算难题,很多时候你将使用到库中的一些函数。因此,通常需要将图像传送到函数中。别忘了我们探讨的是图像处理算法,将面临着非常巨大的计算。因此如果能减少大图像中不必要的数据复制就可获得更高的程序处理速度。

为了处理这个问题,OpenCV使用引用计数系统。这个想法就是每个Mat对象有自己的头,而不同对象(实例)可以通过指针指向同一个地址而
共用一个矩阵。甚至复制操作时只复制头以及指向大矩阵的指针,而不是复制矩阵数据本身。(有点类似九头蛇妖,几个头共用一个身体)

Mat A,C;//只创建头部
A = imread(argv[1], IMREAD_COLOR);//该方法为矩阵数据部分分配内存
Mat B(A);//使用复制构造函数
C = A;//赋值函数

以上所有对象,最后都指向同一个矩阵数据。他们的头不同,但任何使用这个矩阵数据的,只要对其做修改,其他使用者也会受到影响。在实践中不同对象只是提供对相同底层数据的不同访问方法。但他们头部分是不同的。真正有意思的是,你可以创建一个只指向完整数据中一部分的头。例如,为了获得某图像中一个感兴趣区域,你可以创建一个带新边界的新的头。

Mat D(A,Rect(10,10,100,100)); //使用矩形作为边界
Mat E = A(Range::all(),Range(1,3)); //使用行和列作为边界

你也许会问如果一个矩阵属于多个Mat对象,当不再需要这个矩阵的时候,谁负责清理(释放内存)。简单的说,是由最后一个使用他的对象负责。这由引用计数机制来处理。当复制一个Mat对象的头时,计数器会为矩阵增加计数。当一个头被清除的时候,计数器会减少计数。当计数器减至零的时矩阵会被释放。不过如果你想复制矩阵本身时,OpenCV提供了cv::Mat::clone()和cv::Mat::copyTo()函数。

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

像上面的代码,修改F或者G不会影响由矩阵头A指向的矩阵。你需要记住以下几点:

  • OpenCV函数的输出图像内存分配是自动的(除非特别说明)。
  • 使用OpenCV的C++接口,你不需要考虑内存管理。
  • 赋值运算符和复制构造函数只复制头。
  • 想复制图像的底层矩阵数据可以用cv::Mat::clone()和cv::Mat::copyTo()函数。

存储方法

现在要讲关于如何存储像素值。你可以选择颜色空间和使用的数据类型。

颜色空间指为了把给定颜色编码我们如何组合颜色的元素。最简单的是灰度等级表示,这种方式我们将彩色只处理为黑色和白色。黑白的各种组合可以创造很多渐变的灰色。(比如每个像素点用0到255表示从白到黑不同等级的灰色)

我们也有很多方法来表示全色彩。这些方法可以分解为3种或者4种基本元素。我们可以用这些元素来混合出其图像的表示方法。其中最常用是表示方法是RGB,这也是因为该方法是我们眼睛看见色彩的方式。基本色彩是红、绿和蓝。颜色的透明度有时候作为第四个元素:称之为alpha(A).

不过,其他颜色表示方法都有自己的优势。

  • 正如我们眼睛看色彩原理一样,RGB是最常用的。然而要注意的是OpenCV标准中表示色彩用的是BGR颜色空间(交换了红色和蓝色的通道顺序)。

  • HSV和HLS将颜色分解为它们的色调、饱和度和亮度元素,这是我们描述颜色的一种更自然的方式。例如,您可能会忽略最后一个元素,使您的算法对输入图像的光线条件不那么敏感。

  • YCrCb为流行的JPEG图像格式所使用。

  • CIE L*a*b* 是一个视觉一致的颜色空间,如果你需要测量给定颜色到另一种颜色的距离,使用它就很方便了。

每个构成元素都有自己的有效域。这就导致使用数据类型的不同。如何存储这些元素就决定了我们能对期所作的操作。最小的数据类型可能是char,这意味着一个字节或8位。这可能是无符号的(因此可以将值存储为0到255)或有符号(从-127到+127的值)。在三个元素的情况下,虽然char可以提供1600万种可能的颜色表示(比如RGB),但是我们还可以通过使用浮点数(4字节=32位)或双精度(8字节=64位)数据类型来获得更好的操作。不过,请记住,这会增加元素的大小,最终增加图像在内存大小。

显式地创建一个Mat对象

在加载、修改和保存一个图像教程中,您可以学习如何通过使用cv::imwrite()函数来将矩阵写入一个图像文件。但是,出于调试目的,查看实际图像值要方便得多。你可以使用Mat的<<操作符。请注意,这只适用于二维矩阵。

尽管Mat作为图像容器用起来很便利,但它也是一个普通的矩阵类。因此,用它创建和操作多维矩阵是可以的。您可以通过多种方式创建Mat对象:

  • cv::Mat::Mat构造函数
Mat M(2,2,CV_8UC3,Scalar(0,0,255));
cout << "M = " << endl << " " << M << endl << endl;

这里写图片描述

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

我们需要指定用于存储元素的数据类型和每个矩阵点的通道数量。要做到这一点,我们需要根据以下约定构造多个定义:
CV_[每一项的位数][有无符合][类型的前缀][通道数量]
比如,CV_8UC3 意思是我们使用的是8位长的无符号字符类型,每个像素都有3个通道构成了三个通道。这是OpenCV预先定义的,最多有四个通道。

cv::Scalar 是short向量,可用初始化来第四项。

指定了以上,您可以用一个自定义值初始化所有的矩阵点。如果需要更多类型,可以使用上面的宏创建类型,如下所示,在括号中设置通道数。

  • 使用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::zeros , 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;

这里写图片描述

  • 对于小矩阵,您可以使用逗号分隔初始化:
 Mat C = (Mat_<double>(3,3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);
    cout << "C = " << endl << " " << C << endl << endl;

这里写图片描述

  • 为现有的Mat对象创建一个新的空间,使用 cv::Mat::clone 或 cv::Mat::copyTo。
Mat RowClone = C.row(1).clone();
    cout << "RowClone = " << endl << " " << RowClone << endl << endl;

这里写图片描述

您可以使用cv::randu()函数给矩阵填充一个随机值。你需要给出随机值的下上限:
Mat R = Mat(3, 2, CV_8UC3);
randu(R, Scalar::all(0), Scalar::all(255));

输出格式

在上面的例子中可以看到默认的格式选项。然而OpenCV,允许你格式矩阵输出:

  • 默认
 cout << "R (default) = " << endl <<        R           << endl << endl;

这里写图片描述

  • Python
cout << "R (python)  = " << endl << format(R, Formatter::FMT_PYTHON) << endl << endl;

这里写图片描述

  • 逗号分割方式(CSV)
cout << "R (csv)     = " << endl << format(R, Formatter::FMT_CSV   ) << endl << endl;

这里写图片描述

  • 科学方式
cout << "R (numpy)   = " << endl << format(R, Formatter::FMT_NUMPY ) << endl << endl;

这里写图片描述

  • C
cout << "R (c)       = " << endl << format(R, Formatter::FMT_C     ) << endl << endl;

这里写图片描述

其他常用项目的输出

通过<<操作符,OpenCV还提供了对其他常见OpenCV数据结构输出的支持:

  • 2D点
 Point2f P(5, 1);
 cout << "Point (2D) = " << P << endl << endl;

这里写图片描述

  • 3D点
Point3f P3f(2, 6, 7);
cout << "Point (3D) = " << P3f << endl << endl;

这里写图片描述

  • 经过cv::Mat的std::vector
vector<float> v;
v.push_back( (float)CV_PI); 
v.push_back(2);    
v.push_back(3.01f);
cout << "Vector of floats via Mat = " << Mat(v) << endl << endl;

这里写图片描述

  • 点的std:vector
vector<Point2f> vPoints(20);
for (size_t i = 0; i < vPoints.size(); ++i)
     vPoints[i] = Point2f((float)(i * 5), (float)(i % 7));
cout << "A vector of 2D Points = " << vPoints << endl << endl;

这里写图片描述

END

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值