跳转至

四、深入研究直方图和过滤器

在上一章中,我们学习了 OpenCV 中使用 Qt 库或本机库的用户界面的基础知识;我们还学习了如何使用高级 OpenGL 用户界面。 我们了解了基本的颜色转换和允许我们创建第一个应用的过滤器。 本章将向您介绍以下概念:

  • 直方图和直方图均衡化
  • 查找表
  • 模糊和中间模糊
  • 精明过滤器
  • 图像颜色均衡
  • 了解图像类型之间的转换

在我们了解了 OpenCV 和用户界面的基础知识之后,我们将在本章中创建我们的第一个完整的应用,一个基本的照片工具,并涵盖以下主题:

  • 生成 CMake 脚本文件
  • 创建图形用户界面
  • 计算和绘制直方图
  • 直方图均衡
  • Lomography 相机效果
  • 卡通化效果

这个应用将帮助我们理解如何从头开始创建整个项目,并理解直方图的概念。 我们将结合使用滤镜和查找表,了解如何均衡彩色图像的直方图并创建两种效果。

技术要求

本章要求您熟悉 C++ 编程语言的基础知识。 本章使用的所有代码都可以从以下 giHub 链接下载:https://github.com/PacktPublishing/Building-Computer-Vision-Projects-with-OpenCV4-and-CPlusPlus/tree/master/Chapter04。 该代码可以在任何操作系统上执行,尽管它只在 Ubuntu 上进行了测试。

请查看以下视频,了解实际操作中的代码: http://bit.ly/2Sid17y

生成 CMake 脚本文件

在开始创建源文件之前,我们将生成CMakeLists.txt文件,以允许我们编译、构造和执行项目。 下面的 CMake 脚本简单而基本,但足以编译和生成可执行文件:

cmake_minimum_required (VERSION 3.0)

PROJECT(Chapter4_Phototool)

set (CMAKE_CXX_STANDARD 11)

# Requires OpenCV
FIND_PACKAGE( OpenCV 4.0.0 REQUIRED )
MESSAGE("OpenCV version : ${OpenCV_VERSION}")

include_directories(${OpenCV_INCLUDE_DIRS})
link_directories(${OpenCV_LIB_DIR})

ADD_EXECUTABLE(${PROJECT_NAME} main.cpp)
TARGET_LINK_LIBRARIES(${PROJECT_NAME} ${OpenCV_LIBS})

第一行表示生成项目所需的最低 CMake 版本,第二行设置我们可以用作${PROJECT_NAME}变量的项目名称,第三行设置所需的 C++ 版本;在我们的示例中,我们需要C++ 11版本,如下面的代码片段所示:

cmake_minimum_required (VERSION 3.0)

PROJECT(Chapter4_Phototool)

set (CMAKE_CXX_STANDARD 11)

此外,我们还需要 OpenCV 库。 首先,我们需要找到该库,然后我们将显示一条关于使用MESSAGE函数找到的 OpenCV 库版本的消息:

# Requires OpenCV 
FIND_PACKAGE( OpenCV 4.0.0 REQUIRED ) 
MESSAGE("OpenCV version : ${OpenCV_VERSION}") 

如果找到最低版本为 4.0 的库,则我们将在项目中包含头文件和库文件:

include_directories(${OpenCV_INCLUDE_DIRS}) 
link_directories(${OpenCV_LIB_DIR})

现在,我们只需要添加要编译并链接到 OpenCV 库的源文件。 项目名称变量用作可执行文件名,我们只使用一个名为main.cpp的源文件:

ADD_EXECUTABLE(${PROJECT_NAME} main.cpp) 
TARGET_LINK_LIBRARIES(${PROJECT_NAME} ${OpenCV_LIBS})

创建图形用户界面

在开始图像处理算法之前,我们先为应用创建主用户界面。 我们将使用基于 Qt 的用户界面来创建单个按钮。 应用接收一个输入参数来加载要处理的图像,我们将创建四个按钮,如下所示:

  • 显示直方图
  • 均衡化直方图
  • 光照相效果
  • 卡通化效果

我们可以在下面的截图中看到四个结果:

