OpenCV 图像处理学习手册(二)

原文:Learning Image Processing with OpenCV

协议:CC BY-NC-SA 4.0

四、处理色彩

颜色是响应于我们的视觉系统的激发而产生的感知结果,入射光是在光谱的可见光区域中入射到视网膜上的。 图像的颜色可能包含大量信息,这些信息可用于简化图像分析,对象识别和基于颜色的提取。 通常在考虑定义其的色彩空间中的像素值的情况下执行这些过程。 本章将讨论以下主题:

  • OpenCV 中使用的色彩空间以及如何将图像从一种色彩模型转换为另一种色彩模型
  • 考虑定义图片色彩空间的方式如何对图片进行分割的示例
  • 如何使用颜色转移方法将图像的外观转移到另一个

色彩空间

人类的视觉系统能够分辨出数十万种颜色。 为了获得此信息,人类视网膜具有三种类型的彩色感光锥体细胞,它们对入射辐射作出响应。 因此,可以使用称为原色的三个数字分量生成大多数人的颜色感知。

为了根据三个或更多个特定特性来指定颜色,存在许多称为颜色空间颜色模型的方法。 在它们之间代表图像进行选择取决于要执行的操作,因为根据所需的应用,某些操作更合适。 例如,在某些颜色空间(例如 RGB)中,亮度会影响三个通道,这对于某些图像处理操作可能是不利的。 下一节将说明 OpenCV 中使用的色彩空间,以及如何将图片从一种色彩模型转换为另一种色彩模型。

颜色空间之间的转换(cvtColor

OpenCV 中有 150 多种颜色空间转换方法。 OpenCV 在imgproc模块中提供的函数是void cvtColor(InputArray src, OutputArray dst, int code, int dstCn=0)。 该函数的参数为​​:

  • src:这是 8 位无符号,16 位无符号(CV_16UC)或单精度浮点的输入图像。
  • dst:此是与src相同大小和深度的输出图像。
  • code:此是色彩空间转换代码。 该参数的结构为COLOR_SPACEsrc2SPACEdst。 一些示例值是COLOR_BGR2GRAYCOLOR_YCrCb2BGR
  • dstCn:这是目标图像中的通道数。 如果此参数为 0 或省略,则通道数自动从srccode派生。

此函数的示例将在接下来的部分中进行介绍。

提示

cvtColor函数只能从 RGB 转换为另一个颜色空间,或从另一个颜色空间转换为 RGB,因此,如果读者想在 RGB 以外的两个颜色空间之间转换,则必须先进行到 RGB 的转换。

OpenCV 中的各种色彩空间将在接下来的部分中进行讨论。

RGB

RGB 是可加模型,其中图像由三个独立图像平面或通道组成:红色,绿色和蓝色(以及可选的透明度的第四个通道,有时称为 alpha 通道)。 为了指定特定的颜色,每个值指示每个像素上存在的每种成分的数量,较高的值对应于较亮的像素。 由于该色彩空间对应于人眼的三个感光器,因此被广泛使用。

注意

OpenCV 中的默认颜色格式通常称为 RGB,但实际上将其存储为 BGR (通道相反)。

示例代码

以下BGRsplit示例向您展示了如何加载 RGB 图像,如何以灰色和彩色分割并显示每个特定通道。 代码的第一部分用于加载和显示图片:

#include <opencv2/opencv.hpp>
#include <opencv2/imgproc.hpp>

using namespace std;
using namespace cv;

vector<Mat> showSeparatedChannels(vector<Mat> channels);

int main(int argc, const char** argv)
{
    //Load the image
    Mat image = imread("BGR.png");
    imshow("Picture",image);

代码的下一部分将图片分成每个通道并显示:

    vector<Mat> channels;

    split( image, channels );

    //show channels in gray scale
    namedWindow("Blue channel (gray)", WINDOW_AUTOSIZE );
    imshow("Blue channel (gray)",channels[0]);
    namedWindow("Green channel (gray)", WINDOW_AUTOSIZE );
    imshow("Green channel (gray)",channels[1]);
    namedWindow("Red channel (gray)", WINDOW_AUTOSIZE );
    imshow("Red channel (gray)",channels[2]);

    //show channels in BGR
    vector<Mat> separatedChannels=showSeparatedChannels(channels);

    namedWindow("Blue channel", WINDOW_AUTOSIZE );
    imshow("Blue channel",separatedChannels[0]);
    namedWindow("Green channel", WINDOW_AUTOSIZE );
    imshow("Green channel",separatedChannels[1]);
    namedWindow("Red channel", WINDOW_AUTOSIZE );
    imshow("Red channel",separatedChannels[2]);

    waitKey(0);

    return 0;
}

值得注意使用void split(InputArray m, OutputArrayOfArrays mv) OpenCV 函数将图像m分成三个通道,并将其保存在称为mv的向量中。 相反,void merge( InputArrayOfArrays mv, OutputArray dst)函数用于将所有mv通道合并到一个dst图像中。 此外,命名为showSeparatedChannels的函数用于创建代表每个通道的三个彩色图像。 对于每个通道,该函数都会生成vector<Mat> aux,该vector<Mat> aux由通道本身和两个辅助通道组成,它们的所有值均设置为 0,表示颜色模型的其他两个通道。 最后,辅助图片被合并,生成仅满足一个通道的图像。 该函数代码将在本章的其他示例中使用,如下所示:

vector<Mat> showSeparatedChannels(vector<Mat> channels){
    vector<Mat> separatedChannels;
    //create each image for each channel
    for ( int i = 0 ; i < 3 ; i++){
        Mat zer=Mat::zeros( channels[0].rows, channels[0].cols, channels[0].type());
        vector<Mat> aux;
        for (int j=0; j < 3 ; j++){
            if(j==i)
                aux.push_back(channels[i]);
            else
                aux.push_back(zer);
        }

        Mat chann;
        merge(aux,chann);

        separatedChannels.push_back(chann);
    }
    return separatedChannels;
}

下图显示了示例的输出:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_04_1.jpg

原始 RGB 图像和通道分割

灰度

在灰度中,每个像素的值表示为仅包含强度信息的单个值,它构成了一个由不同灰度组成的图像。 使用cvtColor在 OpenCV 中在 RGB 和灰度(Y)之间转换的颜色空间转换代码是COLOR_BGR2GRAYCOLOR_RGB2GRAYCOLOR_GRAY2BGRCOLOR_GRAY2RGB。 这些转换的内部计算如下:

注意

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_04_12.jpg

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_04_13.jpg

请注意,根据上式中的,不可能直接从灰度图像中检索颜色。

示例代码

Gray示例之后的显示了如何将 RGB 图像转换为灰度,并显示了两张图片。 示例代码为:

#include <opencv2/opencv.hpp>
#include <opencv2/imgproc.hpp>

using namespace cv;

int main(int argc, const char** argv)
{
    //Load the image
    Mat image = imread("Lovebird.jpg");
    namedWindow("Picture", WINDOW_AUTOSIZE );
    imshow("Picture",image);

    Mat imageGray;
    cvtColor(image, imageGray, COLOR_BGR2GRAY);

    namedWindow( "Gray picture", WINDOW_AUTOSIZE );
    imshow("Gray picture",imageGray);

    waitKey(0);
    return 0;
}

下图显示了代码的输出:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_04_2.jpg

原始 RGB 图像和灰度转换

注意

从 RGB 转换为灰度的方法的缺点是失去了原始图像的对比度。 本书的第 6 章,“计算摄影”描述了脱色过程,该过程在克服此问题的同时进行了相同的转换。

CIE XYZ

CIE XYZ 系统使用亮度分量 Y 来描述颜色,该亮度分量与人眼的亮度灵敏度有关,并且由国际照明委员会CIE)使用来自几个人类观察者的实验统计数据,标准化了另外两个通道 X 和 Z。 此颜色空间用于报告测量仪器(例如比色计或分光光度计)的颜色,当需要跨不同设备进行一致的颜色表示时,该颜色空间将非常有用。 这种颜色空间的主要问题是颜色以不均匀的方式缩放。 这一事实导致 CIE 采用 CIE Lab 和 CIE Luv 颜色模型。

使用cvtColor在 OpenCV 中在 RGB 和 CIE XYZ 之间转换的色彩空间转换代码是COLOR_BGR2XYZCOLOR_RGB2XYZCOLOR_XYZ2BGRCOLOR_XYZ2RGB。 这些转换的计算如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_04_14.jpg

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_04_15.jpg

示例代码

CIExyz示例之后的显示了如何将 RGB 图像转换为 CIE XYZ 颜色空间,并分别以灰色和彩色显示并显示每个特定通道。 代码的第一部分用于加载和转换图片:

#include <opencv2/opencv.hpp>
#include <opencv2/imgproc.hpp>
using namespace std;
using namespace cv;

vector<Mat> showSeparatedChannels(vector<Mat> channels);

int main(int argc, const char** argv)
{
    //Load the image
    Mat image = imread("Lovebird.jpg");
    imshow("Picture",image);

    //transform to CIEXYZ
    cvtColor(image,image,COLOR_BGR2XYZ);

代码的下一部分在每个 CIE XYZ 通道中拆分图片并显示它们:

    vector<Mat> channels;

    split( image, channels );

    //show channels in gray scale
    namedWindow("X channel (gray)", WINDOW_AUTOSIZE );
    imshow("X channel (gray)",channels[0]);
    namedWindow("Y channel (gray)", WINDOW_AUTOSIZE );
    imshow("Y channel (gray)",channels[1]);
    namedWindow("Z channel (gray)", WINDOW_AUTOSIZE );
    imshow("Z channel (gray)",channels[2]);

    //show channels in BGR
    vector<Mat> separatedChannels=showSeparatedChannels(channels);

    for (int i=0;i<3;i++){      cvtColor(separatedChannels[i],separatedChannels[i],COLOR_XYZ2BGR);
    }
    namedWindow("X channel", WINDOW_AUTOSIZE );
    imshow("X channel",separatedChannels[0]);
    namedWindow("Y channel", WINDOW_AUTOSIZE );
    imshow("Y channel",separatedChannels[1]);
    namedWindow("Z channel", WINDOW_AUTOSIZE );
    imshow("Z channel",separatedChannels[2]);

    waitKey(0);

    return 0;
}

下图显示了代码的输出:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_04_3.jpg

原始 RGB 图像和 CIE XYZ 通道分割

YCrCb

此色彩空间广泛用于视频和图像压缩方案,它不是绝对的色彩空间,因为它是对 RGB 色彩空间进行编码的一种方式。 Y 通道表示亮度,而 Cr 和 Cb 表示红色差异(RGB 色彩空间中的 R 通道与 Y 之间的差异)和蓝色差异(RGB 色彩空间中的 B 通道与 Y 之间的差异)色度分量。 它广泛用于视频和图像压缩方案,例如 MPEG 和 JPEG。

使用cvtColor在 OpenCV 中在 RGB 和 YCrCb 之间进行转换的颜色空间转换代码是COLOR_BGR2YCrCbCOLOR_RGB2YCrCbCOLOR_YCrCb2BGRCOLOR_YCrCb2RGB。 这些转换的计算如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_04_16.jpg

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_04_17.jpg

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_04_18.jpg

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_04_19.jpg

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_04_20.jpg

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_04_21.jpg

然后,以和来看以下内容:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_04_22.jpg

示例代码

下面的 YCrCb 颜色示例向您展示如何将 RGB 图像转换为 YCrCb 颜色空间,并以灰色和一种颜色拆分并显示每个特定的通道。 代码的第一部分用于加载和转换图片:

#include <opencv2/opencv.hpp>
#include <opencv2/imgproc.hpp>

using namespace std;
using namespace cv;

vector<Mat> showSeparatedChannels(vector<Mat> channels);

int main(int argc, const char** argv)
{
    //Load the image
    Mat image = imread("Lovebird.jpg");
    imshow("Picture",image);

    //transform to YCrCb
 cvtColor(image,image,COLOR_BGR2YCrCb);

代码的下一部分将图片分成每个 YCrCb 通道,并显示它们:

    vector<Mat> channels;

 split( image, channels );

    //show channels in gray scale
    namedWindow("Y channel (gray)", WINDOW_AUTOSIZE );
    imshow("Y channel (gray)",channels[0]);
    namedWindow("Cr channel (gray)", WINDOW_AUTOSIZE );
    imshow("Cr channel (gray)",channels[1]);
    namedWindow("Cb channel (gray)", WINDOW_AUTOSIZE );
    imshow("Cb channel (gray)",channels[2]);

    //show channels in BGR
 vector<Mat> separatedChannels=showSeparatedChannels(channels);

    for (int i=0;i<3;i++){
        cvtColor(separatedChannels[i],separatedChannels[i],COLOR_YCrCb2BGR);
    }
    namedWindow("Y channel", WINDOW_AUTOSIZE );
    imshow("Y channel",separatedChannels[0]);
    namedWindow("Cr channel", WINDOW_AUTOSIZE );
    imshow("Cr channel",separatedChannels[1]);
    namedWindow("Cb channel", WINDOW_AUTOSIZE );
    imshow("Cb channel",separatedChannels[2]);

    waitKey(0);

    return 0;
}

下图显示了代码的输出:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_04_4.jpg

原始 RGB 图像和 YCrCb 通道分割

HSV

HSV 颜色空间属于所谓的面向色相的颜色坐标系。 这种颜色模型与人类颜色感知模型非常相似。 在其他颜色模型(如 RGB)中,图像被视为三种基色的相加结果,而 HSV 的三个通道代表色相(H 给出了颜色的光谱组成的一个度量),饱和度(S 表示主波长的纯光比例,它表示颜色与相等亮度的灰色有多远)和(V 给出相对于类似照亮的白色颜色的亮度的亮度),对应于色调,阴影和色调的直观吸引力。 HSV 被广泛用​​于进行颜色比较,因为 H 几乎是独立的光线变化。 下图显示了此颜色模型,该颜色模型将每个通道表示为圆柱体的一部分:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_04_30.jpg

用于使用cvtColor在 OpenCV 中的 RGB 和 HSV 之间进行转换的颜色空间转换代码是COLOR_BGR2HSVCOLOR_RGB2HSVCOLOR_HSV2BGRCOLOR_HSV2RGB。 在这种情况下,值得注意的是,如果src图像格式为 8 位或 16 位,则cvtColor首先将其转换为浮点格式,并在 0 和 1 之间缩放值。 转换计算如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_04_23.jpg

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_04_24.jpg

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_04_25.jpg

如果H < 0,则H = H + 360。 最后,值将转换为目标数据类型。

示例代码

下面的HSVcolor示例向您展示如何将 RGB 图像转换为 HSV 色彩空间,并以灰度和 HSV 图像拆分和显示每个特定通道。 示例代码为:

#include <opencv2/opencv.hpp>
#include <opencv2/imgproc.hpp>

using namespace std;
using namespace cv;

int main(int argc, const char** argv)
{
    //Load the image
    Mat image = imread("Lovebird.jpg");
    imshow("Picture",image);

    //transform to HSV
 cvtColor(image,image,COLOR_BGR2HSV);

    vector<Mat> channels;

 split( image, channels );

    //show channels in gray scale
    namedWindow("H channel (gray)", WINDOW_AUTOSIZE );
    imshow("H channel (gray)",channels[0]);
    namedWindow("S channel (gray)", WINDOW_AUTOSIZE );
    imshow("S channel (gray)",channels[1]);
    namedWindow("V channel (gray)", WINDOW_AUTOSIZE );
    imshow("V channel (gray)",channels[2]);

    namedWindow("HSV image (all channels)", WINDOW_AUTOSIZE );
    imshow("HSV image (all channels)",image);

    waitKey(0);

    return 0;
}

下图显示了代码的输出:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_04_5.jpg

原始 RGB 图像,HSV 转换和通道分割

注意

OpenCV 的imshow函数假定要显示的图像颜色是 RGB,因此显示不正确。 如果您在其他颜色空间中有图像,并且想要正确显示,则首先必须将其转换回 RGB。

HLS

HLS 颜色空间属于面向色相的颜色坐标系统,例如先前说明的 HSV 颜色模型。 开发该模型以指定每个通道中的色相,亮度和颜色饱和度的值。 HSV 颜色模型的区别在于,HLS 定义的纯色的亮度等于中等灰色的亮度,而 HSV 定义的纯色的亮度等于白色的亮度。

使用cvtColor在 OpenCV 中在 RGB 和 HLS 之间进行转换的颜色空间转换代码为COLOR_BGR2HLSCOLOR_RGB2HLSCOLOR_HLS2BGRCOLOR_HLS2RGB。 在这种情况下,与 HSV 一样,如果src图像格式为 8 位或 16 位,则cvtColor首先将其转换为浮点格式,将值缩放到 0 到 1 之间。然后,转换计算如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_04_26.jpg

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_04_33.jpg

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_04_27.jpg

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_04_28.jpg

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_04_29.jpg

如果H < 0,则H = H + 360。 最后,将值重新转换为目标数据类型。

示例代码

以下HLScolor示例向您展示如何将 RGB 图像转换为 HLS 色彩空间,如何拆分和显示灰度中的每个特定通道以及 HLS 图像。 示例代码为:

#include <opencv2/opencv.hpp>
#include <opencv2/imgproc.hpp>

using namespace std;
using namespace cv;

int main(int argc, const char** argv)
{
    //Load the image
    Mat image = imread("Lovebird.jpg");
    imshow("Picture",image);

    //transform to HSV
 cvtColor(image,image,COLOR_BGR2HLS);

    vector<Mat> channels;

 split( image, channels );

    //show channels in gray scale
    namedWindow("H channel (gray)", WINDOW_AUTOSIZE );
    imshow("H channel (gray)",channels[0]);
    namedWindow("L channel (gray)", WINDOW_AUTOSIZE );
    imshow("L channel (gray)",channels[1]);
    namedWindow("S channel (gray)", WINDOW_AUTOSIZE );
    imshow("S channel (gray)",channels[2]);

    namedWindow("HLS image (all channels)", WINDOW_AUTOSIZE );
    imshow("HLS image (all channels)",image);

    waitKey(0);

    return 0;
}

下图显示了代码的输出:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_04_6.jpg

原始 RGB 图像,HLS 转换和通道分割

CIE Lab

CIE Lab 颜色空间是在 CIE Luv 之后由 CIE 标准化的第二个均匀颜色空间,它是基于 CIE XYZ 空间和白色参考点得出的。 实际上,它是 CIE 指定的最完整的色彩空间,其创建是与设备无关的,例如 CYE XYZ 模型,并用作参考。 它能够描述人眼可见的颜色。 这三个通道代表颜色的亮度(L),品红色和绿色之间的位置(a),以及黄色和蓝色之间的位置(b)。

使用cvtColor在 OpenCV 中在 RGB 和 CIE Lab 之间进行转换的色彩空间转换代码是COLOR_BGR2LabCOLOR_RGB2LabCOLOR_Lab2BGRCOLOR_Lab2RGB。 在这个页面中解释了用于计算这些转换的过程。

示例代码

CIElab示例之后的显示了如何将 RGB 图像转换为 CIE Lab 色彩空间,以灰度和 CIE Lab 分割并显示图片的每个特定通道。 示例代码为:

#include <opencv2/opencv.hpp>
#include <opencv2/imgproc.hpp>

using namespace std;
using namespace cv;

int main(int argc, const char** argv)
{
    //Load the image
    Mat image = imread("Lovebird.jpg");
    imshow("Picture",image);

    //transform to CIE Lab
 cvtColor(image,image,COLOR_BGR2Lab);

    vector<Mat> channels;

 split( image, channels );

    //show channels in gray scale
    namedWindow("L channel (gray)", WINDOW_AUTOSIZE );
    imshow("L channel (gray)",channels[0]);
    namedWindow("a channel (gray)", WINDOW_AUTOSIZE );
    imshow("a channel (gray)",channels[1]);
    namedWindow("b channel (gray)", WINDOW_AUTOSIZE );
    imshow("b channel (gray)",channels[2]);

    namedWindow("CIE Lab image (all channels)", WINDOW_AUTOSIZE );
    imshow("CIE Lab image (all channels)",image);

    waitKey(0);

    return 0;
}

下图显示了代码的输出:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_04_7.jpg

原始 RGB 图像,CIE Lab 转换和通道分割

CIE Luv

CIE Luv 颜色空间是 CIE 标准化的第一个统一颜色空间。 它是 CIE XYZ 空间和白色参考点的简单计算转换,尝试进行感知均匀性。 类似于 CIE Lab 颜色空间,它的创建与设备无关。 三个通道代表颜色的亮度(L)及其在绿色和红色之间的位置(u),最后一个通道主要代表蓝色和紫色类型的颜色(v)。 该颜色模型具有线性加和特性,因此可用于灯光的添加剂混合物。

使用cvtColor在 OpenCV 中在 RGB 和 CIE Luv 之间进行转换的颜色空间转换代码是COLOR_BGR2LuvCOLOR_RGB2LuvCOLOR_Luv2BGRCOLOR_Luv2RGB。 可以在这个页面上看到用于计算这些转换的过程。

示例代码

CIELuvcolor示例之后的显示了如何将 RGB 图像转换为 CIE Luv 色彩空间,以灰度和 CIE Luv 分割并显示图片的每个特定通道。 示例代码为:

#include <opencv2/opencv.hpp>
#include <opencv2/imgproc.hpp>

using namespace std;
using namespace cv;

int main(int argc, const char** argv)
{
    //Load the image
    Mat image = imread("Lovebird.jpg");
    imshow("Picture",image);

    //transform to CIE Luv
 cvtColor(image,image,COLOR_BGR2Luv);

    vector<Mat> channels;

 split( image, channels );

    //show channels in gray scale
    namedWindow("L channel (gray)", WINDOW_AUTOSIZE );
    imshow("L channel (gray)",channels[0]);
    namedWindow("u channel (gray)", WINDOW_AUTOSIZE );
    imshow("u channel (gray)",channels[1]);
    namedWindow("v channel (gray)", WINDOW_AUTOSIZE );
    imshow("v channel (gray)",channels[2]);

    namedWindow("CIE Luv image (all channels)", WINDOW_AUTOSIZE );
    imshow("CIE Luv image (all channels)",image);

    waitKey(0);

    return 0;
}

下图显示了代码的输出:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_04_8.jpg

原始 RGB 图像,CIE Luv 转换和通道分割

拜耳

拜耳像素空间合成被广泛用于带有仅一个图像传感器的数码相机。 与具有三个传感器的相机(每个 RGB 通道一个传感器,可以获取特定组件的所有信息)不同,在一台传感器相机中,每个像素都被一个不同的滤色镜覆盖,因此每个像素仅以此颜色进行测量。 使用拜耳方法从其邻居中推断出丢失的颜色信息。 它使您可以从一个像素交错的单一平面中获取完整的彩色图片,如下所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_04_31.jpg

拜耳模式示例

注意

请注意,拜耳图案由比 R 和 B 多的 G 像素表示,因为人眼对绿色频率更敏感。

通过将图案在任何方向上移动一个像素,可以获得对所示图案的几种修改。 在 OpenCV 中将 Bayer 转换为 RGB 的色彩空间转换代码是将第二行的第二列和第三列(分别为XY)的组件定义为COLOR_BayerXY2BGR的。 例如,前一张图片的图案具有“BG”类型,因此其转换代码为COLOR_BayerBG2BGR

示例代码

以下Bayer示例向您展示如何将由从图像传感器获得的 RG Bayer 模式定义的图片转换为 RGB 图像。 示例代码为:

#include <opencv2/opencv.hpp>
#include <opencv2/imgproc.hpp>

using namespace cv;

int main(int argc, const char** argv)
{
    //Show bayered image in color
    Mat bayer_color = imread("Lovebird_bayer_color.jpg");
    namedWindow("Bayer picture in color", WINDOW_AUTOSIZE );
    imshow("Bayer picture in color",bayer_color);

    //Load bayered image
    Mat bayer = imread("Lovebird_bayer.jpg",CV_8UC3);
    namedWindow("Bayer picture ", WINDOW_AUTOSIZE );
    imshow("Bayer picture",bayer);

    Mat imageColor;
    cvtColor(bayer, imageColor, COLOR_BayerRG2BGR);

    namedWindow( "Color picture", WINDOW_AUTOSIZE );
    imshow("Color picture",imageColor);

    waitKey(0);
    return 0;
}

下图显示了代码的输出:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_04_32.jpg

拜耳图案图像和 RGB 转换

基于颜色空间的分割

每个颜色空间代表一个图像,该图像指示每个像素上每个通道测得的特定特性的数值。 考虑到这些特性,可以使用线性边界(例如,三维空间中的平面和每个通道一个空间)对颜色空间进行分区,从而可以根据其所在的分区对每个像素进行分类,因此可以选择一组具有预定义特性的像素。 这个想法可以用来分割我们感兴趣的图像对象。

OpenCV 提供void inRange(InputArray src, InputArray lowerb, InputArray upperb, OutputArray dst)函数来检查元素数组是否位于其他两个数组的元素之间。 对于基于色彩空间的分割,此函数可让您获得src图像的像素集,其通道值位于lowerb下边界和upperb上边界之间,从而获得dst图片。

注意

lowerbupperb边界通常定义为Scalar(x, y, z),其中xy,z是定义为上下边界的每个通道的数值。

以下示例向您展示如何检测可以被认为是皮肤的像素。 已经观察到,肤色的强度比色度的差异更大,因此通常在皮肤检测中不考虑亮度成分。 由于该色彩空间对亮度的依赖性,这一事实使得难以检测以 RGB 表示的图像中的皮肤,因此使用 HSV 和 YCrCb 颜色模型。 值得注意的是,对于这种类型的分段,必须知道或获得每个通道的边界值。

HSV 分割

如先前所述,HSV 被广泛用​​于进行颜色比较,因为 H 几乎与光的变化无关,因此在皮肤检测中很有用。 在该示例中,选择下边界(0, 10, 60)和上边界(20, 150, 255)以检测每个像素中的皮肤。 示例代码为:

#include <opencv2/opencv.hpp>
#include <opencv2/imgproc.hpp>

using namespace std;
using namespace cv;

int main()
{
    //Load the image
    Mat image = imread("hand.jpg");
    namedWindow("Picture", WINDOW_AUTOSIZE );
    imshow("Picture",image);

    Mat hsv;
 cvtColor(image, hsv, COLOR_BGR2HSV);

    //select pixels
    Mat bw;
 inRange(hsv, Scalar(0, 10, 60), Scalar(20, 150, 255), bw);

    namedWindow("Selected pixels", WINDOW_AUTOSIZE );
    imshow("Selected pixels", bw);

    waitKey(0);
    return 0;
}

下图显示代码的输出:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_04_9.jpg

使用 HSV 颜色空间进行皮肤检测

YCrCb 分割

YCrCb 颜色空间减少了 RGB 颜色通道的冗余,并以独立的组件表示颜色。 考虑到亮度和色度分量是分开的,此空间是皮肤检测的不错选择。

以下示例将 YCrCb 颜色空间用于皮肤检测,并使用每个像素中的下边界(0, 33, 77)和上边界(255, 173, 177)。 示例代码为:

#include <opencv2/opencv.hpp>
#include <opencv2/imgproc.hpp>

using namespace std;
using namespace cv;

int main()
{
    //Load the image
    Mat image = imread("hand.jpg");
    namedWindow("Picture", WINDOW_AUTOSIZE );
    imshow("Picture",image);

    Mat ycrcb;
 cvtColor(image, ycrcb, COLOR_BGR2HSV);

    //select pixels
    Mat bw;
 inRange(ycrcb, Scalar(0, 133, 77), Scalar(255, 173, 177), bw);

    namedWindow("Selected pixels", WINDOW_AUTOSIZE );
    imshow("Selected pixels", bw);

    waitKey(0);
    return 0;
}

下图显示代码的输出:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_04_10.jpg

使用 YCrCb 颜色空间进行皮肤检测

注意

有关的更多图像分割方法,请参阅 Packt Publishing 的《OpenCV Essentials》的第 4 章。

颜色转移

图像处理中通常执行的另一项任务是修改图像的颜色,特别是在需要去除主要或不希望有的偏色的情况下。 这些方法中的一种称为颜色转移,该方法执行一组借用一个源图像的颜色特征的颜色校正,并将源图像的外观转移到目标图像。

示例代码

以下colorTransfer示例显示了如何将颜色从源图像传输到目标图像。 此方法首先将图像色彩空间转换为 CIE Lab。 接下来,它为源图像和目标图像分割通道。 之后,使用均值和标准差拟合从一个图像到另一个图像的通道分布。 最后,通道合并回一起并转换为 RGB。

注意

有关示例中使用的转换的完整理论详细信息,请参见《图像之间的颜色转换》

代码的第一部分将图像转换为 CIE Lab 颜色空间,同时还将图像类型更改为CV_32FC1

#include <opencv2/opencv.hpp>
#include <opencv2/imgproc.hpp>

using namespace std;
using namespace cv;

