鉴于中文语境下,学习 OpenCV 的资料其实稀少,不是主要讲解已经过时de 1.x 版内容《学习 OpenCV》,就是各路博主碎片化的学习心得,《OpenCV 2 计算机视觉编程手册》可以说是学习 OpenCV 的最佳入门途径了。最近需要将卷积神经网络的 Matlab 代码转换成 C++ 的,我也向实验室同房间的一位学弟借了此书,大致看了一下,重点看了第 1 章、第 2 章、第 6 章和附录。
因为这书是手册性质的,都是一些函数实例,所以记录下来,以便日后再用。下文是书中部分内容的摘录,夹杂一些我的理解。全文可能有点长,遂给出目录如下:
第1章 接触图像
第2章 操作像素
第6章 图像滤波
附录 OpenCV3 介绍及代码导读
<div id="Section1">第1章 接触图像</div>
- OpenCV 库的结构
- 载入、显示及保存图像
OpenCV 库的结构
- sources文件夹下的子文件夹:
- doc 文件夹中包含的是文档 + include 文件夹中是所有头文件
- modules 文件夹中包含所有的源程序
- samples 文件夹中则是许多简短的学习用范例
第 2 页讲了下怎么编译的,对于新版 OpenCV 来说已经没有必要了,解压后的 build 文件夹就是编译好的内容。
第 3 页介绍了各模块的功能,还有推荐的声明方式,为什么要用这种声明方式呢?
第 6 页提到,为了遵循 ANSI C++ 标准,在用 Visual Studio 建立工程时选择 Application Settings 时,没有勾选 Precompiled Header 选项,这是 Visual Studio 的预编译头文件特性,可以加速编译过程。
载入、显示及保存图像
- 声明图像变量
cv::Mat image;
创建宽高都为0的图像,返回值是一个结构体,
- 图像读取、解码以及内存分配
image = cv::imread("img.jpg");
- 检查图像是否被正确读取
if (!image.data) {
// 图像尚未创建……
}
此处的成员变量data事实上是指向已分配的内存块的指针,包括图像数据。当不存在数据时,它被简单设置为0.
- 声明一个需要进行图像显示的窗口,接着指定需要显示的图像:
cv::namedWindow("Original Image"); // 定义窗口
cv::imshow("Original Image", image); // 显示图像
显示图像的这条语句之所以还要出现窗口名称,是为了指定究竟把图像显示到哪个窗口去,因为可能存在多个窗口。
- 图像翻转
cv::Mat result;
cv::flip(image, result, 1); // 1表示水平翻转
// 0表示垂直翻转
// 负数表示既有水平也有垂直翻转
- 等待用户输入
cv::waitKey(0); //括号中填的数字是毫秒数,0为一直等待
如果没有这句话,显示的图像会一闪而过。
- 将图像写到磁盘
cv::imwrite("output.bmp", result);
文件的后缀名决定了图像保存时的编码格式。
- 指定初始尺寸
cv::Mat ima(240, 320, CV_8U, cv::Scalar(100));
CV_8U对应的是单字节的像素图象,字母U意味着无符号的(Unsigned)。对于彩色图像,需要指定3个通道(CV_8UC3)。
当 cv::Mat 对象离开作用域后,分配的内存将自动释放,从而避免内存泄漏的困扰。
另外,cv::Mat 实现了引用计数以及浅拷贝,当图像之间进行赋值时,图像数据并没有发生复制,两个对象都指向同一块内存块。这也可用于参数传值的图像,以及返回值传值的图像。引用计数的作用是当所有引用内存数据的对象都被析构后,才会释放内存块。如果你希望创建的图像拥有原始图像的崭新拷贝,那么可以使用copyTo()方法。
cv::Mat image2, image3;
image2 = result; // 两幅图像拥有同一份数据
result.copyTo(image3); // 创建新的拷贝
如果翻转output图像,并显示image2和image3,可以看到image2页翻转了,而image3没有变。
同理,函数返回其实也是一次浅拷贝过程。
cv::Mat function() {
// 创建图像
cv::Mat ima(240, 320, CV_8U, cv::Scalar(100));
// 并返回它
return ima;
}
// 得到灰度图
cv::Mat gray = function();
在函数function内,ima只是个局部变量,在离开作用域时应当被析构掉,但由于他所关联的引用计数表示内部图像正在被另一个对象gray所引用,因此内存块并不会被释放。
<div id="Section2">第2章 操作像素</div>
- 彩色或灰度图像存取像素值
void salt(cv::Mat &image, int n) {
for (int k = 0; k < n; k++) {
// rand() 是随机数生成函数
int i = rand() % image.cols;
int j = rand() % image.rows;
if (image.channels() == 1) { // 灰度图
image.at<uchar>(j,i) = 255;
} else if (image.channels() == 3) { // 彩色图
image.at<cv::Vec3b>(j,i)[0] = 255;
image.at<cv::Vec3b>(j,i)[1] = 255;
image.at<cv::Vec3b>(j,i)[2] = 255;
}
}
}
类 cv::Mat 有若干成员函数可以获取图像的属性。公有成员变量 cols 和 rows 给出了图像的宽和高。成员函数 at(int y, int x) 可以用来存取图像元素。 但是必须在编译期知道图像的数据类型,因为 cv::Mat 可以存放任意数据类型的元素。这也是这个函数用模板函数来实现的原因。所以 at 方法要指定数据类型,而且 at 方法本身不会进行任何数据类型转换。
cv::Vec3b,即由三个 unsigned char 组成的向量。
image.at<cv::Vec3b>(j,i)[channel] = value;
索引值 channel 标明了颜色通道号。
类似的,还有二元素向量类 cv::Vec2b 和四元素向量类 cv::Vec4b,s 代表 short,i 代表 int,f 代表 float,d 代表 double。所有这些类型都是使用模板类 cv::Vect<T, N> 定义的,其中 T 代表类型,N 代表向量中的元素个数。
- 有时候使用 cv::Mat 的成员函数会很麻烦,因为返回值的类型必须通过在调用时通过模板参数指定。因此,OpenCV 提供了类 cv::Mat_,它是 cv::Mat 的一个模板子类。在事先知道矩阵类型的情况下,使用 cv::Mat_ 可以带来一些便利。这个类额外定义了一些方法,但是没有任何成员变量,所以此类的指针或者引用可以直接进行相互类型转换。该类重载了操作符 (),允许我们可以通过它直接存取矩阵元素。因此,假设有一个 uchar 类型的矩阵,我们可以这样写:
cv::Mat_<uchar> im2 = image; // im2 指向 image
im2(50, 100) = 0; // 存取第 50 行,100列
由于 cv::Mat_ 的元素类型在创建实例的时候已经声明,操作符 () 在编译期就知道要返回的数据类型。使用操作符 () 得到返回值和使用 cv::Mat 的 at 方法得到的返回值是完全一致的,而且写起来更加简洁。
- 双重循环遍历所有像素值:
void colorReduce(cv::Mat &image, int div = 64) {
int nl = image.rows; // 行数
int nc = image.cols * image.channels();
for (int j = 0; j < nl; j++) {
// 得到第 j 行的首地址
uchar* data = image.ptr<uchar>(j);
for (int i = 0; i < nc; i++) {
data[i] = data[i] / div * div + div / 2;
}
}
}
OpenCV 默认使用 BGR 的通道顺序,而且 size 成员函数返回的先是宽,然后是高,成员变量 cols 代表图像的宽度(列数),rows 代表图像的高度,step 代表以字节为单位的图像的有效宽度,即使你的图像元素类型不是 uchar,step 仍然带代表着行的字节数。图像的通道数可以由 channels 方法得到,total 函数返回矩阵的像素个数,像素大小可以从 elemSize 函数得到,对于一个三通道的 short 型矩阵 CV_16SC3, elemSize 返回 6。
为了简化指针运算,cv::Mat 提供了 ptr 函数可以得到图像任意行的首地址。 ptr 函数是一个模板函数,它返回第 j 行的首地址:
uchar* data = image.ptr<uchar>(j);
等效地使用指针运算从一列移到下一列,所以,也可以这么些:
*data++ = *data / div * div + div / 2;
- 有一个知识,跟能否快速遍历图像有关,需要提前知道,那就是:
出于效率的考虑,OpenCV 可能会给矩阵的每行填补一些额外元素。这是因为,如果行的长度是 4 或 8 的倍数,一些多媒体处理芯片(如 Intel 的 MMX 架构)可以更高效地处理图像。这些额外的像素不会被显示或者保存,填补的值将被忽略。OpenCV将填补后一行的长度指定为关键字。如果图像没有对行进行填补,那么图像的有效宽度就等于图像的真实宽度。
当不对行进行填补的时候,图像可以被视为一个长为 W*H 的一维数组。我们可以通过 cv::Mat 的一个成员函数 isContinuous 来判断这幅图像是否对行进行了填补。如果 isContinuous 方法返回值为真的话,说明这幅图像没有对行进行填补。在一些图像处理算法中,我们可以利用图像的连续性,把整个处理过程使用一个循环完成;
void colorReduce(cv::Mat &image, int div = 64) {
int nl = image.rows; // 行数
int nc = image.cols * image.channels();
if (image.isContinuous()) {
// 没有额外的填补像素
nc = nc * nl;
nl = 1; // it is now a 1D array
}
// 对于连续图像,本循环只执行一次
for (int j = 0; j < nl; j++) {
// 得到第 j 行的首地址
uchar* data = image.ptr<uchar>(j);
for (int i = 0; i < nc; i++) {
data[i] = data[i] / div * div + div / 2;
}
}
}
当我们通过 isContinuous 函数得知图像没有对行进行填补之后,我们就可以将宽设置为 1,高度设置为 W*H,从而消除外层循环。注意,我们也可以使用 reshape 方法来重写这段代码:
if (image.isContinous()) {
// no padded pixels
image.reshape(1, image.cols*image.rows); // 分别是行数和通道数
}
int nl = image.rows; // 列数
int nc = image.cols * image.channels();
reshape 不需要内存拷贝或者重新分配就能改变矩阵的维度。两个参数分别为新的通道数和新的行数。矩阵的列数可以根据新的通道数和行数来自适应。
在这些视线中,内存循环一次处理图像的全部像素。这个方法在同时处理若干个小图像时会很有优势。
底层指针运算
在类 cv::Mat 中,图像数据以 unsigned char 形式保存在一块内存中。这块内存的首地址可以通过 data 成员变量得到。data 是一个 unsigned char 型的指针,uoyi循环可以以如下方式开始:
uchar *data = image.data;
从当前行到下一行可以通过对指针加上行宽完成:
data += image.step; // 下一行
step 代表图像的行宽(包括填补像素)。通常而言,你可以通过如下方式获得第 j 行、第 i 列像素的地址:
// (j, i) 处像素的地址为 &image.at(j, i)
data = image.data + j * image.step + i * image.elemSize();
但是,即使这种方式确实行之有效,我们依然不建议使用这种处理方式。因为这种方式除了容易出错,还不适用于带有“感兴趣区域”的图像。
使用迭代器遍历图像
在面向对象的编程中,遍历数据集合通常是通过迭代器来完成的。迭代器是一种特殊的类,它专门用来遍历集合中的各个元素,同时隐藏了在给定的集合上元素迭代的具体实现方式。这种信息隐蔽原则的使用使得遍历集合更加容易。另外,不管数据类型是什么,我们都可以使用相似的方式遍历集合。标准模板库 STL 为每个容器类型都提供了迭代器,OpenCV 同样为 cv::Mat 提供了与 STL 迭代器兼容的迭代器。
一个 cv::Mat 实例的迭代器可以通过创建一个 cv::MatIterator_ 的实例来得到。类似于子类 cv::Mat_,下划线意味着 cv::MatIterator_ 是一个模板类。之所以如此是由于通过迭代器来存取图像的元素,就必须在编译期知道图像元素的数据类型。一个图像迭代器可以用如下方式声明:
cv::MatIterator_<cv::Vec3b> it;
另外一种方式是使用定义在 Mat_ 内部的迭代器类型:
cv::Mat_<cv::Vec3b>::iterator it;
这样就可以通过常规的 begin 和 end 这两个迭代器方法来遍历所有像素。值得指出的是,如果使用后一种方式,那么 begin 和 end 方法也必须要使用对应的模板化的版本。这样,颜色缩减函数就可以重写为:
void colorReduce(cv::Mat &image, int div = 64) {
// 得到初始位置的迭代器
cv::Mat_<cv::Vec3b>::iterator it = image.begin<cv::Vec3b>();
// 得到终止位置的迭代器
cv::Mat_<cv::Vec3b>::iterator itend = image.end<cv::Vec3b>();
// 遍历所有像素
for (; it != itend; ++it) {
(*it)[0] = (*it)[0] / div * div + div / 2;
(*it)[1] = (*it)[1] / div * div + div / 2;
(*it)[2] = (*it)[2] / div * div + div / 2;
}
}
注意,因为我们这里处理的彩色图像,所以迭代器返回的是 cv::Vec3b,每个颜色分量可以通过操作符 [] 得到。
使用迭代器遍历任何形式的集合都遵循同样的模式。首先,创建一个迭代器特化版本的实例。在我们的示例代码中,就是 cv::Mat_<cv::Vec3b>::iterator (或者 cv::MatIterator_<cv::Vec3b>).
然后,使用集合初始位置(图像的左上角)的迭代器对其进行初始化。初始位置的迭代器通常是通过 begin 方法得到的。对于一个 cv::Mat 的实例,你可以通过 image.begin<cv::Vec3b>() 来得到图像左上角位置的迭代器。你也可以通过对迭代器进行代数运算。例如:如果你想从图像的第二行开始,那么你可以用 image.begin<cv::Vec3b>() + image.rows 来初始化迭代器。集合终止位置的迭代器可以通过 end 方法得到。但是end 方法得到的迭代器其实已经超出了集合。这也意味着迭代过程必须在迭代器到达这个位置时结束。end 方法得到的迭代器也可以进行代数运算。如果,你希望迭代过程在图像最后一行之前停止,那么迭代器的终止位置应该是 image.end<cv::Vec3b>() - image.rows。一旦迭代器初始化完成之后,你就可以创建一个循环遍历所有的元素知道到达终止位置。一个典型的 while 循环如下所示:
while (it != itend) {
// do something
...
++it;
}
操作符 ++ 用来将迭代器从当前位置移动到下一个位置,你也可以使用更大的补偿,比如,用it+=10将迭代器每次移动 10px。
在循环体内部,你可以使用解引用操作符 * 来读写当前元素。都操作使用 element = *it,写操作使用 *it = element。注意:如果你的操作对象是 const cv::Mat,或者你想强调当前循环不会对 cv::Mat 的实例进行修改,那么你就应该创建常量迭代器。常量迭代器的声明如下:
cv::MatConstIterator_<cv::Vec3b> it;
或者
cv::Mat_<cv::Vec3b>::const_iterator it;
在本例中,迭代器的开始位置和终止位置是通过模板函数 begin 和 end 得到的。如果我们在本章第一则秘诀中所做的那样,我们可以通过 cv::Mat_ 的实例来得到他们。这样可以避免在使用 begin 和 end 方法的时候还要置顶迭代器的类型。之所以可以这样,是因为一个 cv::Mat_ 引用在创建的时候就隐式声明了迭代器的类型。
cv::Mat_<cv::Vec3b> cimage = image;
cv::Mat_<cv::Vec3b>::iterator it = cimage.begin();
cv::Mat_<cv::Vec3b>::iterator itend = cimage.end();
之所以这个例子可以而前面那个例子不可以是因为,前面那个例子的图像类型是 cv::Mat, 而这个例子的图像类型是 cv::Mat_。
获取代码运行时间
OpenCV 有一个非常实用的函数 cv::getTickCount() 可以用来测量一段代码的运行时间。这个函数返回从上次开机算起的时钟周期数。由于我们需要的是某个代码段运行的毫秒数,因此还需要另外一个 cv::getTickFrequency()。此函数返回没秒内的时钟周期数,用于统计函数(或一段代码)耗费时间的方法如下:
double duration;
duration = static_cast<double>(cv::getTickCount());
colorReduce(image); // 被测试的函数
duration = static_cast<double>(cv::getTickCount()) - duration;
duration /= cv::getTickFrequency(); // 运行时间,以 ms 为单位
访问方式 | 时间 |
---|---|
data[i] = data[i] / div * div + div / 2; | 37ms |
*data++ = *data / div * div + div / 2; | 37ms |
*data++ = v - v % div + div / 2; | 52ms |
*data++ = *data&mask + div / 2; | 35ms |
colorReduce(input, output); | 44ms |
i<image.cols*image.channels()>; | 65ms |
MatIterator | 67ms |
.at(j,i) | 80ms |
3-channel loop | 29ms |
当输出图像需要被重新分配而不是以原地(in-place)方式处理时(第5行),运行时间为44ms,比 in-place的要慢。额外的时间消耗来自于内存分配。在循环体内存,对于可提前计算的变量应避免重复计算。
图像邻域操作的一个例子
void sharpen(const cv::Mat &image, cv::Mat &result) {
// 如有必要则分配内存
result.create(image.size(), image.type());
for(int j = 1; j < image.rows-1; j++) { // 处理除了第一行和最后一行之外的所有行
const uchar* previous = image.ptr<const uchar>(j-1); // 上一行
const uchar* current = image.ptr<const uchar>(j); // 当前行
const uchar* next = image.ptr<const uchar>(j+1); // 下一行
for(int i = 1; i < image.cols - 1; i++) {
*output++ = cv::saturate_cast<uchar>(5*current[i]-current[i-1]-current[i+1]-previous[i]-next[i]);
}
}
// 将未处理的像素设置为0
result.row(0).setTo(cv::Scalar(0));
result.row(result.rows-1).setTo(cv::Scalar(0));
result.col(0).setTo(cv::Scalar(0));
result.col(result.cols-1).setTo(cv::Scalar(0));
}
在计算输出像素值时,模板函数 cv::saturate_cast 被用来对计算结果进行阶段。
setTo 函数可以用来设置矩阵的值,这个函数会将矩阵的所有元素都设为指定的值。对于一个三通道的彩色图像,需要用 cv::Scalar(a,b,c) 来指定像素三个通道的目标值。
void sharpen2D(const cv::Mat &image, cv::Mat &result) {
// 构造核(所有项都初始化为 0)
cv::Mat kernel(3, 3, CV_32F, cv::Scalar(0));
// 对核元素进行赋值
kernel.at<float>(1,1) = 5.0;
kernel.at<float>(0,1) = -1.0;
kernel.at<float>(2,1) = -1.0;
kernel.at<float>(1,1) = -1.0;
kernel.at<float>(1,2) = -1.0;
// 对图像进行滤波
cv::filter2D(image, result, image.depth(), kernel);
}
- 函数 cv::split 将彩色图像的三个通道分别拷贝到三个独立的 cv::Mat 实例中,然后在对这个通道单独处理。
// 创建一个图像向量
std::vector<cv::Mat> planes;
// 讲一个三通道图像分离为三个单通道图像
cv::split(image1, planes);
planes[0] += image2;
// 将三个单通道图像重新合并为一个三通道图像
cv::merge(planes, result);
提取兴趣区域(其实就是slicing)
imageROI = image(cv::Rect(colId, rowId, logo.cols, logo.rows));
定义ROI的一种方法是使用 cv::Rect,顾名思义,cv::Rect 表示一个矩形区域。指定矩形的左上角坐标(构造函数的前两个参数)和矩形的长宽(构造函数的后两个参数)就可以定义一个矩形区域。
另一种定义ROI的方式是指定感兴趣行或列的范围(Range)。Range是指从起始索引到终止索引(不包含终止索引)的一段连续序列。cv::Range 可以用来定义Range。如果用 cv::Range 来定义 ROI,那么前例中定义 ROI 的代码可以重写为:
cv::Mat imageROI = image(cv::Range(270,270+logo.rows), cv::Range(385,385+logo.cols));
cv::Mat 的 () 操作符返回另一个 cv::Mat 实例,这个实例可以用在接下来的函数调用中,因为ROI和原始图像共享数据缓冲区,对ROI的任何变换都会影响到原始图像的对应区域。由于创建ROI时不会拷贝数据,所以不论ROI的大小如何,创建ROI的运行时间都是常量。
如果想创建包含原始图像特定行的ROI,可以使用如下代码:
cv::Mat imageROI = image.rowRange(start, end);
类似地,对于列:
cv::Mat imageROI = image.colRange(start, end);
在秘诀“遍历图像和邻域操作”中使用到的row方法和col方法其实是rowRange和colRange方法的特例,即起始索引等于终止索引,等于是定义了一个单行或单列的ROI。
<div id="Section6">第6章 图像滤波</div>
- 均值滤波
cv::blur(image, result, cv::Size(5,5));
- 高斯滤波
cv::GaussianBlur(image, result, cv::Size(5,5), 1.5);
这个 1.5 就是高斯函数的$\sigma$,决定高斯函数平坦与否。
- 生成 1 维高斯核
cv::Mat gauss = cv::getGaussianKernel(9, sigma, CV_32F);
9就是一维高斯核向量的长度。
- 先对原图应用低通滤波,然后隔行、隔列取出像素
cv::Mat reducedImage; // 包含缩小后的图像
cv::pyrDown(image, reducedImage); //将图像尺寸减半
同理,还存在 cv::pyrUp 函数将图像尺寸放大一倍。
- 指定目标图像的尺寸
cv::Mat reducedImage; // 包含改变尺寸后的图像
cv::resize(image, reducedImage, cv::Size(image.cols/3, image.rows/3)); // 改变为 1/3 大小
还提到了 cv::boxFilter 和 cv::filter2D 函数
- 中值滤波
cv::medianBlur(image, result, 5);
- Sobel函数
cv::Sobel
cv::minMaxLoc
sobel.convertTo
cv::threshold
cv::cartToPolar
cv::Scharr
<div id="Section7">附录
把附录的内容全部敲下来,因为让你更好地理解OpenCV的组织架构,以及它是什么,能做到什么?还有就是samples/cpp/ 文件夹中的范例介绍,应该有最纯正的OpenCV编程风格,可以用于学习。
OpenCV3的改动在哪?
C风格的API很快将会消失,完全被C++的API替代,代码风格更加简洁,不易出错。读者如果想借助OpenCV最新的功能,记得清理代码中C风格API
C++ API将更加简洁
所有的算法都将继承自 cv::Algorithm 接口
大型的模块拆分为小模块,模块将在后面继续讲解。
OpenCV 3 的源代码文件夹:
- 3rdparty/: 包含第三方库,如用视频解码用的 ffmpeg、jpg、png、tiff 等图片的解码库。
- apps/: 包含进行 Haar 分类器训练的工具,OpenCV 进行人脸检测便是基于 Haar 分类器。如果你想检测人脸以外的图片,千万不要错过这几个工具。
- cmake/: 包含生成工程项目时 cmake 的依赖文件,用于只能搜索第三方库,普通开发者不需要关心这个文件夹的内容。
- data/: 包含 OpenCV 库及范例中用到的资源文件,Haar 物体检测的分类器位于 haarcascades 子文件中。
- doc/: 包含生成文档所需的源文件及辅助脚本。
- include/: 包含入口头文件。OpenCV 子文件夹中是 C 语言风格的 API,也就是《学习 OpenCV》中描述的 API 函数,官方将逐渐淘汰 C 风格函数,因此我不推荐大家使用该文件夹中的头文件。OpenCV 2 子文件夹中只有一个 opencv.hpp 文件,这是 OpenCV 2 及 OpenCV 3 推荐使用的头文件。
- modules/: 包含核心代码,OpenCV 真正的代码都在这个文件夹中。OpenCV 从 2.0 开始以模块的方式组织各种功能,近两年模块的数量增长得很快,后面我会依次介绍每个模块的作用。
- platforms/: 包含交叉编译所需的工具链及额外的代码,交叉编译指的是在一个操作系统中编译供另一个系统使用的文件。
- samples/: 这是大家最喜欢的范例文件夹,后面我也会进一步讲解。
CPU模块
- androidcamera/: 仅用于 Android 平台,使得可以通过与其他平台相同的接口来控制 Android 设备的相机。
- core/: 核心功能模块,定义了基本的数据结构,包括最重要的 Mat 类、XML 读写、OpenGL 三维渲染等。
- imgproc/: 全称为 Image Processing,即图像处理,包括图像滤波、集合图像变换、直方图计算、形状描述子等。图像处理是计算机视觉的重要工具。
- highgui/: 高级图形界面及多媒体文件读写,包括用户界面、Qt、对图像及视频文件的读写操作。
- video/: 视频分析模块,包括背景提取、光流跟踪、卡尔曼滤波等,做视频监控的开发者会经常使用这个模块。
- calib3d/: 相机标定及三维重建。相机标定用于取出相机自身缺陷导致的画面形变,还原真实的场景,确保计算的准确性。三维重建通常用在双目视觉(立体视觉),即两个标定后的摄像头观察同一个场景,通过计算两幅画面中的相关性来估计像素深度。
- features2d/: 包含 2D 特征值检测的框架。包含各种特征值检测器及描述子,如 FAST、MSER、OBRB、BRISK 等。各类特征值拥有统一的算法接口,因此在不影响程序逻辑的情况下可以替换替换。
- objdetect/: 物体检测模块,包括 Haar 分类器、SVM 检测器及文字检测。
- ml/: 全称为 Machine Learning,即机器学习。包括统计模型、K 最近邻、支持向量机、决策树、神经网络等经典的机器学习算法。
- flann/: 用于在多维空间内聚类及搜索的近似算法,做图像检索的开发者对它不会陌生。
- photo/: 计算摄影学,包括图像修补、去噪、HDR 成像、非真实感渲染等。如果读者想实现 Photoshop 的高级功能,那么这个模块必不可少。
- stitching:/ 图像拼接,可用于制作全景图。
- nonfree/: 受专利保护的算法,包括 SIFT 和 SURF。从功能上来说,这两个算法属于 features2d 模块,但由于它们都是受专利保护的,相拥在项目中可能需要专利方的许可。
- contrib:/ 包含新添加的实验性质的代码。开发者期待已久的人脸识别功能便位于这个模块内,名为 FaceRecognizer。
- legacy/: 英文含义为遗产,即废弃已久的代码,官方不推荐使用这个模块中的功能。
- optim/: 全称为 Optimization,这个模块包含通用的数值优化。包含线性规划等算法。
- shape/: 形状匹配算法模块,用于描述形状、比较形状。
- softcascade/: 另一种物体检测算法,Soft Cascade 分类器,包含检测模块和训练模块。
- superres/: 全称为 Super Resolution,用于增强图像的分辨率。
- videostab/: 全称为 Video Stabilization,用于解决相机移动拍摄时视频不够稳定的问题。
- viz/: 三维可视化模块。可以认为这个模块实现了一个简单的三维可视化引擎,有各种 UI 空间和键盘、鼠标交互方式。底层实现基于 CTK 这个第三方库。
CUDA模块
这些模块的名称都以 cuda 开始,cuda 是显卡制造商 NVIDIA 推出的通用计算语言,在 OpenCV 3 中有大量的模块已经被移植到了 cuda 语言。让我们依次看一下。
- cuda/: CUDA- 加速的计算机视觉算法,包括数据结构 cuda::GpuMat、基于 cuda 的相机标定及三维重建等。
- cudaarithm/: CUDA- 加速的矩阵运算模块。
- cudabgsegm/: CUDA- 加速的背景分割模块,通常用于视频监控。
- cudacodec/: CUDA- 加速的视频编码与解码。
- cudafeatures2d/: CUDA- 加速的特征检测与描述模块,与 features2d/ 模块功能类似。
- cudafilters/: CUDA- 加速的图像滤波。
- cudaimgproc/: CUDA- 加速的图像处理算法,包含直方图计算、霍夫变换等。
- cudaoptflow/: CUDA- 加速的光流检测算法。
- cudastereo/: CUDA- 加速的立体视觉匹配算法。
- cudawarping/: 实现 CUDA- 加速的快速图像变换,包括透视变换、旋转、改变尺寸等。
samples/ 文件夹
- android/: Android 平台的范例。既有完全是 Java 的工程,也有完全是 C++ 的工程,也有更为常见的 Java 与 C++ 共存的工程。
- c/: 使用 C API 的范例。在 C API 逐渐退出历史舞台后,这个文件夹也应该会随之消失吧。
- cpp/: 由于 OpenCV 是一款 C++ 库,因此 C++ 的返利是最多的,后面将重点介绍。
- directx/: directx (d3d) 是微软的私有三维图像 API,这个文件夹中的范例覆盖了 d3d9、d3d10、d3d11.
- gpu/: 利用 cuda 加速的范例。
- java/: OpenCV 3 官方支持 Java 语言绑定,因此这里演示如何使用 Java 版本的 OpenCV。
- python2/: OpenCV 3 官方支持 Python 语言绑定,因此这里演示使用 Python 2 版本的范例。
- tapi/: tapi 是 OpenCV 3 的一个新特性,使用 cv::UMat 替代 cv::Mat,实现 CPU 和 GPU 的运算使用统一的接口,不再需要显式地在 CPU 和 GPU 之间传递数据,方便开发人员。
- winrt/: Windows RT 平台的范例,开发语言是微软的 C++ “方言”.
samples/cpp/ 文件夹中的范例介绍
3calibration.cpp/: 同时标定三台水平放置的相机。
bagofwords_classification.cpp/: 使用图像检测实现简易的图像搜索功能。
bgfg_gmg.cpp/: 演示 GMG 背景检测算法的使用方式。
bgfg_segm.cpp/: 演示高斯混合背景检测算法的使用方式。
brief_match_test.cpp/: 使用 BRIEF 特征值来匹配两张图像。
build3dmodel.cpp/: 演示如何使用基础矩阵和特征值来创建三维模型。
calibration.cpp/: 完整的多用途标定程序。
calibration_artificial.cpp/: 在程序中生成一个虚拟的相机,并进行标定。
camshiftdemo.cpp/: 读取实时的摄像头数据,并演示基于均值偏移算法的视频跟踪。
chamfer.cpp/: 使用 Chamfer 算法匹配两副边缘图像。
cloning_demo.cpp/: 命令行模式的图像克隆。
cloning_gui.cpp/: 图形界面交互的图像克隆。
connected_components.cpp/: 查找并绘制图像中的连通区域。
contours2.cpp/: 查找并绘制图像中的轮廓。
convexhull.cpp/: 查找并绘制由点的集合组成的凸包。
cout_mat.cpp/: 使用 cout 来输出各种格式化的 Mat 对象。
create_mask.cpp/: 演示如何创建黑白掩码图像。
dbt_face_detection.cpp/: 基于检测的人脸跟踪代码。
delaunay2.cpp/: 通过鼠标交互式地生成 Delaunay 三角形。
demhist.cpp/: 演示直方图的用法。
descriptor_extractor_matcher.cpp/: 演示 features2d 检测框架的用法。
detection_based_tracker_sample.cpp/: 与 dbt_face_detection.cpp 类似。
detector_descriptor_evaluation.cpp/: 评估各种特征检测器和描述子。
detector_descriptor_matcher_evaluation.cpp/: 评估各种特征检测器和匹配器。
dft.cpp/: 演示一幅图像的离散傅里叶变换。
distrans.cpp/: 显示边缘图像的距离变换值。
drawing.cpp/: 演示绘画和文字显示功能。
edge.cpp/: 演示 Canny 边缘检测。
em.cpp/: 对随机生成的数据点进行 EM 聚类。
fabmap_sample.cpp/: 演示 FAB-MAP 图像检索算法。
facerec_demo.cpp/: 人脸识别。
fback.cpp/: 实时的 Farneback 光流跟踪。
ffilldemo.cpp/: 演示 floodFill() 像素填充算法。
filestorage.cpp/: 演示序列化到外部文件,如yml、xml等。
fitellipse.cpp/: 将轮廓点匹配到椭圆。
freak_demo.cpp/: 演示 FREAK 特征值的用法。
gencolors.cpp/: 演示 generateColors()。
generic_descriptor_match.cpp/: 基于 SURF 的两幅图像间的匹配。
grabcut.cpp/: 演示 GrabCut 分割算法。
houghcircles.cpp/: 用霍夫算法检测圆。
houghlines.cpp/: 用霍夫算法检测直线。
hybridtrackingsample.cpp/: 混合跟踪算法(Hybrid Tracker)的演示。
image.cpp/: 来回转换 cv::Mat 和 IplImage。
image_alignment.cpp/: 演示 findTransformECC() 函数。
image_sequence.cpp/: 使用 VideoCapture 对象读取序列帧。
imagelist_creator.cpp/: 创建图像列表到 xml 文件。
inpaint.cpp/: 使用鼠标交互地进行图像修补。
intelperc_capture.cpp/: Intel 感知计算设备相关的函数。
kalman.cpp/: 使用卡尔曼滤波进行二维跟踪。
kmeans.cpp/: Kmeans 聚类算法的演示。
laplace.cpp/: 拉普拉斯边缘检测。
latentsvm_multidetect.cpp/: latentSVM 检测器。
letter_recog.cpp/: 字母识别。
linemod.cpp/: 基于 OpenNI 的体感设备应用。
lkdemo.cpp/: 演示Lukas-Kanade 光流法。
logpolar_bsm.cpp/: 演示 LogPolar 盲点模型。
lsd_lines.cpp/: LSD 线段检测。
matcher_simple.cpp/: SURF 特征检测。
matching_to_many_images.cpp/: 一对多的特征检测。
meanshift_segmentation.cpp/: 演示基于均值漂移的色彩分割函数——meanShiftSegmentation()。
minarea.cpp/: 寻找最小包围盒、包围圆。
morphology2.cpp/: 形态学图像处理。
npr_demo.cpp/: 演示各种非真实感渲染效果。
opencv_version.cpp/: 输出 OpenCV 库的版本号。
openni_capture.cpp/: 演示 OpenNI 相关的体感设备。
pca.cpp/: 基于 PCA 的人脸识别。
peopledetect.cpp/: 基于 cascade 或 hog 进行物体(人)检测。
phase_corr.cpp/: 演示 phaseCorrelate() 函数。
points_classifier.cpp/: 演示各种机器学习算法。
rgbdodometry.cpp/: 对深度传感器如 Kinect 的数据进行处理。
segment_objects.cpp/: 实时地在视频或相机画面中检测前景物体。
shape_example.cpp/: 比较并检索形状。
shape_transformation.cpp/: 用 SURF 特征值检测形状并进行变换。
squares.cpp/: 检测图像中的方块形状。
starter_imagelist.cpp/: 一个 “hello worl” 性质的入门范例。
starter_video.cpp/: 另一个 “hello worl” 性质的入门范例。
stereo_calib.cpp/: 双目视觉的标定。
stereo_match.cpp/: 计算左右视觉的图像的差异,生成点云文件。
stitching.cpp/: 演示图像拼接算法。
stitching_detailed.cpp/: 演示更多参数的图像拼接算法。
textdetection.cpp/: 实时场景中的文字定位与识别。
train_HOG.cpp/: 训练 HOG 分类器。
ufacedetect.cpp/: 人脸检测。
video_homography.cpp/: 使用 FAST 特征值来跟踪平面物体。
videostab.cpp/: 演示 videostab 中各个参数的用法。
watershed.cpp/: 演示著名的分水岭图像分割算法。
本书程序代码及彩图下载:
http://www.sciencep.com/downloads/
https://github.com/ITpublishing
<div id="Section8">勘误</div>
- P249 页,倒数第 5-6 行,分别有两个“图像中”多余。
<div id="Section9">我的困惑</div>
- 深拷贝 image.clone() 和 copyTo 有什么区别?不是一样的吗
<div id="Section10">下一步计划</div>
初写于 2015-04-05,未完待续。
首发于 Yimian Dai's Homepage,转载请注明出处。