2015-04-05-《OpenCV 2 计算机视觉编程手册》读书笔记

鉴于中文语境下,学习 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,转载请注明出处。

推荐阅读更多精彩内容