四、深入研究直方图和过滤器
在上一章中,我们学习了 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;
现在,我们只需定义每个回调函数,在下一节中,我们将这样做。
绘制直方图
直方图是变量分布的统计图形表示,它使我们能够理解数据的密度估计和概率分布。 直方图是通过将整个变量值范围划分为一个小范围的值,然后计算每个区间内有多少值来创建的。
如果我们把这个直方图概念应用到一幅图像上,看起来很难理解,但实际上,它很简单。 在灰度图像中,我们的变量值的范围是每个可能的灰度值(从0
到255
),密度是具有该值的图像的像素数。 这意味着我们必须计算图像中值为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);
}
让我们了解如何提取每个通道直方图以及如何绘制它。 首先,我们需要创建三个矩阵来处理每个输入图像通道。 我们使用一个矢量型变量来存储每个变量,并使用split
OpenCV 函数在这三个通道之间划分输入图像:
// 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;
我们可以使用calcHist
OpenCV 函数计算直方图。 此函数有几个参数,顺序如下:
- 输入图像:在我们的示例中,我们使用存储在
bgr
向量中的一个图像通道 - 输入中用于计算直方图的图像数量:在我们的示例中,我们只使用
1
图像 - 用于计算直方图的数字通道维度:我们在本例中使用[T0
- 可选的掩码矩阵。
- 用于存储计算出的直方图的变量。
- 直方图维度:这是图像(这里是灰色平面)取值的空间维度,在我们的示例中为
1
- 要计算的条柱数量:在我们的示例中为
256
个条柱,每个像素值一个 - 输入变量的范围:在我们的例子中,可能的像素值从
0
到255
每个通道的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 );
现在我们已经计算了每个通道直方图,我们必须绘制每个通道直方图并将其显示给用户。 为此,我们将创建一个大小为512
x300
像素的彩色图像:
// 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-1
到i
绘制;垂直位置是对应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);
}
要均衡彩色图像,我们只需均衡亮度通道。 我们可以对每个颜色通道执行此操作,但结果不可用。 或者,我们可以使用分离单个通道中亮度分量的任何其他彩色图像格式,例如HSV或YCrCb。 因此,我们选择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 效果:
- 通过使用查找表将曲线应用于红色通道来实现颜色操纵效果
- 通过在图像上应用深色光晕来实现复古效果
第一步是通过应用以下函数使用曲线变换来处理红色:
此公式生成一条使暗值更暗、亮值更亮的曲线,其中x是可能的像素值(0
到255
),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 位浮点数,因为我们需要将值在0
到1
范围内的模糊图像与具有整数值的输入图像相乘。 下面的代码将为我们做这件事:
// 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 效果中所做的类似,如果我们需要将边缘的结果图像与彩色图像相乘,则需要像素值在0
和1
范围内。 为此,我们将用精明的结果除以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);
在下一个截图中,我们可以看到输入图像(左图)和应用卡通化效果的结果(右图):
简略的 / 概括的 / 简易判罪的 / 简易的
在本章中,我们了解了如何创建一个完整的项目,通过应用不同的效果来处理图像。 我们还将彩色图像分割成多个矩阵,以便仅将效果应用于一个通道。 我们了解了如何创建查找表、如何将多个矩阵合并为一个、如何使用Canny
和bilateral
过滤器、如何绘制圆以及如何将图像相乘以获得光晕效果。
在下一章中,我们将学习如何进行对象检测,以及如何将图像分割成不同的部分并对这些部分进行检测。