int main(int argc, const char** argv)
{
    //Load the images
    Mat src = imread("clock_tower.jpg");
    Mat tar = imread("big_ben.jpg");

    //Convert to Lab space and CV_32F1
    Mat src_lab, tar_lab;

 cvtColor(src, src_lab, COLOR_BGR2Lab );
 cvtColor(tar, tar_lab, COLOR_BGR2Lab );

    src_lab.convertTo(src_lab,CV_32FC1);
    tar_lab.convertTo(tar_lab,CV_32FC1);

代码的接下来的部分执行颜色转移,如前所述:

    //Find mean and std of each channel for each image
    Mat mean_src, mean_tar, stdd_src, stdd_tar;
 meanStdDev(src_lab, mean_src, stdd_src);
 meanStdDev(tar_lab, mean_tar, stdd_src);

    // Split into individual channels
    vector<Mat> src_chan, tar_chan;

 split( src_lab, src_chan );
 split( tar_lab, tar_chan );

    // For each channel calculate the color distribution
    for( int i = 0; i < 3; i++ ) {
 tar_chan[i] -= mean_tar.at<double>(i);
 tar_chan[i] *= (stdd_src.at<double>(i) / stdd_src.at<double>(i));
 tar_chan[i] += mean_src.at<double>(i);
    }

    //Merge the channels, convert to CV_8UC1 each channel and convert to BGR
    Mat output;
 merge(tar_chan, output);
    output.convertTo(output,CV_8UC1);
    cvtColor(output, output, COLOR_Lab2BGR );

    //show pictures
    namedWindow("Source image", WINDOW_AUTOSIZE );
    imshow("Source image",src);
    namedWindow("Target image", WINDOW_AUTOSIZE );
    imshow("Target image",tar);
    namedWindow("Result image", WINDOW_AUTOSIZE );
    imshow("Result image",output);

    waitKey(0);

    return 0;
}

下图显示了代码的输出:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_04_11.jpg

夜间外观颜色转移示例

总结

在本章中,我们对 OpenCV 中使用的色彩空间进行了更深入的介绍,并向您展示了如何使用cvtColor函数在色彩空间之间进行转换。 此外,强调了使用不同颜色模型进行图像处理的可能性以及考虑到我们需要进行的操作选择正确的颜色空间的重要性。 为此,实现了基于颜色空间的分割和颜色转移方法。

下一章将介绍用于视频或一系列图像的图像处理技术。 我们将看到如何使用 OpenCV 实现视频稳定,超分辨率和拼接算法。

五、视频图像处理

本章介绍与视频图像处理有关的各种技术。 尽管大多数经典图像处理都是处理静态图像,但基于视频的处理正变得越来越流行且价格合理。

本章涵盖以下主题:

  • 视频稳定
  • 视频超分辨率过程
  • 图像拼接

在本章中,我们将直接与视频序列或实时摄像机一起使用。 图像处理的输出可以是一组修改的图像或有用的高级信息。 大多数图像处理技术将图像视为二维数字信号,并对其应用不同的技术。 在本章中,将使用视频或实时摄像机的图像序列来使用不同的高级技术来制作或改进新的增强序列。 因此,获得了更多有用的信息,即,结合了第三时间维度。

视频稳定

视频稳定是指用于减少与摄像机运动相关的模糊的一系列方法。 换句话说,它补偿了任何角度移动,等效于摄像机的偏航,俯仰,横滚以及 x 和 y 平移。 最早的图像稳定器出现在 60 年代初期。 这些系统能够略微补偿相机抖动和非自愿移动。 它们由陀螺仪和加速计控制,其机制是通过改变镜头的位置来抵消或减少不必要的运动。 当前,这些方法广泛用于双筒望远镜,摄像机和望远镜。

有多种用于图像或视频稳定的方法,本章重点介绍最广泛的方法系列:

  • 机械稳定系统:这些系统在摄像机镜头上使用机械系统,因此在移动摄像机时,加速度计和陀螺仪会检测到运动,并且系统会在运动时产生运动。 镜片。 这些系统将不在此处考虑。
  • 数字稳定系统:这些通常是视频中使用的,它们直接作用于从摄像机获得的图像。 在这些系统中,稳定图像的表面略小于源图像的表面。 移动相机时,拍摄的图像也会移动以补偿该移动。 尽管这些技术通过减小运动传感器的可用面积有效地消除了运动,但是却牺牲了分辨率和图像清晰度。

视频稳定算法通常包括以下步骤:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659O9_05_01.jpg

视频稳定算法的一般步骤

本章将放在 OpenCV 3.0 Alpha 中的videostab模块上,该模块包含一组可用于解决视频稳定问题的函数和类。

让我们更详细地探讨一般过程。 视频稳定是通过使用 RANSAC 方法对连续帧之间的帧间运动进行第一估计来实现的。 在此步骤结束时,将获得3 x 3矩阵的数组,并且每个矩阵都描述了两对连续帧的运动。 全局运动估计对于此步骤非常重要,它会影响稳定的最终序列的准确率。

注意

您可以在这个页面上找到有关 RANSAC 方法的更多详细信息。

第二步基于估计的运动生成新的帧序列。 执行其他处理,例如平滑,去模糊,边界外推等,以提高稳定的质量。

第三步消除烦人的不规则扰动,请参见下图。 有一些方法假设了摄像机运动模型,当可以对摄像机的实际运动做出一些假设时,这些方法会很好地起作用。

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659O9_05_02.jpg

消除不规则的扰动

在 OpenCV 示例([opencv_source_code]/samples/cpp/videostab.cpp)中,可以找到视频稳定程序示例。 对于以下videoStabilizer示例,videoStabilizer.pro项目需要以下库:lopencv_core300lopencv_highgui300lopencv_features2d300lopencv_videoio300lopencv_videostab300

使用 OpenCV 3.0 Alpha 的videostab模块创建了以下videoStabilizer示例:

#include <string>
#include <iostream>
#include <opencv2/opencv.hpp>
#include <opencv2/videostab.hpp>

using namespace std;
using namespace cv;
using namespace cv::videostab;

void processing(Ptr<IFrameSource> stabilizedFrames, string outputPath);

int main(int argc, const char **argv)
{
Ptr<IFrameSource> stabilizedFrames;
    try
    {

// 1-Prepare the input video and check it
        string inputPath;
       string outputPath;
        if (argc > 1)
            inputPath = argv[1];
        else
            inputPath = ".\\cube4.avi";

  if (argc > 2)
            outputPath = argv[2];
        else
            outputPath = ".\\cube4_stabilized.avi";

Ptr<VideoFileSource> source = makePtr<VideoFileSource>(inputPath);
        cout << "frame count (rough): " << source->count() << endl;

// 2-Prepare the motion estimator
// first, prepare the motion the estimation builder, RANSAC L2
        double min_inlier_ratio = 0.1;
Ptr<MotionEstimatorRansacL2> est = makePtr<MotionEstimatorRansacL2>(MM_AFFINE);
RansacParams ransac = est->ransacParams();
ransac.size = 3;
 ransac.thresh = 5;
 ransac.eps = 0.5;
        est->setRansacParams(ransac);
        est->setMinInlierRatio(min_inlier_ratio);

    // second, create a feature detector
int nkps = 1000;
Ptr<GoodFeaturesToTrackDetector> feature_detector = makePtr<GoodFeaturesToTrackDetector>(nkps);

// third, create the motion estimator
Ptr<KeypointBasedMotionEstimator> motionEstBuilder = makePtr<KeypointBasedMotionEstimator>(est);
        motionEstBuilder->setDetector(feature_detector);
Ptr<IOutlierRejector> outlierRejector = makePtr<NullOutlierRejector>();
        motionEstBuilder->setOutlierRejector(outlierRejector);

// 3-Prepare the stabilizer
StabilizerBase *stabilizer = 0;

// first, prepare the one or two pass stabilizer
        bool isTwoPass = 1;
        int radius_pass = 15;
        if (isTwoPass)
        {
            // with a two pass stabilizer
            bool est_trim = true;

TwoPassStabilizer *twoPassStabilizer = new TwoPassStabilizer();
            twoPassStabilizer->setEstimateTrimRatio(est_trim);
            twoPassStabilizer->setMotionStabilizer(makePtr<GaussianMotionFilter>(radius_pass));

            stabilizer = twoPassStabilizer;
        }
        else
        {
            // with an one pass stabilizer
OnePassStabilizer *onePassStabilizer = new OnePassStabilizer();
            onePassStabilizer->setMotionFilter(makePtr<GaussianMotionFilter>(radius_pass));

            stabilizer = onePassStabilizer;
        }

        // second, set up the parameters
        int radius = 15;
        double trim_ratio = 0.1;
        bool incl_constr = false;
stabilizer->setFrameSource(source);
stabilizer->setMotionEstimator(motionEstBuilder);
        stabilizer->setRadius(radius);
        stabilizer->setTrimRatio(trim_ratio);
        stabilizer->setCorrectionForInclusion(incl_constr);
        stabilizer->setBorderMode(BORDER_REPLICATE);

        // cast stabilizer to simple frame source interface to read stabilized frames
stabilizedFrames.reset(dynamic_cast<IFrameSource*>(stabilizer));

// 4-Processing the stabilized frames. The results are showed and saved.
processing(stabilizedFrames, outputPath);
    }
    catch (const exception &e)
    {
        cout << "error: " << e.what() << endl;
        stabilizedFrames.release();
        return -1;
    }
    stabilizedFrames.release();
    return 0;
}

void processing(Ptr<IFrameSource> stabilizedFrames, string outputPath)
{
    VideoWriter writer;
    Mat stabilizedFrame;
    int nframes = 0;
double outputFps = 25;

    // for each stabilized frame
while (!(stabilizedFrame = stabilizedFrames->nextFrame()).empty())
    {
        nframes++;

        // init writer (once) and save stabilized frame
        if (!outputPath.empty())
        {
            if (!writer.isOpened())                writer.open(outputPath,VideoWriter::fourcc('X','V','I','D'),
outputFps, stabilizedFrame.size());
writer << stabilizedFrame;
        }

imshow("stabilizedFrame", stabilizedFrame);
        char key = static_cast<char>(waitKey(3));
        if (key == 27) { cout << endl; break;}

    }
    cout << "processed frames: " << nframes << endl;
    cout << "finished " << endl;
}

本示例接受输入视频文件的名称作为默认视频文件名(.\cube4.avi)。 将显示结果视频,然后将其另存为.\cube4_stabilized.avi。 请注意如何包含videostab.hpp标头和使用cv::videostab名称空间。 该示例采取了四个重要步骤。 第一步准备输入视频路径,此示例使用标准命令行输入参数(inputPath = argv[1])选择视频文件。 如果没有输入视频文件,则使用默认视频文件(.\cube4.avi)。

第二步建立运动估计器。 使用 OpenCV(Ptr<MotionEstimatorRansacL2> est = makePtr <MotionEstimatorRansacL2> (MM_AFFINE))的智能指针(Ptr<object>)为运动估计器创建了基于 RANSAC 的鲁棒全局 2D 方法。 有不同的运动模型可以稳定视频:

  • MM_TRANSLATION = 0
  • MM_TRANSLATION_AND_SCALE = 1
  • MM_ROTATIO = 2
  • MM_RIGID = 3
  • MM_SIMILARITY = 4
  • MM_AFFINE = 5
  • MM_HOMOGRAPHY = 6
  • MM_UNKNOWN = 7

在稳定视频的精度和计算时间之间需要权衡。 基本运动模型越不准确,计算时间就越长。 但是,更复杂的模型具有更好的准确率和更差的计算时间。

现在创建 RANSAC 对象(RansacParams ransac = est-> ransacParams()),并设置它们的参数(ransac.sizeransac.threshransac.eps)。 还需要一个特征检测器来估计稳定器将使用的每个连续帧之间的运动。 本示例使用GoodFeaturesToTrackDetector方法来检测(nkps = 1000)每帧中的显着特征。 然后,它使用鲁棒的 RANSAC 和特征检测器方法使用Ptr<KeypointBasedMotionEstimator> motionEstBuilder = makePtr<KeypointBasedMotionEstimator>(est)类创建运动估计器,并使用motionEstBuilder->setDetector (feature_detector)设置特征检测器。

RANSAC 参数
size
eps
thresh
prob

第三步,创建需要先前运动估计器的稳定器。 您可以选择(isTwoPass = 1)一或两遍稳定器。 如果使用两遍稳定器(TwoPassStabilizer *twoPassStabilizer = new TwoPassStabilizer()),结果通常会更好。 但是,在此示例中,这在计算上较慢。 如果使用其他选项单程稳定器(OnePassStabilizer *onePassStabilizer = new OnePassStabilizer()),结果会更糟,但响应速度会更快。 稳定器需要设置其他选项才能正常工作,例如源视频文件(stabilizer->setFrameSource(source))和运动估计器(stabilizer->setMotionEstimator(motionEstBuilder))。 它还需要将稳定器转换为简单的帧源视频以读取稳定的帧(stabilizedFrames.reset(dynamic_cast<IFrameSource*>(stabilizer)))。

最后一步使用创建的稳定器稳定视频。 创建processing(Ptr<IFrameSource> stabilizedFrames)函数来处理和稳定每个帧。 此函数需要引入一条路径来保存生成的视频(string outputPath = ".//stabilizedVideo.avi")和设置播放速度(double outputFps = 25)。 此后,此函数将计算每个稳定的帧,直到不再有帧(stabilizedFrame = stabilizedFrames-> nextFrame().empty())。 在内部,稳定器首先估计每个帧的运动。 此函数创建一个视频编写器(writer.open(outputPath,VideoWriter::fourcc('X','V','I','D'), outputFps, stabilizedFrame.size())),以 XVID 格式存储每个帧。 最后,它保存并显示每个稳定的帧,直到用户按下Esc键。

为了演示如何使用 OpenCV 稳定视频,使用了先前的videoStabilizer示例。 该示例从命令行执行,如下所示:

<bin_dir>\videoStabilizer.exe .\cube4.avi .\cube4_stabilized.avi

注意

cube4.avi视频可在 OpenCV 示例文件夹中找到。 它还具有大量的相机移动,这对于此示例来说是完美的。

为了显示稳定结果,首先,请参见下图中的cube4.avi的四个帧。 这些帧之后的图显示了cube4.avicube4_stabilized.avi的前 10 个帧,没有(图的左侧)和(图的右侧)稳定叠加。

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659O9_05_03.jpg

cube4.avi视频的四个连续帧是摄像机的移动

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659O9_05_04.jpg

cube4.avicube4_stabilizated视频的 10 个叠加帧

通过右图查看上图,您可以看到由于稳定,减少了摄像机运动产生的振动。

超分辨率

超分辨率是指是指通常用于从较低分辨率的图像序列中提高图像或视频空间分辨率的技术或算法。 它与传统的图像缩放技术不同,传统的图像缩放技术使用单个图像来提高分辨率,同时保持锐利的边缘。 相反,超分辨率合并了来自同一场景的多个图像的信息,以表示最初在原始图像中未捕获的细节。