让我们开始开发我们的项目吧。 首先,我们将包含 OpenCV 所需的标头,定义一个图像矩阵来存储输入图像,并创建一个常量字符串以使用 OpenCV 3.0 中已有的新命令行解析器;在该常量中,我们只允许两个输入参数help和所需的图像输入:

// OpenCV includes 
#include "opencv2/core/utility.hpp" 
#include "opencv2/imgproc.hpp" 
#include "opencv2/highgui.hpp" 
using namespace cv; 
// OpenCV command line parser functions 
// Keys accepted by command line parser 
const char* keys = 
{ 
   "{help h usage ? | | print this message}" 
    "{@image | | Image to process}" 
}; 

Main 函数从命令行解析器变量开始;接下来,我们设置关于指令并打印帮助消息。 该行设置最终可执行文件的帮助指令:

int main(int argc, const char** argv) 
{ 
   CommandLineParser parser(argc, argv, keys); 
    parser.about("Chapter 4\. PhotoTool v1.0.0"); 
    //If requires help show 
    if (parser.has("help")) 
   { 
       parser.printMessage(); 
       return 0; 
   } 

如果用户不需要帮助,那么我们必须获取imgFile变量字符串中的文件路径图像,并使用parser.check()函数检查是否添加了所有必需的参数:

String imgFile= parser.get<String>(0); 

// Check if params are correctly parsed in his variables 
if (!parser.check()) 
{ 
    parser.printErrors(); 
    return 0; 
}

现在,我们可以使用imread函数读取图像文件,然后使用namedWindow函数创建稍后将在其中显示输入图像的窗口:

// Load image to process 
Mat img= imread(imgFile); 

// Create window 
namedWindow("Input"); 

加载图像并创建窗口后,我们只需要为界面创建按钮,并将它们与回调函数链接起来;每个回调函数都在源代码中定义,我们将在本章后面解释这些函数。 我们将使用createButton函数创建具有QT_PUSH_BUTTON常量的按钮样式:

// Create UI buttons 
createButton("Show histogram", showHistoCallback, NULL, QT_PUSH_BUTTON, 0); 
createButton("Equalize histogram", equalizeCallback, NULL, QT_PUSH_BUTTON, 0); 
createButton("Lomography effect", lomoCallback, NULL, QT_PUSH_BUTTON, 0); 
createButton("Cartoonize effect", cartoonCallback, NULL, QT_PUSH_BUTTON, 0); 

要完成我们的主要功能,我们显示输入图像并等待按键完成我们的应用:

// Show image 
imshow("Input", img); 

waitKey(0); 
return 0; 

现在,我们只需定义每个回调函数,在下一节中,我们将这样做。

绘制直方图

直方图是变量分布的统计图形表示,它使我们能够理解数据的密度估计和概率分布。 直方图是通过将整个变量值范围划分为一个小范围的值,然后计算每个区间内有多少值来创建的。

如果我们把这个直方图概念应用到一幅图像上,看起来很难理解,但实际上,它很简单。 在灰度图像中,我们的变量值的范围是每个可能的灰度值(从0255),密度是具有该值的图像的像素数。 这意味着我们必须计算图像中值为0的像素数、值为1的像素数,依此类推。

显示输入图像直方图的回调函数为showHistoCallback;此函数计算每个通道图像的直方图,并在新图像中显示每个直方图通道的结果。

现在,检查以下代码:

void showHistoCallback(int state, void* userData) 
{ 
    // Separate image in BRG 
    vector<Mat> bgr; 
    split(img, bgr); 

    // Create the histogram for 256 bins 
    // The number of possibles values [0..255] 
    int numbins= 256; 

    /// Set the ranges for B,G,R last is not included 
    float range[] = { 0, 256 } ; 
    const float* histRange = { range }; 

    Mat b_hist, g_hist, r_hist; 

    calcHist(&bgr[0], 1, 0, Mat(), b_hist, 1, &numbins, &histRange); 
    calcHist(&bgr[1], 1, 0, Mat(), g_hist, 1, &numbins, &histRange); 
    calcHist(&bgr[2], 1, 0, Mat(), r_hist, 1, &numbins, &histRange); 

    // Draw the histogram 
    // We go to draw lines for each channel 
    int width= 512; 
    int height= 300; 
    // Create image with gray base 
    Mat histImage(height, width, CV_8UC3, Scalar(20,20,20)); 

    // Normalize the histograms to height of image 
    normalize(b_hist, b_hist, 0, height, NORM_MINMAX); 
    normalize(g_hist, g_hist, 0, height, NORM_MINMAX); 
    normalize(r_hist, r_hist, 0, height, NORM_MINMAX); 

    int binStep= cvRound((float)width/(float)numbins); 
    for(int i=1; i< numbins; i++) 
    { 
        line(histImage,  
                Point( binStep*(i-1), height-cvRound(b_hist.at<float>(i-1) )), 
                Point( binStep*(i), height-cvRound(b_hist.at<float>(i) )), 
                Scalar(255,0,0) 
            ); 
        line(histImage,  
                Point(binStep*(i-1), height-cvRound(g_hist.at<float>(i-1))), 
                Point(binStep*(i), height-cvRound(g_hist.at<float>(i))), 
                Scalar(0,255,0) 
            ); 
        line(histImage,  
                Point(binStep*(i-1), height-cvRound(r_hist.at<float>(i-1))), 
                Point(binStep*(i), height-cvRound(r_hist.at<float>(i))), 
                Scalar(0,0,255) 
            ); 
    } 

    imshow("Histogram", histImage); 

} 

让我们了解如何提取每个通道直方图以及如何绘制它。 首先,我们需要创建三个矩阵来处理每个输入图像通道。 我们使用一个矢量型变量来存储每个变量,并使用splitOpenCV 函数在这三个通道之间划分输入图像:

// Separate image in BRG 
    vector<Mat> bgr; 
    split(img, bgr); 

现在,我们将定义直方图的箱数,在我们的示例中,每个可能的像素值一个:

int numbins= 256; 

让我们定义变量范围并创建三个矩阵来存储每个直方图:

/// Set the ranges for B,G,R 
float range[] = {0, 256} ; 
const float* histRange = {range}; 

Mat b_hist, g_hist, r_hist;

我们可以使用calcHistOpenCV 函数计算直方图。 此函数有几个参数,顺序如下:

  • 输入图像:在我们的示例中,我们使用存储在bgr向量中的一个图像通道
  • 输入中用于计算直方图的图像数量:在我们的示例中,我们只使用1图像
  • 用于计算直方图的数字通道维度:我们在本例中使用[T0
  • 可选的掩码矩阵。
  • 用于存储计算出的直方图的变量。
  • 直方图维度:这是图像(这里是灰色平面)取值的空间维度,在我们的示例中为1
  • 要计算的条柱数量:在我们的示例中为256个条柱,每个像素值一个
  • 输入变量的范围:在我们的例子中,可能的像素值从0255

每个通道的calcHist函数如下所示:

calcHist(&bgr[0], 1, 0, Mat(), b_hist, 1, &numbins, &histRange ); 
calcHist(&bgr[1], 1, 0, Mat(), g_hist, 1, &numbins, &histRange ); 
calcHist(&bgr[2], 1, 0, Mat(), r_hist, 1, &numbins, &histRange ); 

现在我们已经计算了每个通道直方图,我们必须绘制每个通道直方图并将其显示给用户。 为此,我们将创建一个大小为512x300像素的彩色图像:

// Draw the histogram 
// We go to draw lines for each channel 
int width= 512; 
int height= 300; 
// Create image with gray base 
Mat histImage(height, width, CV_8UC3, Scalar(20,20,20)); 

在将直方图值绘制到图像中之前,我们将标准化最小值0和最大值之间的直方图矩阵;最大值与输出直方图图像的高度相同:

// Normalize the histograms to height of image 
normalize(b_hist, b_hist, 0, height, NORM_MINMAX); 
normalize(g_hist, g_hist, 0, height, NORM_MINMAX); 
normalize(r_hist, r_hist, 0, height, NORM_MINMAX);

现在我们必须从 bin0到 bin1画一条线,依此类推。 在每个 bin 之间,我们必须计算有多少像素;然后,通过宽度除以 bin 的数量计算出一个binStep变量。 每条小线从水平位置i-1i绘制;垂直位置是对应i中的直方图值,并使用颜色通道表示法绘制:

int binStep= cvRound((float)width/(float)numbins); 
    for(int i=1; i< numbins; i++) 
    { 
        line(histImage,  
                Point(binStep*(i-1), height-cvRound(b_hist.at<float>(i-1))), 
                Point(binStep*(i), height-cvRound(b_hist.at<float>(i))), 
                Scalar(255,0,0) 
            ); 
        line(histImage,  
                Point(binStep*(i-1), height-cvRound(g_hist.at<float>(i-1))), 
                Point( binStep*(i), height-cvRound(g_hist.at<float>(i))), 
                Scalar(0,255,0) 
            ); 
        line(histImage,  
                Point(binStep*(i-1), height-cvRound(r_hist.at<float>(i-1))), 
                Point( binStep*(i), height-cvRound(r_hist.at<float>(i))), 
                Scalar(0,0,255) 
            ); 
    } 

最后,我们使用imshow函数显示直方图图像:

    imshow("Histogram", histImage); 

这是lena.png图像的结果:

图像色彩均衡

在本节中,我们将学习如何均衡彩色图像。 图像均衡化,或直方图均衡化,试图获得值分布均匀的直方图。 均衡的结果是增加了图像的对比度。 均衡可以使局部对比度较低的区域获得高对比度,从而分散最频繁的亮度。 当图像非常暗或很亮,并且背景和前景之间的差异非常小时,这种方法非常有用。 使用直方图均衡化,我们增加了对比度和曝光过多或曝光不足的细节。 这项技术在医学图像(如 X 射线)中非常有用。

然而,这种方法有两个主要缺点:背景噪声的增加和有用信号的减少。 我们可以在下面的照片中看到均衡的效果,直方图在增加图像对比度时会发生变化和扩散:

让我们实现我们的均衡直方图;我们将在用户界面代码中定义的Callback函数中实现它:

void equalizeCallback(int state, void* userData)
{ 
    Mat result; 
    // Convert BGR image to YCbCr 
    Mat ycrcb; 
    cvtColor(img, ycrcb, COLOR_BGR2YCrCb); 

    // Split image into channels 
    vector<Mat> channels; 
    split(ycrcb, channels); 

    // Equalize the Y channel only 
    equalizeHist(channels[0], channels[0]); 

    // Merge the result channels 
    merge(channels, ycrcb); 

    // Convert color ycrcb to BGR 
    cvtColor(ycrcb, result, COLOR_YCrCb2BGR); 

    // Show image 
    imshow("Equalized", result); 
} 

要均衡彩色图像,我们只需均衡亮度通道。 我们可以对每个颜色通道执行此操作,但结果不可用。 或者,我们可以使用分离单个通道中亮度分量的任何其他彩色图像格式,例如HSVYCrCb。 因此,我们选择YCrCb并使用 Y 通道(亮度)进行均衡。 然后,我们遵循以下步骤:

1.使用cvtColor函数将bgr图像转换或输入为YCrCb

Mat result; 
// Convert BGR image to YCbCr 
Mat ycrcb; 
cvtColor(img, ycrcb, COLOR_BGR2YCrCb); 

2.将YCrCb镜像拆分成不同的通道矩阵:

// Split image into channels 
vector<Mat> channels; 
split(ycrcb, channels); 

3.使用只有两个参数(输入和输出矩阵)的equalizeHist函数,仅均衡 Y 通道中的直方图:

// Equalize the Y channel only 
equalizeHist(channels[0], channels[0]); 

4.合并生成的通道,并将其转换为BGR格式,向用户显示结果:

// Merge the result channels 
merge(channels, ycrcb); 

// Convert color ycrcb to BGR 
cvtColor(ycrcb, result, COLOR_YCrCb2BGR); 

// Show image 
imshow("Equalized", result);

应用于低对比度Lena图像的过程将产生以下结果:

光照相效果

在本节中,我们将创建另一个图像效果,这是一种在不同的移动应用中非常常见的照片效果,例如 Google Camera 或 Instagram。 我们将了解如何使用查找表(LUT)。 我们将在同一节后面介绍 LUT。 我们将学习如何添加一个覆盖图像,在本例中是一个暗晕,以创建我们想要的效果。 实现此效果的函数是lomoCallback回调,它具有以下代码:

void lomoCallback(int state, void* userData) 
{ 
    Mat result; 

    const double exponential_e = std::exp(1.0); 
    // Create Look-up table for color curve effect 
    Mat lut(1, 256, CV_8UC1); 
    for (int i=0; i<256; i++) 
    { 
        float x= (float)i/256.0;  
        lut.at<uchar>(i)= cvRound( 256 * (1/(1 + pow(exponential_e, -((x-0.5)/0.1)) )) ); 
    } 

    // Split the image channels and apply curve transform only to red channel 
    vector<Mat> bgr; 
    split(img, bgr); 
    LUT(bgr[2], lut, bgr[2]); 
    // merge result 
    merge(bgr, result); 

    // Create image for halo dark 
    Mat halo(img.rows, img.cols, CV_32FC3, Scalar(0.3,0.3,0.3) ); 
    // Create circle  
    circle(halo, Point(img.cols/2, img.rows/2), img.cols/3, Scalar(1,1,1), -1);  
    blur(halo, halo, Size(img.cols/3, img.cols/3)); 

    // Convert the result to float to allow multiply by 1 factor 
    Mat resultf; 
    result.convertTo(resultf, CV_32FC3); 

    // Multiply our result with halo 
    multiply(resultf, halo, resultf); 

    // convert to 8 bits 
    resultf.convertTo(result, CV_8UC3); 

    // show result 
    imshow("Lomography", result); 
} 

让我们来看看 Lomography 效果是如何工作的,以及如何实现它。 Lomography 效果分为不同的步骤,但在我们的示例中,我们用两个步骤制作了一个非常简单的 Lomography 效果:

  1. 通过使用查找表将曲线应用于红色通道来实现颜色操纵效果
  2. 通过在图像上应用深色光晕来实现复古效果

第一步是通过应用以下函数使用曲线变换来处理红色:

此公式生成一条使暗值更暗、亮值更亮的曲线,其中x是可能的像素值(0255),s是我们在示例中设置为0.1的常量。 较低的常量值生成的值低于128的像素非常暗,高于128的像素非常亮。 接近1的值将曲线转换为直线,并且不会产生我们想要的效果:

通过应用 LUT 可以非常容易地实现此功能。 LUT 是返回给定值的预处理值以在内存中执行计算的向量或表。 LUT 是一种常用技术,通过避免重复执行代价高昂的计算来节省 CPU 周期。 我们不是为每个像素调用exponential/divide函数,而是对每个可能的像素值只执行一次(256次),并将结果存储在表中。 因此,我们以牺牲一点内存为代价节省了 CPU 时间。 虽然这在图像尺寸较小的标准 PC 上可能不会有太大的不同,但对于 CPU 受限的硬件(如 Raspberry PI)来说,这就是一个巨大的差异。

例如,在我们的示例中,如果要对图像中的每个像素应用函数,则必须进行widthxhigh操作;例如,在 100x100 像素中,将有 10,000 次计算。 如果我们可以为所有可能的输入预先计算所有可能的结果,我们就可以创建 LUT 表。 在图像中,只有个可能的值作为像素值。 如果我们想要通过应用函数来更改颜色,我们可以预计算出 256 个值,并将它们保存在 LUT 向量中。 在我们的示例代码中,我们定义了E变量,并创建了一个由1行和256列组成的lut矩阵。 然后,我们对所有可能的像素值进行循环,方法是应用我们的公式并将其保存到一个lut变量中:

const double exponential_e = std::exp(1.0); 
// Create look-up table for color curve effect 
Mat lut(1, 256, CV_8UC1); 
Uchar* plut= lut.data; 
for (int i=0; i<256; i++) 
{ 
    double x= (double)i/256.0;  
    plut[i]= cvRound( 256.0 * (1.0/(1.0 + pow(exponential_e, -((x-0.5)/0.1)) )) ); 
} 

正如我们在本节前面提到的,我们不会将该函数应用于所有通道;因此,我们需要使用split函数按通道分割输入图像:

// Split the image channels and apply curve transform only to red channel 
vector<Mat> bgr; 
split(img, bgr); 

然后,我们将lut表变量应用于红色通道。 OpenCV 提供了LUT函数,它有三个参数:

  • 输入图像
  • 查找表矩阵
  • 输出图像

然后,我们对LUT函数和红色通道的调用如下所示:

LUT(bgr[2], lut, bgr[2]); 

现在,我们只需合并我们的计算通道:

// merge result 
merge(bgr, result);

第一步已经完成,我们只需要创建黑暗光环就可以完成我们的效果。 然后,我们创建一个内部有一个白色圆圈的灰色图像,具有相同的输入图像大小:

 // Create image for halo dark 
 Mat halo(img.rows, img.cols, CV_32FC3, Scalar(0.3,0.3,0.3)); 
 // Create circle  
 circle(halo, Point(img.cols/2, img.rows/2), img.cols/3, Scalar(1,1,1), -1);  

查看以下屏幕截图:

如果我们将此图像应用于我们的输入图像,我们将获得从深色到白色的强烈变化;因此,我们可以使用blur滤镜函数对我们的圆形光晕图像应用大模糊,以获得平滑的效果:

blur(halo, halo, Size(img.cols/3, img.cols/3)); 

图像将被更改,以提供以下结果:

现在,如果我们必须从第一步开始将这个光环应用于我们的图像,一个简单的方法是将两个图像相乘。 但是,我们必须将输入图像从 8 位图像转换为 32 位浮点数,因为我们需要将值在01范围内的模糊图像与具有整数值的输入图像相乘。 下面的代码将为我们做这件事:

// Convert the result to float to allow multiply by 1 factor 
Mat resultf; 
result.convertTo(resultf, CV_32FC3); 

在转换图像之后,我们只需要将每个元素的每个矩阵相乘:

// Multiply our result with halo 
multiply(resultf, halo, resultf); 

最后,我们将浮点图像矩阵结果转换为 8 位图像矩阵:

// convert to 8 bits 
resultf.convertTo(result, CV_8UC3); 

// show result 
imshow("Lomograpy", result); 

这将是结果:

卡通化效果

本章的最后一节致力于创建另一种效果,称为卡通化;此效果的目的是创建一个看起来像卡通的图像。 为此,我们将算法分为两个步骤:边缘检测颜色过滤

cartoonCallback函数定义此效果,其代码如下:

void cartoonCallback(int state, void* userData) 
{ 
    /** EDGES **/ 
    // Apply median filter to remove possible noise 
    Mat imgMedian; 
    medianBlur(img, imgMedian, 7); 

    // Detect edges with canny 
    Mat imgCanny; 
    Canny(imgMedian, imgCanny, 50, 150); 

    // Dilate the edges 
    Mat kernel= getStructuringElement(MORPH_RECT, Size(2,2)); 
    dilate(imgCanny, imgCanny, kernel); 

    // Scale edges values to 1 and invert values 
    imgCanny= imgCanny/255; 
    imgCanny= 1-imgCanny; 

    // Use float values to allow multiply between 0 and 1 
    Mat imgCannyf; 
    imgCanny.convertTo(imgCannyf, CV_32FC3); 

    // Blur the edgest to do smooth effect 
    blur(imgCannyf, imgCannyf, Size(5,5)); 

    /** COLOR **/ 
    // Apply bilateral filter to homogenizes color 
    Mat imgBF; 
    bilateralFilter(img, imgBF, 9, 150.0, 150.0); 

    // truncate colors 
    Mat result= imgBF/25; 
    result= result*25; 

    /** MERGES COLOR + EDGES **/ 
    // Create a 3 channles for edges 
    Mat imgCanny3c; 
    Mat cannyChannels[]={ imgCannyf, imgCannyf, imgCannyf}; 
    merge(cannyChannels, 3, imgCanny3c); 

    // Convert color result to float  
    Mat resultf; 
    result.convertTo(resultf, CV_32FC3); 

    // Multiply color and edges matrices 
    multiply(resultf, imgCanny3c, resultf); 

    // convert to 8 bits color 
    resultf.convertTo(result, CV_8UC3); 

    // Show image 
    imshow("Result", result); 

} 

第一步是检测图像最重要的边缘。 在检测边缘之前,我们需要从输入图像中去除噪声。 有几种方法可以做到这一点。 我们将使用中值滤波器来去除所有可能的小噪声,但我们也可以使用其他方法,例如高斯模糊。 OpenCV 函数是medianBlur,它接受三个参数:输入图像、输出图像和内核大小(内核是一个小矩阵,用于对图像应用一些数学运算,如卷积方法):

Mat imgMedian; 
medianBlur(img, imgMedian, 7); 

在去除任何可能的噪声之后,我们使用Canny滤波器检测强边缘:

// Detect edges with canny 
Mat imgCanny; 
Canny(imgMedian, imgCanny, 50, 150); 

Canny过滤器接受以下参数:

  • 输入图像
  • 输出图像
  • 第一阈值
  • 第二阈值
  • 索贝尔大小孔径
  • 布尔值,指示我们是否需要使用更精确的图像渐变幅度

第一阈值和第二阈值之间的最小值用于边缘链接。 最大值用于查找强边缘的初始分段。 Sobel 大小孔径是算法中将使用的 Sobel 滤波器的核大小。 在检测到边之后,我们将应用一个小的扩张来连接破碎的边:

// Dilate the edges 
Mat kernel= getStructuringElement(MORPH_RECT, Size(2,2)); 
dilate(imgCanny, imgCanny, kernel); 

与我们在 Lomography 效果中所做的类似,如果我们需要将边缘的结果图像与彩色图像相乘,则需要像素值在01范围内。 为此,我们将用精明的结果除以256,并将边缘反转为黑色:

// Scale edges values to 1 and invert values 
imgCanny= imgCanny/255; 
imgCanny= 1-imgCanny; 

我们还将把 Canny 8 无符号位像素格式转换为浮点矩阵:

// Use float values to allow multiply between 0 and 1 
Mat imgCannyf; 
imgCanny.convertTo(imgCannyf, CV_32FC3); 

要获得凉爽的结果,我们可以模糊边缘,要获得平滑的结果线,我们可以应用blur滤镜:

// Blur the edgest to do smooth effect 
blur(imgCannyf, imgCannyf, Size(5,5)); 

算法的第一步已经完成,现在我们要处理颜色。 要获得卡通外观,我们将使用bilateral滤镜:

// Apply bilateral filter to homogenizes color 
Mat imgBF; 
bilateralFilter(img, imgBF, 9, 150.0, 150.0); 

bilateral滤波器是一种在保持边缘的同时降低图像噪声的滤波器。 有了适当的参数,我们将在后面讨论,我们可以得到卡通效果。

bilateral过滤器的参数如下:

  • 输入图像
  • 输出图像
  • 像素邻域的直径;如果设置为负值,则根据 sigma 空间值计算

  • 西格玛颜色值

  • 西格玛坐标空间

With a diameter greater than five, the bilateral filter starts to become slow. With sigma values greater than 150, a cartoonish effect appears.

为了创建更强的卡通效果,我们将像素值相乘并除以,将可能的颜色值截断为 10:

// truncate colors 
Mat result= imgBF/25; 
result= result*25; 

最后,我们必须合并颜色和边缘结果。 然后,我们必须创建一个三通道图像,如下所示:

// Create a 3 channles for edges 
Mat imgCanny3c; 
Mat cannyChannels[]={ imgCannyf, imgCannyf, imgCannyf}; 
merge(cannyChannels, 3, imgCanny3c); 

我们可以将颜色结果图像转换为 32 位浮点图像,然后将每个元素的两个图像相乘:

// Convert color result to float  
Mat resultf; 
result.convertTo(resultf, CV_32FC3); 

// Multiply color and edges matrices 
multiply(resultf, imgCanny3c, resultf); 

最后,我们只需要将图像转换为 8 位,然后向用户显示结果图像:

// convert to 8 bits color 
resultf.convertTo(result, CV_8UC3); 

// Show image 
imshow("Result", result); 

在下一个截图中,我们可以看到输入图像(左图)和应用卡通化效果的结果(右图):

简略的 / 概括的 / 简易判罪的 / 简易的

在本章中,我们了解了如何创建一个完整的项目,通过应用不同的效果来处理图像。 我们还将彩色图像分割成多个矩阵,以便仅将效果应用于一个通道。 我们了解了如何创建查找表、如何将多个矩阵合并为一个、如何使用Cannybilateral过滤器、如何绘制圆以及如何将图像相乘以获得光晕效果。

在下一章中,我们将学习如何进行对象检测,以及如何将图像分割成不同的部分并对这些部分进行检测。



回到顶部