本文共 5386 字,大约阅读时间需要 17 分钟。
现实中我们有很多种方法来获取数字图像:数字摄像头、扫描仪、计算机断层扫描以及核磁共振生成图像等等。对我们人类来说这些设备生成的结果我们称之为图像。而我们从这些设备获取的图像最终是以组成点阵的数值来表示的。
就好像是一张车的图片中就是包含了点阵强度值的矩阵。我们可以根据需要来获取或者存储点阵,但最终所有计算机中的图片就剩下点阵以及描述点阵的信息。OpenCV 是一个计算机视觉库,主要用来处理和操作这类图像信息。因此你首先需要熟悉的是 OpenCV 是如何存取图像的。
Mat
OpenCV 项目大约在 2001 年推出,之前主要是提供了 C 接口并通过名为 IplImage 的 C 结构体来处理内存中的图像。在一些老的教程和学习材料中你经常会看到这个结构体。使用这个结构体的问题是让 OpenCV 严重受限于 C 语言的特性和缺点,最大的问题就是需要进行手工内存管理。它要求用户必须小心的操作内存的分配和释放。对一些小程序而言,这不是什么大问题,但是一旦你的代码量增长越来越迅速时,这个问题变得非常严重。
幸运的是,C++ 语言实现了类的概念,可以轻松的实现自动化的内存管理(或多或少)。好消息是 C++ 完全兼容 C 语言,因此改用 C++ 并没有兼容性问题需要解决。所以 OpenCV 2.0 引入了全新的 C++ 接口,意味着你无需再关系内存管理的问题,让你的代码运行更加可靠。而 C++ 接口的缺点是很多嵌入式开发系统当前还只是支持 C 语言。因此,除非你使用一些特定的嵌入式系统,否则没有理由继续使用老的接口(除非你就是想自寻烦恼)。
首先我们需要了解的是 Mat 无需手工进行内存的分配和释放。虽然这样仍然只是一种可能性,因为绝大多数的 OpenCV 函数将自动的分配输出数据所需的内存。作为一个很好的红利,如果你传递一个已有的而且已经分配了阵列内存空间的 Mat 对象,它会被重用。换句话说,任何时候我们只需要使用最少的内存来执行各种任务。
Mat 是一个类,包含了阵列的头(阵列大小、存储的方法以及存储地址等等)和指向阵列点阵数据的指针(维度取决于存储的方法)。阵列的头部大小是一个常量,不同图片的头部存放的阵列大小是不同的。
OpenCV 是一个图像处理库。其包含大量各种图像处理函数。为了满足计算的要求,绝大多数时间你都会使用多个 OpenCV 函数。例如传递图片给某个函数是经常需要做的。我们别忘了我们正在讨论图像处理算法,这往往是非常沉重的计算。最后我们需要做的是进一步提升程序的速度,减少潜在的不必要的大图片拷贝。
为了解决这个问题,OpenCV 使用引用计数系统。这个思路就是每个 Mat 拥有独立的头部信息,而阵列数据是共享的。此外,拷贝操作只拷贝头部而不拷贝数据。
Mat A, C; // 创建头部A = imread(argv[1], CV_LOAD_IMAGE_COLOR); // 分配阵列Mat B(A); // 使用拷贝构造函数C = A; // 赋值操作
上述代码中所有的对象都指向同一个数据阵列。而它们的头部是不同的,这样的话对某个对象进行操作就会影响到其他的对象。实际上不同的对象只是提供不同的访问方法来访问相同的底层数据。真正有趣的是你可以创建只指向部分数据的头部。例如,你可以使用如下代码来创建图像的兴趣点,包含新的头部和新的边界:
Mat D (A, Rect(10, 10, 100, 100) ); // 使用矩形Mat E = A(Range::all(), Range(1,3)); // 使用行列边界
这时候你可能会问该阵列本身是否属于多个 Mat 对象,这些对象负责在其不需要的时候进行数据清理。最短的回答就是:它使用的是最后一个对象。这是通过引用计数机制来实现的。当某人拷贝一个 Mat 对象的头部,该阵列的计数器就会加1.当头部被清理时计数器就会减1.当计数器值为0的时候,阵列就会被释放。有时候你也想拷贝阵列本身,OpenCV 提供了 和 函数。
Mat F = A.clone();Mat G;A.copyTo(G);
现在修改 F 或者 G 都不会影响 Mat 头部所指向的阵列。你需要记住的是:
这是关于点阵值的存储问题。你可以选择色彩空间和所使用的数据类型。色彩空间指的是我们如何利用给定的颜色代码组合成颜色组件。最简单的是灰度图,我们所需要处理的颜色只有黑白两色。这样的组合可以让我们创建很多灰色阴影。
而我们有很多的方法来处理彩色图。每一种方法都至少包含 3 到 4 中基本组件,我们可以对这些进行合并来创建彩色图。最通用的是 RGB,主要因为这是我们眼睛对色彩的识别方式。其基准色是红、绿、蓝。为了生成透明图像我们还需要第四个元素 —— alpha(A).
不同的色彩方案有不同的优势:
每个颜色组件都有其有效的域,这个决定了我们所使用的数据类型:我们是如何存储一个组件决定了我们在这个域上的控制。最小的数据类型是 char,相当于一个字节或者 8 位数据。这个可以是无符号的(可以存储 0 - 255) 或者有符号的(-127 - 127)。虽然在三组件情况下(如 BGR)已经提供了 1600 多万的色彩值。我们还可以使用 float (4 byte = 32 bit) 或者 double (8 byte = 64 bit) 数据类型来定义每个组件。不过,需要记住的是,提升组件的值同样也提升了整个图片占用内存的大小。
在教程 中我们已经知道如何通过 函数将点阵数据写到图像文件中。这样做可以大大方便调试的过程。你可以使用 Mat 的 << 操作符,不过需要注意的是这个只适合二维的阵列。
虽然 Mat 作为一个图像的容器挺合适,但它同时也是一个矩阵类。所以可以用它来创建和操作多维的矩阵。有很多方法来创建一个 Mat 对象:
构造函数
Mat M(2,2, CV_8UC3, Scalar(0,0,255)); cout << "M = " << endl << " " << M << endl << endl;
对于两个维度或者多个维度的图像我们首先要定义大小,包括行列数。
然后需要指定用来存储元素的数据类型和每个点阵的通道数量。可以使用使用如下代码来一次定义多个变量:
CV_[The number of bits per item][Signed or Unsigned][Type Prefix]C[The channel number]
例如 CV_8UC3 意味着使用无符号 char 类型,8位长度长整数以及每个点阵使用 3 通道。最多可预定义 4 个通道数量。 是一个 4 元素的短整数向量。指定完后可以使用定制值来初始化点阵。如果你需要更多的通道,可以使用 upper 宏来创建,并在括号中指定通道数量,如下所示:
使用 C/C++ 数组并通过构造函数初始化
int sz[3] = { 2,2,2}; Mat L(3,sz, CV_8UC(1), Scalar::all(0));
上述示例显示如何创建一个超过 2 个维度的阵列。指定阵列的维度数,并传递包含每个维度大小的指针。
为已有的 IplImage 指针创建一个头部:
IplImage* img = cvLoadImage("greatwave.png", 1);Mat mtx(img); // convert IplImage* -> Mat
函数:
M.create(4,4, CV_8UC(2)); cout << "M = "<< endl << " " << M << endl << endl;
你不能在这个构造函数中初始化阵列值,它只在其阵列数据存储大小与老的不匹配时重新分配。
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;
对于一些小的阵列你可以使用逗号隔开初始化方法:
Mat C = (Mat_(3,3) << 0, -1, 0, -1, 5, -1, 0, -1, 0); cout << "C = " << endl << " " << C << endl << endl;
为一个已有的 Mat 对象创建一个新的头部,并进行 后者 .
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));
在前面的例子中我们看到了默认的格式化选项。而 OpenCV 允许你自定义阵列输出的格式化方式::
默认
cout << "R (default) = " << endl << R << endl << endl;
Python
cout << "R (python) = " << endl << format(R,"python") << endl << endl;
逗号分隔的值 (CSV)
cout << "R (csv) = " << endl << format(R,"csv" ) << endl << endl;
Numpy
cout << "R (numpy) = " << endl << format(R,"numpy" ) << endl << endl;
C
cout << "R (c) = " << endl << format(R,"C" ) << endl << endl;
其他常用的 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 via cv::Mat
vectorv; 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 of points
vectorvPoints(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;
这里的大多数示例都包含一个小的控制台程序,你可以从这里 这些代码。
你也可以在 观看视频教程.