从真实场景捕获图像或视频的过程需要以下步骤:

  • 采样:这是连续系统从理想离散系统场景开始的变换,没有混叠
  • 几何变换:此是指根据摄像机的位置和镜头系统应用一组变换(例如平移或旋转)来理想地推断到达每个传感器的场景细节
  • 模糊:这是由于镜头系统或积分期间场景中的现有运动造成的
  • 二次采样:传感器仅对可使用的像素数(照片)进行积分

您可以在下图中看到此图像捕获过程:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659O9_05_05.jpg

从真实场景捕获图像的过程

在此捕获过程中,场景的细节由不同的传感器集成,因此每次捕获中的每个像素都包含不同的信息。 因此,超分辨率基于试图找到获得场景不同细节的不同捕获之间的关系,以便创建具有更多信息的新图像。 因此,超分辨率用于再生具有更高分辨率的离散场景。

可以通过各种技术来获得超分辨率,从空间领域中最直观的技术到基于频谱分析的技术。 技术基本上分为光学技术(使用镜头,变焦等)或基于图像处理的技术。 本章重点介绍基于图像处理的超分辨率。 这些方法使用低分辨率图像或其他无关图像的其他部分来推断高分辨率图像的外观。 这些算法也可以分为频域或空间域。 最初,超分辨率方法仅适用于灰度图像,但是已经开发出新方法来使它们适应彩色图像。

通常,由于低分辨率和高分辨率图像的尺寸很大,并且可能需要数百秒才能生成图像,因此在空间和时间上都需要超分辨率。 为了尝试减少计算时间,当前将预处理器用于负责使这些功能最小化的优化器。 另一种选择是使用 GPU 处理来改善计算时间,因为超分辨率过程固有地可并行化。

本章重点介绍 OpenCV 3.0 Alpha 中的superres模块,其中包含一组可用于解决分辨率增强问题的函数和类。 该模块实现了多种基于图像处理超分辨率的方法。 本章重点介绍已实现的双边总变异 L1(BTVL1)超分辨率方法。 超分辨率过程的主要困难是估计扭曲函数以建立超分辨率图像。 双总变异 L1 使用光流来估计翘曲函数。

注意

您可以在这个页面这个页面找到双边 TV-L 方法的更多详细信息。

在 OpenCV 示例([opencv_source_code]/samples/gpu/super_resolution.cpp)中,可以找到超分辨率的基本示例。

注意

您还可以从这个页面的 OpenCV GitHub 存储库中下载此示例。

对于以下超分辨率示例项目,superresolution.pro项目文件应包括以下库:lopencv_core300lopencv_imgproc300lopencv_highgui300lopencv_features2d300lopencv_videoio300lopencv_superres300才能正常工作:

#include <iostream>
#include <iomanip>
#include <string>

#include <opencv2/core.hpp>
#include <opencv2/core/utility.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/superres.hpp>
#include <opencv2/superres/optical_flow.hpp>
#include <opencv2/opencv_modules.hpp>

using namespace std;
using namespace cv;
using namespace cv::superres;

static Ptr<DenseOpticalFlowExt> createOptFlow(string name);

int main(int argc, char *argv[])
{
// 1-Initialize the initial parameters

// Input and output video
string inputVideoName;
 string outputVideoName;
    if (argc > 1)
        inputVideoName = argv[1];
    else
        inputVideoName = ".\\tree.avi";

    if (argc > 2)
        outputVideoName = argv[2];
    else
        outputVideoName = ".\\tree_superresolution.avi";

const int scale = 4;// Scale factor

const int iterations = 180;// Iterations count

const int temporalAreaRadius =4;// Radius of the temporal search area

string optFlow = "farneback";// Optical flow algorithm
       // optFlow = "farneback";
       // optFlow = "tvl1";
       // optFlow = "brox";
       // optFlow = "pyrlk";

double outputFps = 25.0;// Playback speed output

// 2- Create an optical flow method
Ptr<DenseOpticalFlowExt> optical_flow = createOptFlow(optFlow);

    if (optical_flow.empty()) return -1;

// 3- Create the superresolution method and set its parameters
Ptr<SuperResolution> superRes;
 superRes = createSuperResolution_BTVL1();

superRes->set("opticalFlow", optical_flow);
 superRes->set("scale", scale);
 superRes->set("iterations", iterations);
 superRes->set("temporalAreaRadius", temporalAreaRadius);

Ptr<FrameSource> frameSource;
 frameSource = createFrameSource_Video(inputVideoName);

superRes->setInput(frameSource);

    // Not use the first frame
    Mat frame;
    frameSource->nextFrame(frame);

// 4- Processing the input video with the superresolution
    // Show the initial options
    cout << "Input                  : " << inputVideoName << " " << frame.size() << endl;
    cout << "Output                 : " << outputVideoName << endl;
    cout << "Playback speed output  : " << outputFps << endl;
    cout << "Scale factor           : " << scale << endl;
    cout << "Iterations             : " << iterations << endl;
    cout << "Temporal radius        : " << temporalAreaRadius << endl;
    cout << "Optical Flow           : " << optFlow << endl;
    cout << endl;

    VideoWriter writer;
    double start_time,finish_time;

    for (int i = 0;; ++i)
    {
        cout << '[' << setw(3) << i << "] : ";
        Mat result;

        // Calculate the processing time
        start_time = getTickCount();
superRes->nextFrame(result);
        finish_time = getTickCount();
        cout << (finish_time - start_time)/getTickFrequency() << " secs, Size: " << result.size() << endl;

        if (result.empty()) break;

        // Show the result
imshow("Super Resolution", result);

        if (waitKey(1000) > 0) break;

        // Save the result on output file
        if (!outputVideoName.empty())
        {
if (!writer.isOpened())
 writer.open(outputVideoName, VideoWriter::fourcc('X', 'V', 'I', 'D'), outputFps, result.size());
 writer << result;
        }
    }
    writer.release();
    return 0;
}

static Ptr<DenseOpticalFlowExt> createOptFlow(string name)
{
    if (name == "farneback")
return createOptFlow_Farneback();

    else if (name == "tvl1")
return createOptFlow_DualTVL1();

    else if (name == "brox")
        return createOptFlow_Brox_CUDA();

    else if (name == "pyrlk")
        return createOptFlow_PyrLK_CUDA();

    else
        cerr << "Incorrect Optical Flow algorithm - " << name << endl;

    return Ptr<DenseOpticalFlowExt>();
}

本示例创建一个程序(超分辨率)以获取具有超分辨率的视频。 它采用输入视频的路径或使用默认视频路径(.\tree.avi)。 显示所得的视频并将其另存为.\tree_superresolution.avi。 首先,包含superres.hppsuperres/optical_flow.hpp标头,并使用cv::superres命名空间。 该示例遵循四个重要步骤。

第一步设置初始参数。 输入视频路径使用标准输入(inputVideoName = argv[1])选择视频文件,如果没有输入视频文件,则使用默认视频文件。 输出视频路径还使用输入标准(outputVideoName = argv[2])选择输出视频文件,如果它没有输出视频文件,则使用默认输出视频文件(.\tree_superresolution)同时也设置输出回放速度(double outputFps = 25.0)。 superresolution方法的其他重要参数是比例因子(const int scale = 4),迭代count(const int iterations = 100,时间搜索区域的半径(const int temporalAreaRadius = 4)和光流算法(string optFlow = "farneback")。 。

第二步创建光流方法以检测显着特征并针对每个视频帧跟踪它们。 已经创建了一种新方法(static Ptr<DenseOpticalFlowExt> createOptFlow(string name)),以便在不同的光流方法之间进行选择。 您可以在 FarnebackTVL1BroxPyrlk 光流方法之间进行选择。 编写了新的方法(static Ptr<DenseOpticalFlowExt> createOptFlow(string name)),以创建光学流动方法来跟踪特征。 两个最重要的方法是 Farneback(createOptFlow_Farneback())和 TV-L1(createOptFlow_DualTVL1())。 第一种方法基于 Gunner Farneback 的算法,该算法计算帧中所有点的光流。 第二种方法基于电视能量的双重公式计算两个图像帧之间的光流,并采用有效的逐点阈值化步骤。 此第二种方法在计算上更有效。

不同光流方法之间的比较
方法复杂度可并行化
Farneback二次
TVL1线性
Brox线性
Pyrlk线性

注意

您还可以在此处了解有关 Farneback 光流方法的更多信息。

第三步创建并设置superresolution方法。 创建此方法的一个实例(Ptr<SuperResolution> superRes),该实例使用双边总变异 L1 算法(superRes = createSuperResolution_BTVL1())。 对于算法,此方法具有以下参数:

  • scale:这是比例因子
  • iterations:这是迭代计数
  • tau:这是最速下降法的渐近值
  • lambda:这是权重参数,用于平衡数据项和平滑度项
  • alpha:这是双边电视中空间分布的参数
  • btvKernelSize:这是双边电视过滤器的核大小
  • blurKernelSize:这是高斯模糊核大小
  • blurSigma:这是高斯模糊西格玛
  • temporalAreaRadius:这是时间搜索区域的半径
  • opticalFlow:这是一种密集光流算法

这些参数设置如下:

superRes->set("parameter", value);

仅设置以下参数; 其他参数使用其默认值:

superRes->set("opticalFlow", optical_flow);
superRes->set("scale", scale);
superRes->set("iterations", iterations);
superRes->set("temporalAreaRadius", temporalAreaRadius);

之后,选择输入视频帧(superRes->setInput(frameSource))。

最后一步处理输入视频以计算超分辨率。 对于每个视频帧,计算超分辨率(superRes->nextFrame(result)); 这种计算在计算上非常慢,因此估计处理时间可以显示进度。 最后,显示每个结果帧(imshow("Super Resolution", result))并保存(writer << result)。

为了显示超分辨率的结果,比较了tree.avitree_superresolution.avi视频的第一帧的一小部分是否有超分辨率:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659O9_05_06.jpg

tree.avitree_superresolution.avi视频的第一帧的一部分,不包括超分辨率处理

在上图的右侧部分,由于超分辨率过程,您可以在树的叶子和树枝中观察到更多细节。

拼接

图像拼接或照片拼接可以发现具有一定程度重叠的图像之间的对应关系。 此过程将一组图像与重叠的视场组合在一起,以生成全景图像或高分辨率图像。 大多数用于图像拼接的技术需要在图像之间几乎完全重叠以产生无缝结果。 一些数码相机可以在内部拼接一组图像以构建全景图像。 下图显示了一个示例:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659O9_05_07.jpg

通过拼接创建的全景图像

注意

可以在这个页面上找到前面的图像示例和有关图像拼接的更多信息。

拼接通常可以分为三个重要步骤:

  • 配准(图像)表示一组图像中的匹配特征以搜索使重叠像素之间差异的绝对值之和最小的位移。 直接比对方法可用于获得更好的结果。 用户还可以添加全景图的粗略模型以帮助特征匹配阶段,在这种情况下,结果通常更准确且计算速度更快。
  • 校准(图像)将重点放在最小化理想模型和相机镜头系统之间的差异:不同的相机位置和光学缺陷,例如失真,曝光,色差等。
  • 合成(图像)使用上一步校准的结果,并将图像重新映射到输出投影。 图像之间的颜色也会进行调整,以补偿曝光差异。 将图像融合在一起,并进行接缝线调整以最小化图像之间接缝的可见性。

当从空间的同一点拍摄了图像片段时,可以使用各种地图投影之一进行拼接。 最重要的地图投影如下所示:

  • 直线投影:在这里,在与全景球在一个点相交的二维平面上查看拼接图像。 无论图像上的方向如何,现实中笔直的线都显示类似。 当视野开阔(大约 120 度)时,图像在边框附近会变形。
  • 圆柱投影:此处,拼接图像显示 360 度水平视场和有限的垂直视场。 该投影旨在被视为好像图像被包裹在圆柱体中并从内部观看。 在 2D 平面上查看时,水平线看起来是弯曲的,而垂直线则保持笔直。
  • 球形投影:在这里,拼接图像显示了 360 度水平视野和 180 度垂直视野,即整个球体。 具有这种投影的全景图像旨在像被包裹在一个球体中并从内部观看一样被观看。 在 2D 平面上查看时,水平线看起来像圆柱投影一样弯曲,而垂直线则保持垂直。
  • 立体投影或鱼眼投影:通过将虚拟摄像机指向下方并将视场设置为足够大以显示整个地面及其上方的某些区域,可以将其用于形成一个小行星全景。 将虚拟摄像机指向上方会产生隧道效果。
  • 帕尼尼投影:这具有专业投影,可能比常规制图投影在美学上更具优势。 此投影将同一图像中的不同投影组合在一起,以微调输出全景图像的最终外观。

本章重点介绍 OpenCV 3.0 Alpha 中的stitching模块和detail子模块,其中包含实现拼接器的一组函数和类。 使用这些模块,可以配置或跳过某些步骤。 实现的缝合示例具有以下常规示意图:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659O9_05_08.jpg

在 OpenCV 示例中,有两个基本的缝合示例,可以在([opencv_source_code]/samples/cpp/stitching.cpp])和([opencv_source_code]/samples/cpp/stitching_detailed.cpp])处找到。

对于以下,更高级的拼接示例,stitchingAdvanced.pro项目文件必须包含以下库才能正常工作:lopencv_core300lopencv_imgproc300lopencv_highgui300lopencv_features2d300lopencv_videoio300, lopencv_imgcodecs300lopencv_stitching300

#include <iostream>
#include <string>
#include <opencv2/opencv_modules.hpp>
#include <opencv2/core/utility.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/features2d.hpp>
#include <opencv2/stitching/detail/blenders.hpp>
#include <opencv2/stitching/detail/camera.hpp>
#include <opencv2/stitching/detail/exposure_compensate.hpp>
#include <opencv2/stitching/detail/matchers.hpp>
#include <opencv2/stitching/detail/motion_estimators.hpp>
#include <opencv2/stitching/detail/seam_finders.hpp>
#include <opencv2/stitching/detail/util.hpp>
#include <opencv2/stitching/detail/warpers.hpp>
#include <opencv2/stitching/warpers.hpp>

using namespace std;
using namespace cv;
using namespace cv::detail;

