美文网首页iOS 图像处理图像处理opencv
2015-04-05-《OpenCV 2 计算机视觉编程手册》读

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

作者: YimianDai | 来源:发表于2015-08-25 10:55 被阅读3046次

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

    相关文章

      网友评论

      • Warren_Liu:我发现一个错误,第二行的开始的迭代器指针应该是image.begin<cv::Vec3b> + image.cols
      • 64ca281c82d8:我们公司蒜泥科技正在做三维人像扫描仪。总部西安,深圳是软件研发中心,邀请各路三维重建算法大神加入蒜泥,一起做最任性的技术最牛逼的事业!如有兴趣,邮件沟通wangjun@suanier.com

      本文标题:2015-04-05-《OpenCV 2 计算机视觉编程手册》读

      本文链接:https://www.haomeiwen.com/subject/dklqcttx.html