总述
我们有很多方式从现实世界中获取数字图像,比如:数字摄像机,扫描仪,计算机断层摄影术等等。我们人类直接看到的都是图像,然而当我们把他转换到数字设备里面去后,我们记录的就是图像中每个点的数值了。
打个比方,在上面这幅图中,车的后视镜部分在数字设备中存储成的就是一个数学矩阵。但是如何去获取和存储像素值是由我们的需要来决定的。但最终计算机存储图像都可以简化为数字矩阵和描述矩阵的信息。OpenCV 是一个主要关注处理和操作这些信息的计算机视觉库,因此我们最先需要熟悉的就是如何去利用OpenCV去存储和处理图像.
Mat
OpenCV 自从2001年发布,那时候这个库是围绕C语言编写的,存储图像的方式都是使用了C语言结构体–Ipllmage,这个是你可能会在老版本的教程中所看到的。但问题也随之而来,C语言需要手动管理内存,它使用的前提是用户能够很好的对内存的进行分配和释放。这可能对于小程序而言并不是什么大事情,可是一旦你的程序逐渐变得复杂起来,那么这将是一场噩梦,而你也无法将注意力集中在算法实现上了。
然而幸运的是,C++ 带来了类与对象的概念,管理内存变得简单了起来,并且C++是完全兼容C的,所以也不会产生兼容性问题,因此,OpenCV 2.0 带来了一种全新的操作方式,这意味着你可以不用太过于担心内存管理问题了!C++唯一的缺点可能是嵌入式开发只支持C语言,所以除非从事嵌入式开发,完全没有必要去使用老方法。
关于 Mat 首先应该了解的是,它不再需要手动分配和释放内存了,大多数OpenCV函数将自动分配空间给输出数据。
Mat 类分为两个数据部分,一部分是矩阵头(包含着矩阵的大小,存储方式,存储的地址等等),一部分是指向矩阵包含数值的指针。矩阵头的信息大小是一个常量,但矩阵本身的数据变化非常大,每一幅图像都不一样。
OpenCV 是一个图像处理库。它包含了超级多的图像处理函数。有时候为了解决一个计算难题,会利用里面非常多的函数,所以利用函数时,传递图像给函数是常见的事情,但不要忘记了,我们在使用的是图像处理算法,是非常消耗性能,我们最不想遇见的一件事情就是把时间浪费在毫无意义的图像拷贝上。
为了解决这个情况,OpenCV 使用了引用计数系统(reference counting system)。这个意思就是每个 Mat 对象都有自己的矩阵头,但是通过让两个Mat对象的矩阵指针指向同一个地址,可以在两个Mat对象之间共享一个矩阵。不仅如此,拷贝运算符(=)只会拷贝矩阵头和指针,而不是矩阵数据它本身。
Mat A, C; // 仅仅只创建矩阵头部分
A = imread(argv[1], IMREAD_COLOR); // 读取想要的数据
Mat B(A); // 使用拷贝构造函数
C = A; //赋值运算符
以上所有的矩阵,最终指向的都是同一块数据部分,改变它们其中任何一个,都会使得数据部分的改变。也就是说,这些不同的变量名仅仅只是提供不同的方式来访问相同的数据。但尽管如此,他们的矩阵头部信息是不一样的。最有意思的地方是,你可以创建一个矩阵头,并让他指向整体数据的一部分。
Mat D (A, Rect(10, 10, 100, 100) ); // 使用方形区域(Rect)
Mat E = A(Range::all(), Range(1,3)); // 用行和列来进行界定,这里的意思就是取所有行,取第2列到第4列的数据
但可能会有一个新的疑问,如果矩阵可以被多个矩阵头指向,那么谁来对这块数据进行释放操作呢?答案是最后一个使用他的矩阵头。这通常是由引用计数机制(reference counting mechanism)来处理的,只要矩阵被矩阵头所指向,这个计数机制就会自动增加,当矩阵头被释放了,这个计数就会下降,直到计数为零,这一部分矩阵数据就被释放了。但有时你确实想要拷贝一份数据该怎么办呢?OpenCV 提供了cv::Mat::clone() 和 cv::Mat::copyTo() 函数
Mat F = A.clone();
Mat G;
A.copyTo(G);
存储方式
这部分讲述我们该如何存储像素值。我们可以选择我们需要的颜色空间和使用的数据类型。颜色空间是指我们如何结合颜色分量,以便对给定的颜色进行编码。
- RGB 是最常用的,但需要记住的是OpenCV里存储的方式是BGR。
- HSV和HLS
- YCrCb 常用在JPEG图像中
- CIE
创建矩阵对象
Mat 作为图像的容器运行的非常好,但他同时也是一个一般意义上的矩阵类,因此,是可以创建和操作多维度矩阵的。你可以通过多种方法来创建矩阵对象。
- cv::Mat::Mat 构造函数
Mat M(2,2, CV_8UC3, Scalar(0,0,255));
cout << "M = " << endl << " " << M << endl << endl;
参数含义:(行,列,如何存储数据,标量初始化(可选))
如何存储数据,是由OpenCV定义好的字段
CV_[每个像素的像素大小][有符号或无符号][类型前缀]C[通道数量]
- 使用C/C++数组来初始化
int sz[3] = {2,2,2};
Mat L(3,sz, CV_8UC(1), Scalar::all(0));
通过这种方式可以创建多维数据
- cv::Mat::creat
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 = (Mat_<double>({0, -1, 0, -1, 5, -1, 0, -1, 0})).reshape(3);
cout << "C = " << endl << " " << C << endl << endl;
- 可以克隆其他矩阵的某一行
Mat RowClone = C.row(1).clone();
cout << "RowClone = " << endl << " " << RowClone << endl << endl;
注:你可以用随机数来填满矩阵
Mat R = Mat(3, 2, CV_8UC3);
randu(R, Scalar::all(0), Scalar::all(255));
输出格式
Mat有多个输出格式可供选择
- 默认
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;
- Numpy
cout << "R (numpy) = " << endl << format(R, Formatter::FMT_NUMPY ) << endl << endl;
- C
cout << "R (c) = " << endl << format(R, Formatter::FMT_C ) << endl << endl;