int main(int argc, char* argv[])
{
    // Default parameters
    vector<String> img_names;
double scale = 1;
string features_type = "orb";//"surf" or "orb" features type
float match_conf = 0.3f;
float conf_thresh = 1.f;
string adjuster_method = "ray";//"reproj" or "ray" adjuster method
bool do_wave_correct = true;
WaveCorrectKind wave_correct_type = WAVE_CORRECT_HORIZ;
string warp_type = "spherical";
int expos_comp_type = ExposureCompensator::GAIN_BLOCKS;
string seam_find_type = "gc_color";
float blend_strength = 5;
int blend_type = Blender::MULTI_BAND;
string result_name = "panorama_result.jpg";

    double start_time = getTickCount();

// 1-Input images
    if(argc > 1)
    {
        for(int i=1; i < argc; i++)
img_names.push_back(argv[i]);
    }
    else
    {
img_names.push_back("./panorama_image1.jpg");
 img_names.push_back("./panorama_image2.jpg");
    }
    // Check if have enough images
    int num_images = static_cast<int>(img_names.size());
if (num_images < 2) {cout << "Need more images" << endl; return -1; }

// 2- Resize images and find features steps
    cout << "Finding features..." << endl;
    double t = getTickCount();

    Ptr<FeaturesFinder> finder;
    if (features_type == "surf")
finder = makePtr<SurfFeaturesFinder>();

    else if (features_type == "orb")
finder = makePtr<OrbFeaturesFinder>();

    else {cout << "Unknown 2D features type: '" << features_type << endl; return -1; }

    Mat full_img, img;
    vector<ImageFeatures> features(num_images);
    vector<Mat> images(num_images);
    vector<Size> full_img_sizes(num_images);

    for (int i = 0; i < num_images; ++i)
    {
        full_img = imread(img_names[i]);
        full_img_sizes[i] = full_img.size();

        if (full_img.empty()) {cout << "Can't open image " << img_names[i] << endl; return -1; }

resize(full_img, img, Size(), scale, scale);
        images[i] = img.clone();

(*finder)(img, features[i]);
        features[i].img_idx = i;
        cout << "Features in image #" << i+1 << " are : " << features[i].keypoints.size() << endl;
    }
    finder->collectGarbage();
    full_img.release();
    img.release();
    cout << "Finding features, time: " << ((getTickCount() - t) / getTickFrequency()) << " sec" << endl;

// 3- Match features
    cout << "Pairwise matching" << endl;
    t = getTickCount();
    vector<MatchesInfo> pairwise_matches;
BestOf2NearestMatcher matcher(false, match_conf);
    matcher(features, pairwise_matches);
    matcher.collectGarbage();
    cout << "Pairwise matching, time: " << ((getTickCount() - t) / getTickFrequency()) << " sec" << endl;

// 4- Select images and matches subset to build panorama
vector<int> indices = leaveBiggestComponent(features, pairwise_matches, conf_thresh);
    vector<Mat> img_subset;
    vector<String> img_names_subset;
    vector<Size> full_img_sizes_subset;
    for (size_t i = 0; i < indices.size(); ++i)
    {
        img_names_subset.push_back(img_names[indices[i]]);
        img_subset.push_back(images[indices[i]]);
        full_img_sizes_subset.push_back(full_img_sizes[indices[i]]);
    }
    images = img_subset;
    img_names = img_names_subset;
    full_img_sizes = full_img_sizes_subset;

    // Estimate camera parameters rough
    HomographyBasedEstimator estimator;
    vector<CameraParams> cameras;
    if (!estimator(features, pairwise_matches, cameras)){cout << "Homography estimation failed." << endl; return -1; }

    for (size_t i = 0; i < cameras.size(); ++i)
    {
        Mat R;
        cameras[i].R.convertTo(R, CV_32F);
        cameras[i].R = R;
        cout << "Initial intrinsic #" << indices[i]+1 << ":\n" << cameras[i].K() << endl;
    }

// 5- Refine camera parameters globally
Ptr<BundleAdjusterBase> adjuster;
    if (adjuster_method == "reproj")
        // "reproj" method
adjuster = makePtr<BundleAdjusterReproj>();
    else // "ray" method
adjuster = makePtr<BundleAdjusterRay>();

    adjuster->setConfThresh(conf_thresh);
    if (!(*adjuster)(features, pairwise_matches, cameras)) {cout << "Camera parameters adjusting failed." << endl; return -1; }

    // Find median focal length
    vector<double> focals;
    for (size_t i = 0; i < cameras.size(); ++i)
    {
        cout << "Camera #" << indices[i]+1 << ":\n" << cameras[i].K() << endl;
        focals.push_back(cameras[i].focal);
    }
    sort(focals.begin(), focals.end());
    float warped_image_scale;
    if (focals.size() % 2 == 1)
        warped_image_scale = static_cast<float>(focals[focals.size() / 2]);
    else
        warped_image_scale = static_cast<float>(focals[focals.size() / 2 - 1] + focals[focals.size() / 2]) * 0.5f;

// 6- Wave correlation (optional)
    if (do_wave_correct)
    {
        vector<Mat> rmats;
        for (size_t i = 0; i < cameras.size(); ++i)
            rmats.push_back(cameras[i].R.clone());

waveCorrect(rmats, wave_correct_type);
        for (size_t i = 0; i < cameras.size(); ++i)
            cameras[i].R = rmats[i];
    }

// 7- Warp images
    cout << "Warping images (auxiliary)... " << endl;
    t = getTickCount();
    vector<Point> corners(num_images);
    vector<UMat> masks_warped(num_images);
    vector<UMat> images_warped(num_images);
    vector<Size> sizes(num_images);
    vector<UMat> masks(num_images);

    // Prepare images masks
    for (int i = 0; i < num_images; ++i)
    {
        masks[i].create(images[i].size(), CV_8U);
        masks[i].setTo(Scalar::all(255));
    }

    // Map projections
    Ptr<WarperCreator> warper_creator;
    if (warp_type == "rectilinear")
warper_creator = makePtr<cv::CompressedRectilinearWarper>(2.0f, 1.0f);

    else if (warp_type == "cylindrical")
warper_creator = makePtr<cv::CylindricalWarper>();

    else if (warp_type == "spherical")
warper_creator = makePtr<cv::SphericalWarper>();

    else if (warp_type == "stereographic")
warper_creator = makePtr<cv::StereographicWarper>();

    else if (warp_type == "panini")
warper_creator = makePtr<cv::PaniniWarper>(2.0f, 1.0f);

    if (!warper_creator){ cout << "Can't create the following warper '" << warp_type << endl; return 1; }

Ptr<RotationWarper> warper = warper_creator->create(static_cast<float>(warped_image_scale * scale));

    for (int i = 0; i < num_images; ++i)
    {
        Mat_<float> K;
        cameras[i].K().convertTo(K, CV_32F);
        float swa = (float)scale;
        K(0,0) *= swa; K(0,2) *= swa;
        K(1,1) *= swa; K(1,2) *= swa;

        corners[i] = warper->warp(images[i], K, cameras[i].R, INTER_LINEAR, BORDER_REFLECT, images_warped[i]);
        sizes[i] = images_warped[i].size();

warper->warp(masks[i], K, cameras[i].R, INTER_NEAREST, BORDER_CONSTANT, masks_warped[i]);
    }

    vector<UMat> images_warped_f(num_images);
    for (int i = 0; i < num_images; ++i)
        images_warped[i].convertTo(images_warped_f[i], CV_32F);

    cout << "Warping images, time: " << ((getTickCount() - t) / getTickFrequency()) << " sec" << endl;

// 8- Compensate exposure errors
Ptr<ExposureCompensator> compensator = ExposureCompensator::createDefault(expos_comp_type);
 compensator->feed(corners, images_warped, masks_warped);

// 9- Find seam masks
    Ptr<SeamFinder> seam_finder;
    if (seam_find_type == "no")
seam_finder = makePtr<NoSeamFinder>();

    else if (seam_find_type == "voronoi")
seam_finder = makePtr<VoronoiSeamFinder>();

    else if (seam_find_type == "gc_color")
        seam_finder = makePtr<GraphCutSeamFinder>(GraphCutSeamFinderBase::COST_COLOR);

    else if (seam_find_type == "gc_colorgrad")
        seam_finder = makePtr<GraphCutSeamFinder>(GraphCutSeamFinderBase::COST_COLOR_GRAD);

    else if (seam_find_type == "dp_color")
seam_finder = makePtr<DpSeamFinder>(DpSeamFinder::COLOR);

    else if (seam_find_type == "dp_colorgrad")
seam_finder = makePtr<DpSeamFinder>(DpSeamFinder::COLOR_GRAD);

    if (!seam_finder){cout << "Can't create the following seam finder '" << seam_find_type << endl; return 1; }
    seam_finder->find(images_warped_f, corners, masks_warped);

    // Release unused memory
    images.clear();
    images_warped.clear();
    images_warped_f.clear();
    masks.clear();

// 10- Create a blender
Ptr<Blender> blender = Blender::createDefault(blend_type, false);
    Size dst_sz = resultRoi(corners, sizes).size();
    float blend_width = sqrt(static_cast<float>(dst_sz.area())) * blend_strength / 100.f;
    if (blend_width < 1.f)
blender = Blender::createDefault(Blender::NO, false);

    else if (blend_type == Blender::MULTI_BAND)
    {
MultiBandBlender* mb = dynamic_cast<MultiBandBlender*>(blender.get());
        mb->setNumBands(static_cast<int>(ceil(log(blend_width)/log(2.)) - 1.));
        cout << "Multi-band blender, number of bands: " << mb->numBands() << endl;
    }
    else if (blend_type == Blender::FEATHER)
    {
FeatherBlender* fb = dynamic_cast<FeatherBlender*>(blender.get());
        fb->setSharpness(1.f/blend_width);
        cout << "Feather blender, sharpness: " << fb->sharpness() << endl;
    }
blender->prepare(corners, sizes);

    // 11- Compositing step
    cout << "Compositing..." << endl;
    t = getTickCount();
    Mat img_warped, img_warped_s;
    Mat dilated_mask, seam_mask, mask, mask_warped;

    for (int img_idx = 0; img_idx < num_images; ++img_idx)
    {
        cout << "Compositing image #" << indices[img_idx]+1  << endl;

        // 11.1- Read image and resize it if necessary
full_img = imread(img_names[img_idx]);

        if (abs(scale - 1) > 1e-1)
resize(full_img, img, Size(), scale, scale);
        else
            img = full_img;

        full_img.release();
        Size img_size = img.size();

        Mat K;
        cameras[img_idx].K().convertTo(K, CV_32F);

        // 11.2- Warp the current image
warper->warp(img, K, cameras[img_idx].R, INTER_LINEAR, BORDER_REFLECT, img_warped);

        // Warp the current image mask
        mask.create(img_size, CV_8U);
        mask.setTo(Scalar::all(255));
        warper->warp(mask, K, cameras[img_idx].R, INTER_NEAREST, BORDER_CONSTANT, mask_warped);

        // 11.3- Compensate exposure error step
compensator->apply(img_idx, corners[img_idx], img_warped, mask_warped);

        img_warped.convertTo(img_warped_s, CV_16S);
        img_warped.release();
        img.release();
        mask.release();

        dilate(masks_warped[img_idx], dilated_mask, Mat());
        resize(dilated_mask, seam_mask, mask_warped.size());
        mask_warped = seam_mask & mask_warped;

        // 11.4- Blending images step
blender->feed(img_warped_s, mask_warped, corners[img_idx]);
    }
    Mat result, result_mask;
    blender->blend(result, result_mask);

    cout << "Compositing, time: " << ((getTickCount() - t) / getTickFrequency()) << " sec" << endl;

    imwrite(result_name, result);

    cout << "Finished, total time: " << ((getTickCount() - start_time) / getTickFrequency()) << " sec" << endl;
    return 0;
}

此示例创建一个程序,以使用 OpenCV 步骤拼接图像。 它采用输入路径来选择不同的输入图像或使用默认的输入图像(.\panorama_image1.jpgpanorama_image2.jpg),这些将在后面显示。 最后,显示所得图像并将其另存为.\panorama_result.jpg。 首先,包含stitching.hppdetail标头,并使用cv::detail命名空间。 还设置了更重要的参数,您可以使用这些参数配置针迹处理。 如果您需要使用自定义配置,则了解拼接过程的总体图(上图)非常有用。 这个高级示例包含 11 个重要步骤。 第一步读取并检查输入图像。 本示例需要两个或更多图像才能工作。

第二步使用double scale = 1参数调整输入图像的大小,并在每个图像上找到特征; 您可以使用string features_type = "orb"参数在 Surffinder = makePtr<SurfFeaturesFinder>())或 Orbfinder = makePtr<OrbFeaturesFinder>())特征查找器之间进行选择。 之后,此步骤将调整输入图像的大小(resize(full_img, img, Size(), scale, scale))并找到特征((*finder)(img, features[i]))。

注意

有关 SURF 和 ORB 描述符的更多信息,请参阅 Packt Publishing 的《OpenCV Essentials》的第 5 章。

第三步匹配先前找到的特征。 使用float match_conf = 0.3f参数创建一个匹配器(BestOf2NearestMatcher matcher(false, match_conf))。

第四步,选择图像并匹配子集以构建全景图。 然后,使用vector<int> indices = leaveBiggestComponent(features, pairwise_matches, conf_thresh)函数选择并匹配最佳特征。 通过这些特征,将创建一个新的子集以供使用。

第五步,使用调整来全局调整的参数,以构建调整器(Ptr<BundleAdjusterBase> adjuster)。 给定一组从不同视点描绘多个 2D 或 3D 点的图像,可以将束调整定义为同时细化 2D 或 3D 坐标,描述场景几何形状,以及所部署的相机的相对运动和光学特性参数,根据涉及所有点的相应图像投影的最优性标准,来获取图像。 有两种计算束调整的方法,reprojadjuster = makePtr<BundleAdjusterReproj>())或rayadjuster = makePtr<BundleAdjusterRay>()),这些方法是通过string adjuster_method = "ray"参数选择的。 最后,将该束调整用作(*adjuster)(features, pairwise_matches, cameras)

第六步是可选步骤(bool do_wave_correct = true),该步骤计算波形相关性以改善相机设置。 波相关的类型通过WaveCorrectKind wave_correct_type = WAVE_CORRECT_HORIZ参数选择,并计算为waveCorrect(rmats, wave_correct_type)

第七步,创建需要地图投影的变形图像。 地图投影以前已经描述过,可以是直线圆柱球形立体帕尼尼。 实际上,OpenCV 中实现了更多的地图投影。 可以使用string warp_type = "spherical"参数选择地图投影。 此后,创建整形器(Ptr<RotationWarper> warper = warper_creator-> create(static_cast<float>(warped_image_scale * scale))),并变形每个图像(warper->warp(masks[i], K, cameras[i].R, INTER_NEAREST, BORDER_CONSTANT, masks_warped[i]))。

第八步通过创建补偿器(Ptr<ExposureCompensator> compensator = ExposureCompensator::createDefault(expos_comp_type))补偿曝光误差,并将其应用于每个变形图像(compensator->feed(corners, images_warped, masks_warped))。

第九步找到接缝口罩。 此过程将为每个全景图像搜索最佳的附件区域。 OpenCV 中实现了一些方法来执行此任务,并且本示例使用string seam_find_type = "gc_color"参数进行选择。 这些方法是NoSeamFinder(不使用此方法),VoronoiSeamFinderGraphCutSeamFinderBase::COST_COLORGraphCutSeamFinderBase::COST_COLOR_GRADDpSeamFinder::COLORDpSeamFinder::COLOR_GRAD

第十步,创建一个混合器,将每个图像合成全景图。 OpenCV 中实现了两种类型的搅拌器,即MultiBandBlender* mb = dynamic_cast<MultiBandBlender*>(blender.get())FeatherBlender* fb = dynamic_cast<FeatherBlender*>(blender.get()),可以使用int blend_type = Blender::MULTI_BAND参数进行选择。 最后,准备搅拌器(blender->prepare(corners, sizes))。

最后一步合成了最终的全景图。 该步骤需要前面的步骤来配置针脚。 执行四个子步骤以计算最终全景图。 首先,读取每个输入图像(full_img = imread(img_names[img_idx])),并在必要时调整其大小(resize(full_img, img, Size(), scale, scale))。 其次,将这些图像与创建的变形器(warper->warp(img, K, cameras[img_idx].R, INTER_LINEAR, BORDER_REFLECT, img_warped))变形。 第三,使用创建的补偿器(compensator->apply(img_idx, corners[img_idx], img_warped, mask_warped))补偿这些图像的曝光误差。 最后,使用创建的混合器混合这些图像。 现在,最终结果全景图已保存在string result_name = "panorama_result.jpg"文件中。

为了向您显示高级拼接结果,将两个输入图像进行拼接,结果全景图如下所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659O9_05_09.jpg

总结

在本章中,您学习了如何使用 OpenCV 的三个重要模块来处理视频中的图像处理。 这些模块是视频稳定超分辨率拼接。 每个模块还解释了一些理论基础。

在本章的每一节中,都将说明用 C++ 开发的完整示例。 还显示了每个模块的图像结果,显示了主要效果。

下一章介绍高动态范围图像,并向您展示如何使用 OpenCV 处理它们。 通常在现在称为计算摄影的范围内考虑高动态范围成像。 粗略地说,计算摄影是指允许您扩展数字摄影的典型功能的技术。 这可能包括硬件附加组件或修改,但主要指基于软件的技术。 这些技术可能会产生“传统”数码相机无法获得的输出图像。

六、计算摄影

计算摄影是指使您能够扩展数字摄影的典型功能的技术。 这可能包括硬件附加组件或修改,但主要指基于软件的技术。 这些技术可能会产生“传统”数码相机无法获得的输出图像。 本章介绍了 OpenCV 中用于计算摄影的一些鲜为人知的技术:高动态范围成像,无缝克隆,脱色和非照片级渲染。 这三个位于库的photo模块中。 注意,在前面的章节中已经考虑了该模块内部的其他技术(修复和去噪)。

高动态范围图像

我们处理的典型图像每像素有 8 位(BPP)。 彩色图像还使用 8 位表示每个通道的值,即红色,绿色和蓝色。 这意味着仅使用 256 个不同的强度值。 在数字成像的整个历史中,这个 8 BPP 的限制一直盛行。 但是,很明显,自然界中的光并不只有 256 个不同的水平。 因此,我们应该考虑这种离散化是理想的还是足够的。 例如,已知人眼可以捕获更高的动态范围(最暗和最亮之间的亮度级别数),估计在 1 亿到 1 亿个亮度级别之间。 在只有 256 个光照级别的情况下,有些情况下明亮的光线看起来过度曝光或饱和,而黑暗的场景只是被捕获为黑色。

有些相机可以捕获超过 8 BPP 的图像。 但是,创建高动态范围图像的最常见方法是使用 8 BPP 相机并拍摄具有不同曝光值的图像。 当我们这样做时,动态范围有限的问题显而易见。 例如,考虑下图:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_06_01.jpg

用六个不同的曝光值拍摄的场景

注意

左上方的图像大部分为黑色,但窗口详细信息可见。 相反,右下角的图像显示了房间的细节,但窗口的细节几乎看不见。

我们可以使用现代智能手机相机以不同的曝光水平拍摄照片。 例如,对于 iPhone 和 iPad,从 iOS 8 开始,使用本机相机应用更改曝光非常容易。 触摸屏幕,将出现一个黄色框,侧面带有一个小太阳。 向上或向下滑动可以更改曝光(请参见以下屏幕截图)。

注意

曝光级别的范围非常大,因此我们可能不得不重复多次滑动手势。

如果您使用的是 iOS 的早期版本,则可以下载相机应用,例如 Camera+,这些应用可让您专注于特定点并更改曝光。

对于 Android,可以在 Google Play 上使用大量相机应用来调整曝光度。 一个例子是 Camera FV-5,它具有免费和付费版本。

提示

如果使用手持设备捕获图像,请确保该设备是静态的。 实际上,您可能会使用三脚架。 否则,具有不同曝光度的图像将无法对齐。 同样,移动的被摄体将不可避免地产生鬼影。 在大多数情况下,低,中和高曝光量的三张图像就足够了。

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_06_02.jpg

使用 iPhone 5S 中的本机摄像头应用进行曝光控制

智能手机和桌子很方便,可以拍摄许多曝光不同的图像。 要创建 HDR 图像,我们需要知道每个捕获图像的曝光(或快门)时间(原因请参见以下部分)。 并非所有应用都允许您手动控制(甚至查看)此功能(iOS 8 本机应用则不允许)。 在撰写本文时,至少有两个免费的应用允许 iOS 版使用:ManuallyManualShot。 在 Android 中,免费的 Camera FV-5 可让您控制和查看曝光时间。 请注意,F/Stop 和 ISO 是控制曝光的其他两个参数。

捕获的图像可以传输到开发计算机,并用于创建 HDR 图像。

注意

从 iOS 7 开始,本机相机应用具有 HDR 模式,可自动快速捕获三幅图像,每幅图像具有不同的曝光度。 这些图像也会自动组合为单个(有时更好)的图像。

创建 HDR 图像

我们如何将多张(例如三张)曝光图像合成为 HDR 图像? 如果我们仅考虑一个通道和一个给定的像素,则必须在较大的输出范围(例如 16 bpp)中将这三个像素值(每个曝光级别一个)映射到单个值。 这种映射并不容易。 首先,我们必须考虑像素强度是传感器辐照度(入射在相机传感器上的光量)的(粗略)度量。 数码相机以非线性方式测量辐照度。 相机具有非线性响应函数,可以将辐照度转换为像素强度值,范围为 0 到 255。 为了将这些值映射到更大的离散值集,我们必须估计摄像机的响应函数(即,响应范围在 0 到 255 之间)。

我们如何估计相机响应函数? 我们从像素本身做到这一点! 响应函数是每个颜色通道的 Sigmoid 曲线,可以根据像素进行估计(如果有 3 个像素曝光,则每个颜色通道的曲线上有 3 个点)。 由于这非常耗时,因此通常选择一组随机像素。

只剩下一件事了。 我们之前曾讨论过估计辐照度和像素强度之间的关系。 我们如何知道辐照度? 传感器的辐照度与曝光时间(或等效地,快门速度)成正比。 这就是我们需要曝光时间的原因!

最后,将 HDR 图像计算为从每次曝光的像素中恢复的辐照度值的加权和。 请注意,此图像无法在范围有限的常规屏幕上显示。

注意

关于高动态范围成像的一本好书是 Reinhard 等人的《高动态范围成像:获取,显示和基于图像的照明》,Morgan Kaufmann Pub。 该书随附 DVD,其中包含不同 HDR 格式的图像。

范例

OpenCV(仅从 3.0 版开始)提供了从一组以不同曝光拍摄的图像中创建 HDR 图像的函数。 甚至还有一个名为hdr_imaging的教程示例,该示例从图像文件中读取图像文件和曝光时间列表,并创建 HDR 图像。

注意

为了运行hdr_imaging教程,您将需要使用列表下载所需的图像文件和文本文件。 您可以从这个页面下载它们。

CalibrateDebevecMergeDebevec类实现 Debevec 的方法来估计相机响应函数并将曝光分别合并为 HDR 图像。createHDR示例之后的向您展示了如何使用这两个类:

#include <opencv2/photo.hpp>
#include <opencv2/highgui.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main(int, char** argv)
{
    vector<Mat> images;
    vector<float> times;

    // Load images and exposures...
    Mat img1 = imread("1div66.jpg");
    if (img1.empty())
    {
        cout << "Error! Input image cannot be read...\n";
        return -1;
    }
    Mat img2 = imread("1div32.jpg");
    Mat img3 = imread("1div12.jpg");
    images.push_back(img1);
    images.push_back(img2);
    images.push_back(img3);
    times.push_back((float)1/66);
    times.push_back((float)1/32);
    times.push_back((float)1/12);

    // Estimate camera response...
    Mat response;
 Ptr<CalibrateDebevec> calibrate = createCalibrateDebevec();
 calibrate->process(images, response, times);

    // Show the estimated camera response function...
    cout << response;

    // Create and write the HDR image...
    Mat hdr;
 Ptr<MergeDebevec> merge_debevec = createMergeDebevec();
 merge_debevec->process(images, hdr, times, response);
    imwrite("hdr.hdr", hdr);

    cout << "\nDone. Press any key to exit...\n";
    waitKey(); // Wait for key press
    return 0;
}

该示例使用三个杯子的图像(这些图像以及本书随附的代码均可用)。 图像是使用 ManualShot 拍摄的前面提到的应用,使用的曝光时间为 1/66、1/32 和 1/12 秒; 请参考下图:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_06_03.jpg

示例中用作输入的三个图像

请注意,createCalibrateDebevec方法在 STL 向量中期望图像和曝光时间(STL 是一种有用的常用函数和标准 C++ 中可用的数据结构的库)。 相机响应函数以 256 个实值向量的形式给出。 这表示像素值和辐照度之间的映射。 实际上,它是一个256 x 3的矩阵(三个颜色通道中的每个颜色通道一列)。 下图显示了示例给出的响应:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_06_04.jpg

估计的 RGB 相机响应函数

提示

代码的cout部分以 MATLAB 和 Octave(两种用于数值计算的包)使用的格式显示矩阵。 复制输出中的矩阵并将其粘贴到 MATLAB/Octave 中以进行显示很简单。

生成的 HDR 图像以无损 RGBE 格式存储。 此图像格式使用每个颜色通道一个字节,再加上一个字节作为共享指数。 格式使用与浮点数表示法相同的原理:共享指数允许您表示更大范围的值。 RGBE 图像使用.hdr扩展名。 请注意,由于它是无损图像格式,因此.hdr文件相对较大。 在此示例中,RGB 输入图像分别为 1224 x 1632(每个 100 至 200 KB),而输出.hdr文件占用 5.9 MB。

该示例使用 Debevec 和 Malik 的方法,但是 OpenCV 还基于 Robertson 的方法提供了另一个校准函数。 校准和合并函数均可用,即createCalibrateRobertsonMergeRobertson

注意

有关其他函数及其背后原理的更多信息,请参见这个页面

最后,请注意示例不会显示结果图像。 HDR 图像无法在常规屏幕中显示,因此我们需要执行另一步,称为色调映射。

色调映射

当要显示高动态范围图像时,信息可能会丢失。 这是由于,因为计算机屏幕的对比度也很有限,而且打印材料通常也限制为 256 色。 当我们具有高动态范围的图像时,有必要将强度映射到一组有限的值。 这称为色调映射。

为了提供逼真的输出,仅将 HDR 图像值缩放到显示设备的缩小范围是不够的。 缩放通常会产生缺乏细节(对比度)的图像,从而消除了原始场景内容。 最终,色调映射算法旨在提供视觉上看起来类似于原始场景的输出(即类似于人类在查看场景时所看到的输出)。 已经提出了各种音调映射算法,并且仍然是广泛研究的问题。 以下代码行可以将色调映射应用于上一个示例中获得的 HDR 图像:

Mat ldr;
Ptr<TonemapDurand> tonemap = createTonemapDurand(2.2f);
tonemap->process(hdr, ldr); // ldr is a floating point image with 
ldr=ldr*255;      //  values in interval [0..1]
imshow("LDR", ldr);

该方法由 Durand 和 Dorsey 于 2002 年提出。构造器实际上接受许多影响输出的参数。 下图显示了输出。 请注意,此图像不一定比三个原始图像中的任何一个都要好:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_06_05.jpg

音调输出

OpenCV 中提供了其他三种音调映射算法:createTonemapDragocreateTonemapReinhardcreateTonemapMantiuk

可以使用 MATLAB 显示 HDR 图像(RGBE 格式,即扩展名为.hdr的文件)。 它只需要三行代码:

hdr=hdrread('hdr.hdr');
rgb=tonemap(hdr); 
imshow(rgb);

注意

pfstools 是命令行工具的开源套件,用于读取,写入和渲染 HDR 图像。 该套件可以读取.hdr和其他格式,其中包括许多相机校准和色调映射算法。 Luminance HDR 是基于 pfstools 的免费 GUI 软件。

对齐

用多张曝光图像拍摄的场景必须是静态的。 摄像机也必须是静态的。 即使满足两个条件,也建议执行对齐过程。

OpenCV 提供了 G. Ward 在 2003 年提出的图像对齐算法。主要函数createAlignMTB采用定义最大位移的输入参数(实际上,每个尺寸的最大位移以 2 为底的对数)。 在上一示例中估计摄像机响应函数之前,应插入以下几行:

    vector<Mat> images_(images);
    Ptr<AlignMTB> align=createAlignMTB(4);// 4=max 16 pixel shift
    align->process(images_, images);

曝光融合

我们也可以使用相机响应校准(即曝光时间)或中间 HDR 图像,将具有多次曝光的图像组合在一起。 这称为曝光融合。 该方法由 Mertens 等人在 2007 年提出。以下几行执行曝光融合(images是输入图像的 STL 向量;请参见前面的示例):

    Mat fusion;
    Ptr<MergeMertens> merge_mertens = createMergeMertens();
    merge_mertens->process(images, fusion); // fusion is a 
 fusion=fusion*255; // float. point image w. values in [0..1]
 imwrite("fusion.png", fusion);

下图显示了结果:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_06_06.jpg

曝光融合

无缝克隆

在无缝克隆中,我们通常要在源图像中剪切一个对象/人并将其插入目标图像。 当然,这可以通过简单地粘贴对象以简单的方式完成。 但是,这不会产生现实的效果。 参见下图,例如,我们想要将图像上半部分的船插入图像下半部分的海中:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_06_07.jpg

克隆

从 OpenCV 3 开始,已有无缝克隆函数可用,其结果更为真实。 此函数称为seamlessClone,它使用 Perez 和 Gangnet 在 2003 年提出的方法。以下SeamlessCloning示例向您展示了如何使用它:

