一. 目标
在现实世界我们有多种获取数字图像的方式:数码相机,扫描器,计算机X射线,还有比如核磁共振成像.在任何情况下我们(人类)看到的都是图像.然而,当我们将这些图像转换到我们的数字设备上的时候,记录的都是图像上的每个点的数值.比如:
比如上图中你能够看到汽车的镜子只不过是一个包含所有像素点强度值的矩阵.我们获取和存储像素值的方式可能会根据我们的需要而不同,而最终计算机世界中的所有图像都可能被简化为数字矩阵和描述矩阵本身的其他信息.Opencv是一个计算机视觉库,它主要重点是处理和操作这些信息.因此,你需要熟悉的第一件事是OpenCV如何存储和处理图像.
二. Mat
OpenCV诞生于2001年,在那个时候,库是围绕着C接口构建的,为了将图像存储在内存中,他们使用了以纵横叫做lplImage
的C
结构.
这是您将在大多数较早的教程和教育材料中看到的.这样做的问题是,它把C
语言所有的缺点都暴露出来了.最大的问题是手动内存管理.它建立在这样的假设之上: 用户负责内存的分配和回收.虽然对于较小的陈旭来说,这不是问题,但是一旦代码库增大,处理所有这些问题将变得更加的困难,而不是专注于解决您的开发目标.
幸运的是,C++
出现了,并引入了类的概念,通过自动内存管理(或多或少)使用户使用更方便.好消息是,C++
和C
完全兼容,因此进行更改不会产生兼容性问题.因此,OPENCV2.0引入了一个新的C++
界面,它提供了一种新的做事方式,这意味着你不需要调整内存管理,让你的代码简洁(更少的编程,实现更多的功能).C++
接口的主要确定是目前许多嵌入式开发系统仅支持C,因此,除非你的目标是嵌入式平台,否则使用旧方法就没有意义了(除非你是一个受虐狂程序员,你自找麻烦去用旧方法)
你需要了解的第一件事Mat
是您不再需要手动分配其内存并在不需要它时立即释放它,虽然这样做也是可以的,但大多数的Opencv
函数将自动分配其输出数据,如果你传递了一个已经存在的Mat
对象,该对象已经为矩阵分配了所需的空间,那么这将是一个不错的奖励,它将被重用.换句话说,我们始终只使用执行任务所需的内存.
Mat
大体上是一个具有两个数据部分的类:矩阵头(包含诸如矩阵的大小,用于存储的方法,存储矩阵的地址等信息)和指向包含像素值的矩阵的指针(根据选择用于存储的方法获取任何维度).矩阵头的大小是定的,但是矩阵本身的大小可能因图像而异,并且通常大一个数量级.
Opencv
是一个图像处理库,它包含大量图像处理功能.为了解决计算难题,大多数的时候你最终会使用库中的多个函数.因此,将图像传递给函数是一种常见的做法.我们不应该忘记,我们在那个在谈论图像处理算法,这些算法往往计算量很大.最后,我们最不希望看到的事情就是通过对可能很大的图像进行不必要的复制来进一步降低程序的速度.
为了解决这个问题,Opencv
使用了一个引用计数系统.其思想是每个Mat
对象都有自己的头,但是两个Mat
对象之间可以共享矩阵,因为它们的矩阵指针指向相同的地址.此外,复制操作只会复制头文件和指向大矩阵的指针,而不会复制数据本身.
Mat A,c; / 仅仅是创建了 矩阵头部分
A = imerad(argv[1],IMREAD_COLOR); // 这里我们知道了使用何种方式(分配矩阵)
Mat B(A); // 调用拷贝构造
C = A; // 赋值操作符
最后,所有上述对象都指向同一个数据矩阵,使用其中任何一个对象进行修改都将影响所有其他的对象.实际上,不同的对象只是为相同的底层数据提供了不同的访问方法.然而,它们的头部部分是不同的.真正有趣的部分是,你可以创建只引用完整数据的一个子数据头.例如,要在图像中创建一个感兴趣的区域(ROI
),你只需要创建一个带有新边界的新矩阵头.
Mat D(A,Rect(10,10,100,100)); // 使用一个矩形对象创建一个ROI
Mat E = A(Range::all(),Range(1,3)); // 使用行列边界
现在你也许会问-如果矩阵本身属于多个Mat
对象,当它不再需要时谁负责清理它.简单的答案是:最后一个使用它的对象.这可以通过使用引用计数机制来处理.每当有人复制Mat
对象的时,矩阵的计数器就会增加.每当一个矩阵头被清理的时候,这个计数器就会减少.当计数器达到0的时候,矩阵会被释放.有时候你也想复制矩阵本身.所以,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()函数来复制
三. 存储方法
这是关于如何存储像素值.您可以选择使用的颜色空间和数据类型.颜色空间指的是我们如何组合颜色组件来编码给定的颜色.最简单的一种是灰度,我们仅仅使用颜色是黑色和白色来表示它.这些组合使我能够创造出许多灰色阴影.
对于彩色的方式,我们有更多的方法可供选择.每一个都可以分解成3到4个基本的部分,我们使用这些部分组合创建其他的颜色.最受欢迎的是RGB
,主要是因为这是我们眼睛的颜色的形成方式.它的底色是红,绿,蓝.为了编码颜色的透明度,有时会添加第四个元素alpha(a)
然而,还有许多其他的颜色系统,每个都有自己的优点:
RGB是最常见的,因为我们的眼睛使用类似的东西,但请记住,Opencv标准显示系统使用BGR颜色空间组成颜色(红色和蓝色交换位置)
HSV和HLS将颜色分解为色相,饱和度和值/亮度分量,这是我们教书颜色更自然的方式.例如,您可能会和忽略最后一个部分,使您的算法对输入图像的光照条件不太敏感
YCrCb是目前流行的JPEG图像格式
CIE L*a*b 是一个感知均匀的颜色空间,如果你需要测量一种颜色到另外一种颜色的距离,它很方便
每个构建组件都有自己的有效域.它由使用的数据类型决定.我们如何存储一个组件定义了我们对它的域的控制.最小数据类型可能是char
,它意味着一个字节或者是8位.这可能是无符号的(因此可以存储0到255的值)或者是有符号的(-127-127).虽然在三个组件的情况下,这已经提供二路1600万中可能的颜色来表示(如在RGB
的情况下),我们可以通过使用float
(4字节=32位)或者double
(8字节=64位)数据类型为每个组件获取更精细的控制.然而,请记住,增加组件的大小也会增加内存中的整个图像的大小.
四. 显示的创建一个图像矩阵Mat
在加载,修改,保存图像教程中,您已经学会了如何使用cv::imwrite()
函数将矩阵写入图像文件.但是,为了调试目的,查看实际值要方便的额多.你可以使用Mat
的<<
的操作符来完成此操作.请注意,此操作仅适用于二维矩阵.
虽然Mat
作为图像容器工作的很好,但它也只是一个普通的矩阵类.因此可以创建和操作多威矩阵.你可以通过多种方式创建Mat
对象;
-
cv::Mat::Mat Constuctor
Mat M(2, 2, CV_8UC3, Scalar(0, 0, 255)); cout << "M = " << endl << " " << M << endl << endl;
对于二维和多通道的图像,我们首先定义它们的大小:行数和列数
然后,我们需要指定用于存储元素的数据类型和每个矩阵点的通道数.为此,我们根据以下约定构造了多个定义:CV_[每项所占用的位数][是有符号还是无符号][类型前缀]C[通道数]
例如,
CV_8UC3
表示我们使用无符号字符类型,8位长,每个像素有三个这样的类型,以形成三个通道.cv::Scalar
是一个四个元素的小向量.指定它,你就可以用自定义值初始化所有的矩阵点.如果需要更多,我们使用上面的宏创建类型,将通道号设置在括号中,如下所示: -
使用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;
你不能使用此结构初始化矩阵.只有当新的大小不能转入旧的大小时,它才会重新分配它的矩阵数据内存. -
MATLIB 风格的初始化: 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; Mat Z = Mat::zeros(3, 3, CV_8UC1); cout << "Z = " << endl << Z << 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;
-
为已有的Mat对象创建一个新的矩阵头以及使用cv::Mat::clone或者使用cv::Mat::copyTo
Mat rowClone = C.row(1).clone(); cout << "rowClone = " << endl << rowClone << 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;
-
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;
-
其他常见项目的输出
Opencv
也支持通过<<
操作符输出其他常见的Opencv
数据结构 -
2D Point
Point2f P(5, 1); cout << "Point (2D) = " << P << endl << endl;
-
3D Point
Point3f P3f(2, 6, 7); cout << "Point (3D) = " << P3f << endl << endl;
-
std::vector 通过 cv::Mat
ector<float> v; v.push_back((float)CV_PI); v.push_back(2); v.push_back(3.01f); cout << "Vector of floats via Mat = " << endl << Mat(v) << 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 = " << endl << vPoints << endl;
五. 上述代码的完整示例
#include <opencv2/core.hpp>
#include <iostream>
#include <vector>
using namespace std;
using namespace cv;
#ifndef CV_CXX11
#define CV_CXX11
#endif // !CV_CXX11
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();
// 使用构造函数来创建图像矩阵
Mat M(2, 2, CV_8UC3, Scalar(0, 0, 255));
cout << "M = " << endl << M << endl << endl;
// 使用create()函数创建图像矩阵
M.create(4, 4, CV_8UC(2));
cout << "M = " << endl << M << endl << endl;
// 创建多维矩阵
int sz[3] = { 2,2,2 };
Mat L(3, sz, CV_8UC(1), Scalar::all(0));
// 多维矩阵不能通过 << 操作符进行打印
// 使用MATLAB风格的eye,ones,或者zeros来创建图像矩阵
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;
// 创建一个 3*3的double类型矩阵
Mat C = (Mat_<double>(3, 3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);
cout << "C = " << endl << C << endl << endl;
// 创建一个同样的矩阵,注意这里需要C++11支持
#ifdef CV_CXX11
C = (Mat_<double>({ 0,-1,0,-1,5,-1,0,-1,0 })).reshape(3);
cout << "C = " << endl << C << endl << endl;
#endif // CV_CXX11
// clone()
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));
// 演示输出选项
// 默认输出
cout << "R(default) = " << endl << R << endl << endl;
// python风格输出
cout << "R(Python) = " << endl << format(R, Formatter::FMT_PYTHON) << endl << endl;
// numpy风格输出
cout << "R(numpy) = " << endl << format(R, Formatter::FMT_NUMPY) << endl << endl;
// csv风格输出
cout << "R(csv) = " << endl << format(R, Formatter::FMT_CSV) << endl << endl;
// C风格输出
cout << "R(C) = " << endl << format(R, Formatter::FMT_C) << endl << endl;
// Point2f输出
Point2f P(5,1);
cout << "Point (2D) = " << P << endl << endl;
// Point3f输出
Point3f P3f(2, 6, 7);
cout << "Point (3D) = " << P3f << endl << endl;
// vector通过Mat输出
vector<float> v;
v.push_back((float)CV_PI);
v.push_back(2);
v.push_back(3.01f);
cout << "Vector of floats via Mat = " << endl << Mat(v) << endl << endl;
// 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 = " << endl << vPoints << endl << endl;
}
结果: