目标
我们有多种方法从现实世界中获取数字图像:数码相机,扫描仪,计算机断层摄影技术,核磁共振成像等。在任何情况下我们看到的都是图像。然而当将其转换到我们的数字设备时,我们所记录的是图像中每个点的数值。
就和上面的图像一样,你可以看到汽车的镜像只是一个包含所有像素点的强度值的矩阵。我们会根据我们的不同需要来获取存储这些像素值,但最终在计算机世界中的所有图像都可能被简化位数字矩阵和描述矩阵本身的其他信息。OpenCV是一个计算机视觉库,它的主要重点是处理和操作这些信息。因此我们首先需要熟悉OpenCV是如何存储和处理图像的。
MAT
OpenCV在2001年就已经出现的。当时OpenCV是一个使用C语言构建,当时位了将图片存储在内存中他们使用了一个成为IpIImage的C结构体。这也是为什么在大多数老版本的OpenCV中会经常看到IpIImgae结构的原因。这样做存在的问题是将C语言的负面问题都引入了进来,最大的问题是需要程序员手动进行内存管理。虽然这对较小的程序来说问题不大,但一旦你的代码库增长了,处理这些就会比专注于解决你的开发目标要困难的多。
幸运的是C++的诞生并且引入了类的概念,通过自动内存管理使得用户可以更方便的使用类。C++和C是完全兼容的,所以修改后不会出现兼容问题。因此OpenCV2.0引入了新的C++接口,它提供了一种新方式,这意味着你不需要进行内存管理,使你的代码更为整洁。C++的主要缺点是目前许多嵌入式开发系统仅仅支持C语言。因此除非你的目标平台是嵌入式平台,否则强烈建议使用OpenCV2.0以上的版本。
对于Mat来说你所要知道的第一件事是,你不再需要手动分配内存,并且在不需要的时候手动释放内存。虽然手动创建释放内存仍然可能会有,但是大多数的OpenCV函数会自动为其输出数据分配内存。如果你传递了一个已经分配过内存的Mat对象,那么它将直接重用这个对象。换句话说,在任何时候我们都只需要执行任务所需要的内存空间。
Mat是一个有两部分书的基本类:矩阵头(包含了矩阵的大小,存储方法,已经矩阵的存储地址等信息)和一个指向包含像素值的矩阵指针(像素数据的存储维度是依据所选用的存储方法)。矩阵头部的大小是恒定的,但是矩阵本身的大小可能因为图像的不同而不同,可能要大上几个数量级。
OPenCV是一个图像处理库,它包含了大量的图像处理算法函数。为了解决一个计算问题,大多数情况下你需要使用此库的多个函数。正因为如此将图像传递给函数是一种常见的作法。我们需要知道的是我们正在讨论的是图像处理算法,它的计算量往往是非常大的。我们最不希望做的是对大图像进行不必要的复制,从而降低了程序的处理速度。
为了解决图像复制的问题 ,OpenCV使用了引用计数系统。每个Mat都有其自己的头,但是每两个Mat对象之间可以通过让他们的矩阵指针指向相同的地址的方法来共享一个矩阵。另外赋值操作符也仅仅是复制头和指向大型矩阵的指针,而并不复制数据本身。
Mat A, C; // 仅仅创建Mat头
A = imread(argv[1], IMREAD_COLOR); // here we'll know the method used (allocate matrix)
Mat B(A); // 使用复制构造器
C = A;
上述所有Mat对象最终都指向同一个数据矩阵,使用任何一个对象进行数据修改也会影响到其他的所有对象。实际上不同的对象只是对不同的底层数据提供了不同的方法方法。然而他们的头部是不同的。真正有趣的是你可以创建只引用完整数据的一部分的头部。例如要在要在一个图像中创建一个感兴趣区域(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负责清理空间。这些可通过引入计数机制来首先。每当有人复制了一个Mat对象的头部时,矩阵计数器就会加1.每当清理头部的时候,此引用计数器就会减1.当引用计数器达到0的时候,释放此矩阵。有时你也想复制图像矩阵自身,所以OpenCV提供了cv::Mat::clone()和cv::Mat::copyTo()函数。
Mat F = A.clone();
Mat G;
A.copyTo(G);
现在修改F或者G就不会影响到由A所指向的矩阵,你需要记住的是
- OpenCV函数的输出图像分配空间是自动的(除非另有说明)
- 使用OpenCV的C++接口不用考虑内存管理
- 赋值操作符和复制构造函数仅仅复制Mat的头部,像素矩阵并不复制
- 图像的底层像素矩阵可以使用cv::Mat::clone() cv::Mat::copyTo()进行复制
存储方法
存储像素值的时候你可以选择颜色空间以及数据类型。颜色空间指的是我们如何组合基本颜色对给定颜色进行编码,最简单的就是灰度值,我们使用的颜色是黑和白,这些组合可以让我们创造出许多灰色的阴影。
我们可以有多种选择来实现彩色。每种方式都是将颜色分为3到4个基本颜色,我们可以使用这些基本颜色通过各种组合方式来创建不同的颜色出来。最流行的一种颜色空间是RGB,主要是因为这也是我们眼睛构建颜色的方式。为了编码颜色的透明度信息,有时还会加入第四个元素:alpha.然而还有其他不同的颜色系统,它们都各有各的优势:
- RGB是最常见的颜色系统,主要是因为我们的眼睛就是按照RGB来构建颜色的。但是请记住OpenCV标准显示系统使用的是BGR颜色系统来组成颜色的
- HSV和HLS将颜色分解为色相,饱和度和亮度分量。这是我们描述颜色更为自然的方式。例如去掉亮度分量,这样你的算法对输入图像的光照条件就不那么敏感了
- YCrCb是常用的JPEG图像格式
- CIE L*a*b是一个感知上一致的颜色空间。如果你需要测量一种给定颜色到另一种颜色的距离,那么CIE L*a*b就非常有用了。
每种基本颜色都有自己的有范围,这就引出了不同颜色空间使用的数据类型。我们如何定义一个基本颜色就决定了在这个色域桑有多少个等级。最小的数据类型可能是char,这意味着1个字节,这一个字节可能是无符号的(可存储0-255),也可能是有符号的(-127--+127).尽管在RGB三原色的情况下我们已经可以给出1600万种可能的颜色,我们仍可以使用float(4字节)或者double(8)字节每个基色的数据类型来获得更精细的控制。尽管如此,请记住,增加基色的大小也会大大增加在内存种存储的整个画面的大小。
显式创建Mat对象
在加载,修改,保存图像的教程种,你已经学习了如何使用cv::imwrite()函数将矩阵写入图像文件。但是出于实际目睹,查看实际值要更为方便。你可以使用Mat的<<操作符来实现这一点,需要注意的是<<操作符只对二维矩阵有效。
尽管Mat作为图像容器工作的很不错,但是Mat也是一个通用的矩阵类,因此可以用于创建和操作多维矩阵,你可以是哦那个多种方式来创建Mat对象:
cv::Mat::Mat ///构造函数
Mat M(2,2, CV_8UC3, Scalar(0,0,255));
cout << "M = " << endl << " " << M << endl << endl;
对于二维和多通道图像,我们首先定义图像大小:行和列.之后我们需要指定存储像素元素的数据类型以及每个像素点的通道个数.为了做到这一点,我们根据以下约定创建了多个不同的定义:
CV_[每个item的位数][有符号或无符号][类型前缀]C[通道数]
例如CV_8UC3意味着我们使用长度为8位的unsigned char数据类型,且每个像素有3个unsigned char数据来表示以形成3个颜色通道.最多可以有4个预定义的颜色通道.标量cv::Scalar是第四个短向量.指定了cv::Scalar你可以使用自定义值初始化所有的像素点.
- 使用C/C++数组来初始化
int sz[3] = {2,2,2}; Mat L(3,sz, CV_8UC(1), Scalar::all(0));
上面的例子展示了如何创建一个超过二维的矩阵.指定维度,然后传递一个包含每个维度大小的指针,其余数据不变传递给Mat的构造函数.
- 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;
- 对于较小的矩阵,你可以使用逗号分割来初始化,或者使用初始化列表(初始化列表需要C++11支持)
Mat C = (Mat_<double>(3,3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);
cout << "C = " << endl << " " << C << endl << endl;
///下面的初始化需要c++11支持
C = (Mat_<double>({0, -1, 0, -1, 5, -1, 0, -1, 0})).reshape(3);
cout << "C = " << endl << " " << C << endl << endl;
- 使用cv::Mat::clone()或者cv::Mat::copyTo()来为现有Mat对象创建一个新的头部
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支持格式化矩阵的输出格式:
输出其他常见的选项
OpenCV支持使用<<操作符来对其他常见的OpenCV数据结构进行输出:
官方程序示例:
/* For description look into the help() function. */
#include "opencv2/core.hpp"
#include <iostream>
using namespace std;
using namespace cv;
static void help()
{
cout
<< "\n---------------------------------------------------------------------------" << endl
<< "This program shows how to create matrices(cv::Mat) in OpenCV and its serial"
<< " out capabilities" << endl
<< "That is, cv::Mat M(...); M.create and cout << M. " << endl
<< "Shows how output can be formatted to OpenCV, python, numpy, csv and C styles." << endl
<< "Usage:" << endl
<< "./mat_the_basic_image_container" << endl
<< "-----------------------------------------------------------------------------" << endl
<< endl;
}
int main(int,char**)
{
help();
// create by using the constructor
//! [constructor]
Mat M(2,2, CV_8UC3, Scalar(0,0,255));
cout << "M = " << endl << " " << M << endl << endl;
//! [constructor]
// create by using the create function()
//! [create]
M.create(4,4, CV_8UC(2));
cout << "M = "<< endl << " " << M << endl << endl;
//! [create]
// create multidimensional matrices
//! [init]
int sz[3] = {2,2,2};
Mat L(3,sz, CV_8UC(1), Scalar::all(0));
//! [init]
// Cannot print via operator <<
// Create using MATLAB style eye, ones or zero matrix
//! [matlab]
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;
//! [matlab]
// create a 3x3 double-precision identity matrix
//! [comma]
Mat C = (Mat_<double>(3,3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);
cout << "C = " << endl << " " << C << endl << endl;
//! [comma]
// do the same with initializer_list
#ifdef CV_CXX11
//! [list]
C = (Mat_<double>({0, -1, 0, -1, 5, -1, 0, -1, 0})).reshape(3);
cout << "C = " << endl << " " << C << endl << endl;
//! [list]
#endif
//! [clone]
Mat RowClone = C.row(1).clone();
cout << "RowClone = " << endl << " " << RowClone << endl << endl;
//! [clone]
// Fill a matrix with random values
//! [random]
Mat R = Mat(3, 2, CV_8UC3);
randu(R, Scalar::all(0), Scalar::all(255));
//! [random]
// Demonstrate the output formatting options
//! [out-default]
cout << "R (default) = " << endl << R << endl << endl;
//! [out-default]
//! [out-python]
cout << "R (python) = " << endl << format(R, Formatter::FMT_PYTHON) << endl << endl;
//! [out-python]
//! [out-numpy]
cout << "R (numpy) = " << endl << format(R, Formatter::FMT_NUMPY ) << endl << endl;
//! [out-numpy]
//! [out-csv]
cout << "R (csv) = " << endl << format(R, Formatter::FMT_CSV ) << endl << endl;
//! [out-csv]
//! [out-c]
cout << "R (c) = " << endl << format(R, Formatter::FMT_C ) << endl << endl;
//! [out-c]
//! [out-point2]
Point2f P(5, 1);
cout << "Point (2D) = " << P << endl << endl;
//! [out-point2]
//! [out-point3]
Point3f P3f(2, 6, 7);
cout << "Point (3D) = " << P3f << endl << endl;
//! [out-point3]
//! [out-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;
//! [out-vector]
//! [out-vector-points]
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;
//! [out-vector-points]
return 0;
}