#include <opencv2/photo.hpp>
#include <opencv2/highgui.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main(int, char** argv)
{
    // Load and show images...
    Mat source = imread("source1.png", IMREAD_COLOR);
    Mat destination = imread("destination1.png", IMREAD_COLOR);
    Mat mask = imread("mask.png", IMREAD_COLOR);
    imshow("source", source);
    imshow("mask", mask);
    imshow("destination", destination);

    Mat result;
    Point p;    // p will be near top right corner
    p.x = (float)2*destination.size().width/3;
    p.y = (float)destination.size().height/4;
    seamlessClone(source, destination, mask, p, result, NORMAL_CLONE);
    imshow("result", result);

    cout << "\nDone. Press any key to exit...\n";
    waitKey(); // Wait for key press
    return 0;
}

这个例子很简单。 seamlessClone函数获取源图像,目标图像和遮罩图像以及目标图像中将插入裁剪对象的点(可以从这个页面)。 请参见下图的结果:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_06_08.jpg

无缝克隆

seamlessClone的最后一个参数表示要使用的确切方法(可以使用三种方法产生不同的最终效果)。 另一方面,库提供以下相关函数:

函数效果
colorChange将源图像的三个颜色通道中的每个乘以一个因子,仅在遮罩给定的区域中应用乘法
illuminationChange仅在遮罩指定的区域内更改源图像的照度
textureFlattening仅在遮罩指定的区域中洗掉源图像中的纹理

seamlessClone相反,这三个函数仅接受源图像和遮罩图像。

脱色

脱色是将彩色图像转换为灰度的过程。 有了这个定义,读者可能会问,我们是否已经有了灰度转换? 是的,灰度转换是 OpenCV 和任何图像处理库中的基本例程。 标准转换基于 R,G 和 B 通道的线性组合。 问题在于这种转换可能会产生原始图像中的对比度丢失的图像。 原因是两种不同的颜色(在原始图像中被视为对比度)可能最终被映射到相同的灰度值。 考虑将 A 和 B 这两种颜色转换为灰度。 假设 B 是 R 和 G 通道中 A 的变体:

A = (R, G, B) => G = (R + G + B) / 3
B = (R-x, G + x, B) => G = (R-x + G + x + B) / 3 = (R + G + B) / 3

即使它们被认为是截然不同的,两种颜色 A 和 B 也被映射为相同的灰度值! 以下脱色示例的图像显示如下:

#include <opencv2/photo.hpp>
#include <opencv2/highgui.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main(int, char** argv)
{
    // Load and show images...
    Mat source = imread("color_image_3.png", IMREAD_COLOR);
    imshow("source", source);

    // first compute and show standard grayscale conversion...
    Mat grayscale = Mat(source.size(),CV_8UC1);
    cvtColor(source, grayscale, COLOR_BGR2GRAY);
    imshow("grayscale",grayscale);

    // now compute and show decolorization...
    Mat decolorized = Mat(source.size(),CV_8UC1);
    Mat dummy = Mat(source.size(),CV_8UC3);
    decolor(source,decolorized,dummy);
    imshow("decolorized",decolorized);

    cout << "\nDone. Press any key to exit...\n";
    waitKey(); // Wait for key press
    return 0;
}

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_06_09.jpg

脱色示例输出

这个例子很简单。 读取图像并显示标准灰度转换的结果后,它使用decolor函数执行脱色。 所使用的图像(color_image_3.png文件)包含在 opencv_extra 存储库中,位于这个页面

注意

该示例中使用的图像实际上是一个极端的情况。 选择其颜色是为了使标准灰度输出相当均匀。

非真实感渲染

作为photo模块的的一部分,提供了四个函数,这些函数可以转换输入图像,从而产生不真实但仍具有艺术感的输出。 这些函数非常易于使用,OpenCV(npr_demo)中包含一个很好的示例。 为了说明的目的,在这里我们为您提供一个表格,让您掌握每个函数的效果。 看一下下面的fruits.jpg输入图像,包含在 OpenCV 中:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_06_10.jpg

输入参考图像

效果是:

函数影响
edgePreservingFilter平滑是一种方便且经常使用的过滤器。 此函数在保留对象边缘细节的同时执行平滑处理。
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_06_11.jpg
detailEnhance增强图像中的细节
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_06_12.jpg
pencilSketch输入图像的铅笔状线条图版本
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_06_13.jpg
stylization水彩效果
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659OT_06_14.jpg

总结

在本章中,您学习了什么是计算摄影以及 OpenCV 3 中可用的相关功能。我们解释了photo模块中最重要的函数,但请注意,在此模块中还考虑了该模块的其他功能(修复和降噪) 前几章。 计算摄影是一个快速发展的领域,与计算机图形学有着紧密的联系。 因此,预计 OpenCV 的此模块将在将来的版本中增加。

下一章将讨论我们尚未考虑的重要方面:时间。 解释的许多功能都需要花费大量时间来计算结果。 下一章将向您展示如何使用现代硬件进行处理。

七、加速图像处理

本章使用通用图形处理单元(GPGPU)或简称为 GPU 进行并行处理来加速图像处理任务。 GPU 本质上是专用于图形处理或浮点运算的协处理器,旨在提高视频游戏和交互式 3D 图形等应用的性能。 在 GPU 中执行图形处理时,CPU 可以专用于其他计算(例如游戏中的人工智能部分)。 每个 GPU 都配备了数百个简单的处理内核,这些内核可对(通常)浮点数的数百个“简单”数学运算进行大规模并行执行。

CPU 似乎已达到其速度和热功率极限。 用多个 CPU 构建计算机已成为一个复杂的问题。 这就是 GPU 发挥作用的地方。 GPU 处理是一种新的计算范例,它使用 GPU 来提高计算性能。 GPU 最初实现了某些称为图形基元的并行操作,这些并行操作已针对图形处理进行了优化。 抗锯齿是 3D 图形处理最常见的原语之一,它使图形的边缘具有更逼真的外观。 其他图元是矩形,三角形,圆形和弧形的图形。 GPU 当前包含数百个通用处理功能,它们的功能远远超过渲染图形。 特别是,它们在可以并行执行的任务中非常有价值,许多计算机视觉算法就是这种情况。

OpenCV 库包括对 OpenCL 和 CUDA GPU 架构的支持。 CUDA 实现了许多算法。 但是,它仅适用于 NVIDIA 图形卡。 CUDA 是由 NVIDIA 创建并由其产生的 GPU 实现的并行计算平台和编程模型。 本章重点介绍 OpenCL 架构,因为它受到更多设备的支持,甚至包括在某些 NVIDIA 图形卡中。

开放计算语言OpenCL)是框架,可编写可在连接到主机处理器(CPU)的 CPU 或 GPU 上执行的程序。 它定义了一种类似于 C 的语言来编写称为内核的函数,这些函数在计算设备上执行。 使用 OpenCL,内核可以在与 CPU 或 GPU 并行的所有或许多单个处理元素(PE)上运行。

此外,OpenCL 定义了应用编程接口API),该接口允许在主机(CPU)上运行的程序在计算机设备上启动内核并管理它们的设备存储器,(至少在概念上)与主机存储器分开。 OpenCL 程序旨在在运行时进行编译,以便使用 OpenCL 的应用可在各种主机设备的实现之间移植。 OpenCL 还是非盈利技术联盟 Khronos Group 维护的开放标准。

OpenCV 包含一组类和函数,这些类和函数使用 OpenCL 来实现和加速 OpenCV 功能。 OpenCV 当前提供一个透明的 API,该 API 可以将其原始 API 与 OpenCL 加速的编程统一起来。 因此,您只需要编写一次代码。 有一个新的统一数据结构(UMat),在需要且可能时处理向 GPU 的数据传输。

OpenCV 中对 OpenCL 的支持是为了易于使用而设计的,不需要任何 OpenCL 知识。 在最低程度上,它可以看作是一组加速,在使用现代 CPU 和 GPU 设备时可以利用强大的计算能力。

要正确运行 OpenCL 程序,OpenCL 运行时应由设备供应商提供,通常以设备驱动程序的形式提供。 另外,要将 OpenCV 与 OpenCL 一起使用,需要兼容的 SDK。 当前,有五个可用的 OpenCL SDK:

  • AMD APP SDK:此 SDK 在 CPU 和 GPU(例如 X86 + SSE2(或更高)CPU 和 AMD Fusion,AMD Radeon,AMD Mobility 和 ATI FirePro GPU)上支持 OpenCL。
  • Intel SDK:此 SDK 在 Intel Core 处理器和 Intel HD GPU(例如 Intel + SSE4.1,SSE4.2 或 AVX,Intel Core i7,i5 和 i3( 第 1 代,第 2 代和第 3 代),Intel HD Graphics,Intel Core 2 Solo(Duo Quad 和 Extreme)和 Intel Xeon CPU。
  • IBM OpenCL 开发套件:此 SDK 在 AMD 服务器(例如 IBM Power,IBM PERCS 和 IBM BladeCenter)上支持 OpenCL。
  • IBM OpenCL 通用运行时:此 SDK 在 CPU 和 GPU(例如 X86 + SSE2(或更高版本) CPU 和 AMD Fusion and Raedon,NVIDIA Ion,NVIDIA GeForce 和 NVIDIA)上支持 OpenCV Quadro GPU。
  • Nvidia OpenCL 驱动程序和工具:此 SDK 在某些 Nvidia 图形设备(例如 NVIDIA Tesla,NVIDIA GeForce,NVIDIA Ion 和 NVIDIA Quadro GPU)上支持 OpenCL。

安装了 OpenCL 的 OpenCV

第 1 章,“处理图像和视频文件”中已经介绍了安装步骤,还需要一些其他步骤来包含 OpenCL。 下节介绍了新需要的软件。

在 Windows 上使用 OpenCL 编译和安装 OpenCV 有一些新要求:

  • 支持 OpenCL 的 GPU 或 CPU:这是最重要的要求。 请注意,OpenCL 支持许多计算设备,但不是全部。 您可以检查图形卡或处理器是否与 OpenCL 兼容。 本章将用于 AMD FirePro W5000 GPU 的 AMD APP SDK 用于执行示例。

    注意

    这个页面上有此 SDK 的支持的计算机设备列表。您还可以在此处查询所需的最低 SDK 版本。

  • 编译器:具有 OpenCL 的 OpenCV 与 Microsoft 和 MinGW 编译器兼容。 可以安装免费的 Visual Studio Express 版本。 但是,如果选择 Microsoft 编译 OpenCV,则建议至少使用 Visual Studio 2012。 但是,本章使用 MinGW 编译器。

  • AMD APP SDK:此 SDK 是一组高级软件技术,使我们能够使用兼容的计算设备来执行和加速除图形之外的许多应用。 该 SDK 可在以下位置获得。本章使用 SDK 的 2.9 版(适用于 64 位 Windows)。 您可以在以下屏幕截图中看到安装进度。

    注意

    如果此步骤失败,则可能需要更新图形卡的控制器。 可在这个页面上获得 AMD 控制器。

    https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659O9_07_01.jpg

    安装 AMD APP SDK

  • OpenCL BLAS基本线性代数子例程BLAS)是一组开源数学库,用于在 AMD 设备上进行并行处理。 可以从这个页面下载。 本章使用 Windows 32/64 位的 1.1 BLAS 版本,并且可以在以下屏幕截图(左侧)中看到安装进度。

  • OpenCL FFT快速傅立叶变换FFT)是许多图像处理算法需要的非常有用的功能。 因此,此功能可在 AMD 设备上实现并行处理。 可以从与前面相同的 URL 下载。 本章使用 Windows 32/64 位的 1.1 FFT 版本,并且可以在以下屏幕截图(右侧)中看到安装进度:

    https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659O9_07_02.jpg

    为 OpenCL 安装 BLAS 和 FFT

  • 用于 C++ 编译器的 Qt 库:在本章中,使用 Qt 库的 MinGW 二进制文件通过 OpenCL 编译 OpenCV。 另一种选择是安装最新版本的 Qt 并使用 Visual C++ 编译器。 您可以选择 Qt 版本和使用的编译器。 通过位于C:\Qt\Qt5.3.1MaintenanceTool.exe应用,包管理器可用于下载其他 Qt 版本。 本章使用 Qt(5.3.1)和 MinGW(4.8.2)32 位来使用 OpenCL 编译 OpenCV。

当满足之前的要求时,您可以使用 CMake 生成新的构建配置。 该过程与第一章中介绍的典型安装在某些方面有所不同。 差异在此列表中说明:

  • 为项目选择生成器时,可以选择与计算机中已安装环境相对应的编译器版本。 本章使用 MinGW 使用 OpenCL 编译 OpenCV,然后选择MinGW Makefiles选项,并指定本机编译器。 以下屏幕截图显示了此选择:

    https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659O9_07_03.jpg

    CMake 选择生成器项目

  • 以下屏幕截图中显示的选项是构建带有 OpenCL 项目的 OpenCV 所必需的。 必须启用WITH_OPENCLWITH_OPENCLAMDBLASWITH_OPENCLAMDFFT选项。 必须在CLAMDBLAS_INCLUDE_DIRCLAMDBLAS_ROOT_DIRCLAMDFFT_INCLUDE_DIRCLAMDFFT_ROOT_DIR上引入 BLAS 和 FFT 路径。 此外,如第 1 章“处理图像和视频文件”中所示,您将需要启用WITH_QT并禁用WITH_IPP选项。 也建议启用BUILD_EXAMPLES。 以下屏幕截图显示了在构建配置中选择的主要选项:

    https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659O9_07_09.jpg

    CMake 选择主要选项

最后,要使用 OpenCL 项目构建 OpenCV,必须编译先前生成的 CMake 项目。 该项目是为 MinGW 生成的,因此,需要 MinGW 编译器来构建此项目。 首先,使用 Windows 控制台选择[opencv_build]/文件夹,然后执行以下操作:

./mingw32-make.exe -j 4 install

-j 4参数是我们要用于编译并行化的系统核心 CPU 的数量。

现在可以使用带有 OpenCL 项目的 OpenCV。 新二进制文件的路径必须添加到系统路径,在这种情况下为[opencv_build]/install/x64/mingw/bin

注意

不要忘记从路径环境变量中删除旧的 OpenCV 二进制文件。

使用 OpenCL 安装 OpenCV 的快速方法

可以通过以下步骤总结安装过程:

  1. 下载并安装 AMD APP SDK,该软件可从这个页面获得。
  2. 下载并安装 BLAS 和 FFT AMD,它们可从这个页面
  3. 使用 CMake 配置 OpenCV 构建。 启用 WITH_OPENCLWITH_OPENCLAMDBLAS,WITH_QTBuild_EXAMPLESWITH_OPENCLAMDFFT 选项。 禁用 WITH_IPP 选项。 最后,介绍 CLAMDBLAS_INCLUDE_DIRCLAMDBLAS_ROOT_DIRCLAMDFFT_INCLUDE_DIRCLAMDFFT_ROOT_DIR 上的 BLAS 和 FFT 路径。
  4. mingw32-make.exe编译 OpenCV 项目。
  5. 最后,修改路径环境变量以更新 OpenCV bin目录(例如[opencv_build]/install/x64/mingw/bin)。

检查 GPU 使用情况

在 Windows 平台上使用 GPU 时,没有应用可以测量其使用情况。 使用 GPU 的原因有两个:

  • 可以知道您是否正确使用了 GPU
  • 您可以监控 GPU 使用率

为此,市场上有一些应用。 本章使用 AMD 系统监视器检查 GPU 的使用情况。 此应用监视 CPU,内存 RAM 和 GPU 的使用情况。 请参考以下屏幕截图:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659O9_07_05.jpg

AMD 系统监视器可监视 CPU,GPU 和内存 RAM 的使用情况

注意

可以从这个页面下载 Microsoft System Monitor (32 或 64 位)。

加速您自己的功能

在本节中,有使用 OpenCV 和 OpenCL 的三个示例。 第一个示例使您可以检查已安装的 SDK 是否可用,并获取有关支持 OpenCL 的计算设备的有用信息。 第二个示例分别显示使用 CPU 和 GPU 编程的同一程序的两个版本。 最后一个示例是检测和标记人脸的完整程序。 另外,执行计算比较。

检查您的 OpenCL

以下是一个简单的程序,用于检查您的 SDK 和可用的计算设备。 该示例是,称为checkOpenCL。它允许您使用 OpenCV 的 OCL 模块显示计算机设备:

#include <opencv2/opencv.hpp>
#include <opencv2/core/ocl.hpp>

using namespace std;
using namespace cv;
using namespace cv::ocl;

int main()
{
vector<ocl::PlatformInfo> info;
 getPlatfomsInfo(info);
PlatformInfo sdk = info.at(0);

    if (sdk.deviceNumber()<1)
        return -1;

    cout << "******SDK*******" << endl;
    cout << "Name: " <<sdk.name()<< endl;
cout << "Vendor: " <<sdk.vendor()<< endl;
cout << "Version: " <<sdk.version()<< endl;
    cout << "Number of devices: " <<sdk.deviceNumber()<< endl;

    for (int i=0; i<sdk.deviceNumber(); i++){
Device device;
 sdk.getDevice(device, i);
          cout << "\n\n*********************\n Device " << i+1 << endl;

        cout << "Vendor ID: " <<device.vendorID()<< endl;
        cout << "Vendor name: " <<device.vendorName()<< endl;
        cout << "Name: " <<device.name()<< endl;
        cout << "Driver version: " <<device.driverVersion()<< endl;
        if (device.isAMD()) cout << "Is an AMD device" << endl;
        if (device.isIntel()) cout << "Is a Intel device" << endl;
        cout << "Global Memory size: " <<device.globalMemSize()<< endl;
        cout << "Memory cache size: " <<device.globalMemCacheSize()<< endl;
        cout << "Memory cache type: " <<device.globalMemCacheType()<< endl;
        cout << "Local Memory size: " <<device.localMemSize()<< endl;
        cout << "Local Memory type: " <<device.localMemType()<< endl;
        cout << "Max Clock frequency: " <<device.maxClockFrequency()<< endl;
    }

    return 0;
}

代码说明

本示例显示安装的 SDK 和与 OpenCL 兼容的可用计算设备。 首先,包含core/ocl.hpp标头,并声明cv::ocl命名空间。

使用getPlatfomsInfo(info)方法获取有关计算机中可用 SDK 的信息。 该信息存储在vector<ocl::PlatformInfo> info向量中,并通过PlatformInfo sdk = info.at(0)选择。 然后,将显示有关您的 SDK 的主要信息,例如名称,供应商,SDK 版本以及与 OpenCL 兼容的计算设备的数量。

最后,对于每个兼容设备,其信息都是通过sdk.getDevice(device, i)方法获得的。 现在可以显示有关每个计算设备的不同信息,例如供应商 ID,供应商名称,驱动程序版本,全局内存大小,内存缓存大小等。

下面的屏幕截图显示了该示例对所用计算机的结果:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659O9_07_06.jpg

有关使用的 SDK 和兼容的计算设备的信息

您的第一个基于 GPU 的程序

在下面的代码中,显示了同一程序的两个版本:一个仅使用 CPU(本机)执行计算,另一个使用 GPU(带有 OpenCL)。 这两个示例分别称为calculateEdgesCPUcalculateEdgesGPU,使您可以观察 CPU 和 GPU 版本之间的差异。

首先显示计算边缘 CPU 示例:

#include <opencv2/opencv.hpp>

using namespace std;
using namespace cv;

int main(int argc, char * argv[])
{
    if (argc < 2)
    {
        cout << "./calculateEdgesCPU <image>" << endl;
        return -1;
    }

Mat cpuFrame = imread(argv[1]);
    Mat cpuBW, cpuBlur, cpuEdges;

    namedWindow("Canny Edges CPU",1);

cvtColor(cpuFrame, cpuBW, COLOR_BGR2GRAY);
 GaussianBlur(cpuBW, cpuBlur, Size(1,1), 1.5, 1.5);
 Canny(cpuBlur, cpuEdges, 50, 100, 3);

imshow("Canny Edges CPU", cpuEdges);
    waitKey();

    return 0;
}

现在,显示计算边缘 GPU 示例:

#include "opencv2/opencv.hpp"
#include "opencv2/core/ocl.hpp"

using namespace std;
using namespace cv;
using namespace cv::ocl;

int main(int argc, char * argv[])
{
    if (argc < 2)
    {
        cout << "./calculateEdgesGPU <image>" << endl;
        return -1;
    }

setUseOpenCL(true);

Mat cpuFrame = imread(argv[1]);
UMat gpuFrame, gpuBW, gpuBlur, gpuEdges;

cpuFrame.copyTo(gpuFrame);

    namedWindow("Canny Edges GPU",1);

    cvtColor(gpuFrame, gpuBW, COLOR_BGR2GRAY);
    GaussianBlur(gpuBW, gpuBlur, Size(1,1), 1.5, 1.5);
    Canny(gpuBlur, gpuEdges, 50, 100, 3);

imshow("Canny Edges GPU", gpuEdges);
    waitKey();

    return 0;
}

代码说明

这两个示例获得相同的结果,如以下屏幕截图所示。 他们从标准命令行输入参数读取图像。 然后,将图像转换为灰度,并应用高斯模糊和 Canny 过滤器功能。

在第二个示例中,使用 GPU 需要一些区别。 首先,必须使用setUseOpenCL(true)方法激活 OpenCL。 其次,统一矩阵UMat)用于在 GPU(UMat gpuFrame, gpuBW, gpuBlur, gpuEdges)中分配内存。 第三,使用cpuFrame.copyTo(gpuFrame)方法将输入图像从 RAM 复制到 GPU 内存。 现在,使用这些功能时,如果它们具有 OpenCL 实现,则这些功能将在 GPU 上执行。 如果其中一些功能没有 OpenCL 实现,则正常功能将在 CPU 上执行。 在此示例中,使用 GPU 编程(第二示例)的时间要好 10 倍:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659O9_07_07.jpg

前两个示例的结果

实时

GPU 处理的主要优点之一是以更快的方式执行计算。 速度的提高使您可以在实时应用中执行繁重的计算算法,例如立体视觉,行人检测,光流或人脸检测。 以下detectFaces示例向您展示了一种用于检测摄像机面部的应用。 此示例还允许您在 CPUGPU 处理之间进行选择,以比较计算时间。

在 OpenCV 示例([opencv_source_code]/samples/cpp/facedetect.cpp)中,可以找到相关的人脸检测器示例。 对于以下detectFaces示例,detectFace.pro项目需要以下库:-lopencv_core300-opencv_imgproc300-lopencv_highgui300-lopencv_videoio300lopencv_objdetct300

detectFaces示例使用 OpenCV 的ocl模块:

#include <opencv2/core/core.hpp>
#include <opencv2/core/ocl.hpp>
#include <opencv2/objdetect.hpp>
#include <opencv2/videoio.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>

#include <iostream>
#include <stdio.h>

using namespace std;
using namespace cv;
using namespace cv::ocl;

int main(int argc, char * argv[])
{
    // 1- Set the initial parameters
    // Vector to store the faces
    vector<Rect> faces;
    CascadeClassifier face_cascade;
String face_cascade_name = argv[2];
    int face_size = 30;
    double scale_factor = 1.1;
    int min_neighbours = 2;

VideoCapture cap(0);
UMat frame, frameGray;
    bool finish = false;

    // 2- Load the file xml to use the classifier
    if (!face_cascade.load(face_cascade_name))
    {
        cout << "Cannot load the face xml!" << endl;
        return -1;
    }

    namedWindow("Video Capture");

    // 3- Select between the CPU or GPU processing
    if (argc < 2)
    {
          cout << "./detectFaces [CPU/GPU | C/G]" << endl;
          cout << "Trying to use GPU..." << endl;
setUseOpenCL(true);
    }
    else
    {
        cout << "./detectFaces trying to use " << argv[1] << endl;
        if(argv[1][0] == 'C')
            // Trying to use the CPU processing
            setUseOpenCL(false);
        else
            // Trying to use the GPU processing
            setUseOpenCL(true);
    }

    Rect r;
 double start_time, finish_time, start_total_time,       finish_total_time;
    int counter = 0;

    // 4- Detect the faces for each image capture
    start_total_time = getTickCount();
    while (!finish)
    {
        start_time = getTickCount();
cap >> frame;
        if (frame.empty())
        {
            cout << "No capture frame --> finish" << endl;
            break;
        }

cvtColor(frame, frameGray, COLOR_BGR2GRAY);
equalizeHist(frameGray,frameGray);

        // Detect the faces
face_cascade.detectMultiScale(frameGray, faces, scale_factor, min_neighbours, 0|CASCADE_SCALE_IMAGE, Size(face_size,face_size));

        // For each detected face
        for (int f = 0; f <faces.size(); f++)
        {
            r = faces[f];
            // Draw a rectangle over the face
rectangle(frame, Point(r.x, r.y), Point(r.x + r.width, r.y + r.height), Scalar(0,255,0), 3);
        }

        // Show the results
imshow("Video Capture",frame);

        // Calculate the time processing
        finish_time = getTickCount();
cout << "Time per frame: " << (finish_time - start_time)/getTickFrequency() << " secs" << endl;

        counter++;

        // Press Esc key to finish
        if(waitKey(1) == 27) finish = true;
    }

    finish_total_time = getTickCount();
cout << "Average time per frame: " << ((finish_total_time - start_total_time)/getTickFrequency())/counter << " secs" << endl;

    return 0;
}

代码说明

第一步,设置初始参数,例如使用分类器检测面部的 xml 文件(String face_cascade_name argv[2]),每个检测到的面部的最小尺寸(face_size=30),比例因子(scale_factor = 1.1 ),以及在真正例和假正面检测之间进行权衡的最小邻居数(min_neighbours = 2)。 您还可以看到 CPU 和 GPU 源代码之间更重要的区别。 您只需要使用统一矩阵UMat frame, frameGray)。

注意

[opencv_source_code]/data/haarcascades/文件夹中还有其他可用的 xml 文件,用于检测不同的身体部位,例如眼睛,下半身,微笑等。

第二步,使用前面的 xml 文件创建检测器以检测面部。 该检测器基于基于 Haar 特征的分类器,这是 Paul Viola 和 Michael Jones 提出的一种有效的对象检测方法。 该分类器具有高精度的人脸检测。 此步骤使用face_cascade.load( face_cascade_name)方法加载 xml 文件。

注意

您可以在这个页面上找到有关 Paul Viola 和 Michael Jones 方法的更多详细信息。

第三步,您可以在 CPU 或 GPU 处理(分别为setUseOpenCL(false)setUseOpenCL(true))之间进行选择。 本示例使用标准命令行输入参数(argv[1])进行选择。 用户可以从 Windows 控制台执行以下操作,以分别在 CPU 或 GPU 处理以及分类器路径之间进行选择:

<bin_dir>/detectFaces CPU pathClassifier
<bin_dir>/detectFaces GPU pathClassifier

如果用户未引入输入参数,则使用 GPU 处理。

第四步为从摄像机捕获的每个图像检测面部。 在此之前,每个捕获的图像都将转换为灰度(cvtColor(frame, frameGray, COLOR_BGR2GRAY))并对其直方图进行均衡(equalizeHist(frameGray, frameGray))。 然后,使用创建的人脸检测器,使用face_cascade.detectMultiScale(frameGray, faces, scale_factor, min_neighbours, 0|CASCADE_SCALE_IMAGE, Size(face_size,face_size))多尺度检测方法在当前帧中搜索不同的面部。 最后,在每个检测到的面部上绘制一个绿色矩形,然后将其显示。 以下屏幕截图显示了此示例运行的屏幕截图:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learn-imgproc-opencv/img/7659O9_07_08.jpg

前面的例子检测人脸

性能

在前面的示例中,计算了计算时间以比较 CPU 和 GPU 处理。 获得每帧的平均处理时间。

选择 GPU 编程的一大优势是性能。 因此,前面的示例计算时间测量值,以比较相对于 CPU 版本获得的加速比。 时间使用getTickCount()方法存储在程序的开头。 之后,在程序结束时,再次使用相同的函数来估计时间。 存储计数器以也知道迭代次数。 最后,计算每帧的平均处理时间。 前面的示例使用 GPU 时每帧的平均处理时间为 0.057 秒(或 17.5 FPS),而使用 CPU 的相同示例时,每帧的平均处理时间为每帧 0.335 秒(或 2.9 FPS)。 总之,速度增量为6x。 此增量非常重要,尤其是当您只需要更改几行代码时。 但是,有可能实现更高的速度增加速率,这与问题甚至内核的设计有关。

总结

在本章中,您学习了如何在计算机上安装带有 OpenCL 的 OpenCV 以及如何使用与 OpenCL 兼容的最新 OpenCV 版本的计算机设备开发应用。

第一部分说明 OpenCL 是什么以及可用的 SDK。 请记住,取决于您的计算设备,您将需要特定的 SDK 才能与 OpenCL 一起正常使用。 在第二部分中,说明了使用 OpenCL 安装 OpenCV 的安装过程,并使用了 AMD APP SDK。 在上一节中,有三个使用 GPU 编程的示例(第二个示例也具有 CPU 版本以便进行比较)。 此外,在最后一节中,在 CPU 和 GPU 处理之间进行了计算比较,显示 GPU 比 CPU 版本快六倍。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值