笔者目前在校本科大二,有志于进行计算机视觉、计算机图形学方向的研究,准备系统性地、扎实的学习一遍OpenCV的内容,故记录学习笔记,同时,由于笔者同时学习数据结构、机器学习等知识,会尽量根据自己的理解,指出OpenCV的应用,并在加上自己理解的前提下进行叙述。
若有不当之处,希望各位批评、指正。
1.图像滤波介绍
2.线性滤波:方框滤波、均值滤波、高斯滤波
3.写一个自己的滤波函数
注1:可以说,前6篇学习笔记都是一些基础知识的学习,从这一篇开始,我们真正开始了图像处理的学习。这实在是令人激动的一件事情。
注2:如果不想看一些繁琐的基础概念介绍,可以直接从目录中的1.3邻域算子:线性邻域滤波开始看。
摘自《OpenCV3编程入门》:
图像滤波,指在尽量保留图像细节特征条件下对目标图像的噪声进行抑制,是图像预处理中不可缺少的操作,其处理效果的好坏将直接影响到后续图像处理和分析的有效性和可靠性。
消除图像中的噪声成分叫做图像的平滑化或滤波操作。信号或图像的能量大部分集中在幅度谱的低频和中频段,而在较高频段,有用的信息经常被噪声淹没。因此一个能降低高频成分幅度的滤波器就能够减弱噪声的影响。
图像滤波的目的有两个:抽出对象的特征作为图象识别的特征模式;为适应图像处理的要求,消除图像数字化时所混入的噪声。
对滤波处理的要求有两条:不能损坏图像的轮廓及边缘等重要信息;使图像清晰视觉效果好。
摘自《OpenCV3编程入门》:
线性滤波器: 经常用于剔除输入信号中不想要的频率或者从许多频率中选择一个想要的频率。
常见的线性滤波器有:
低通滤波器:允许低频率通过
高通滤波器:允许高频率通过
带通滤波器:允许一定范围频率通过
带阻滤波器:阻止一定范围频率通过并且允许其他频率通过
全通滤波器:允许所有频率通过,仅改变相位关系
陷波滤波器:阻止一个狭窄频率范围通过,是一种特殊带阻滤波器
关于滤波和模糊: xx滤波就是指用xx函数作为滤波函数来进行滤波;xx模糊就是xx低通滤波;xx锐化就是xx高通滤波。
摘自《OpenCV3编程入门》:
邻域算子是利用给定像素周围的像素值来决定此像素的最终输出的一种算子。
线性邻域滤波就是一种常用的邻域算子,像素的输出值取决于输出像素的加权和。
如图,中间的h(x,y)就是一个线性邻域滤波的“核”。
这个核和原图f(x,y)中的每个元素作卷积操作,得到最终的图像g(x,y)。
这让人很容易联想到:h(x,y)中的值是否必须是固定的?我们能不能找到一个最好的h(x,y)的参数,让h和f卷积出最好的g?
答案是:可以。我们可以训练h(x,y),让其生成的图片趋于良好。我认为,这就是机器学习中CNN的由来。
当然,现在的CNN已经发展了很多。例如,在Super-Resolution中作为Encoder和Decoder等等,可以做很多复杂的任务。
这三种滤波方式都属于线性滤波。OpenCV已经将它们封装好。
· 方框滤波:boxFilter()
· 均值滤波:blur()
· 高斯滤波:GaussianBlur()
下面对其进行一一介绍。
boxFilter()
void cv::boxFilter (
InputArray src,//输入图像
OutputArray dst,//输出图像
int ddepth, //输出图像深度。-1代表原图深度
Size ksize, //核的大小
Point anchor = Point(-1,-1),//锚点,Point(-1,-1)代表锚点始终在核的中心
bool normalize = true,//是否归一化
int borderType = BORDER_DEFAULT //边界模式。一般不用设置
)
进一步解释:
锚点:也就是被平滑的那个点。
归一化:影响到核的参数。具体如下:
blur()
void cv::blur (
InputArray src,
OutputArray dst,
Size ksize,
Point anchor = Point(-1,-1),
int borderType = BORDER_DEFAULT
)
可以发现,这个函数和boxFilter()很像。实际上,从源码来看,blur()就是
normalize = true的boxFilter()。
GaussianBlur()
void cv::GaussianBlur (
InputArray src,
OutputArray dst,
Size ksize,
double sigmaX,
double sigmaY = 0,
int borderType = BORDER_DEFAULT
)
高斯模糊用正态分布的方式来计算输出的像素值。
sigmaX和sigmaY即为二维正态分布的两个sigma值。
相关数学基础这里便不介绍。
位置(opencv4):opencv\sources\modules\imgproc\src\box_filter.dispatch.cpp
在这个文件中可以看到boxFilter()的具体实现。
在文件中,可以看到,boxFilter()函数在进行了一系列判断后,最终的滤波操作是通过构建一个createBoxFilter()函数实现的。而这个函数返回一个 FilterEngine类的指针。关于滤波的事情大部分都封装在了这个FliterEngine里。
void boxFilter(InputArray _src, OutputArray _dst, int ddepth,
Size ksize, Point anchor,
bool normalize, int borderType)
{
CV_INSTRUMENT_REGION();
CV_Assert(!_src.empty());
CV_OCL_RUN(_dst.isUMat() &&
(borderType == BORDER_REPLICATE || borderType == BORDER_CONSTANT ||
borderType == BORDER_REFLECT || borderType == BORDER_REFLECT_101),
ocl_boxFilter3x3_8UC1(_src, _dst, ddepth, ksize, anchor, borderType, normalize))
CV_OCL_RUN(_dst.isUMat(), ocl_boxFilter(_src, _dst, ddepth, ksize, anchor, borderType, normalize))
Mat src = _src.getMat();
int stype = src.type(), sdepth = CV_MAT_DEPTH(stype), cn = CV_MAT_CN(stype);
if( ddepth < 0 )
ddepth = sdepth;
_dst.create( src.size(), CV_MAKETYPE(ddepth, cn) );
Mat dst = _dst.getMat();
if( borderType != BORDER_CONSTANT && normalize && (borderType & BORDER_ISOLATED) != 0 )
{
if( src.rows == 1 )
ksize.height = 1;
if( src.cols == 1 )
ksize.width = 1;
}
Point ofs;
Size wsz(src.cols, src.rows);
if(!(borderType&BORDER_ISOLATED))
src.locateROI( wsz, ofs );
CALL_HAL(boxFilter, cv_hal_boxFilter, src.ptr(), src.step, dst.ptr(), dst.step, src.cols, src.rows, sdepth, ddepth, cn,
ofs.x, ofs.y, wsz.width - src.cols - ofs.x, wsz.height - src.rows - ofs.y, ksize.width, ksize.height,
anchor.x, anchor.y, normalize, borderType&~BORDER_ISOLATED);
CV_OVX_RUN(true,
openvx_boxfilter(src, dst, ddepth, ksize, anchor, normalize, borderType))
//CV_IPP_RUN_FAST(ipp_boxfilter(src, dst, ksize, anchor, normalize, borderType));
borderType = (borderType&~BORDER_ISOLATED);
Ptr<FilterEngine> f = createBoxFilter( src.type(), dst.type(),
ksize, anchor, normalize, borderType );
f->apply( src, dst, wsz, ofs );
}
值得一提,OpenCV的滤波函数效率是非常高的。在下文中我会将它的效率和我自己写的滤波函数效率进行一个对比。
另外,smooth.dispatch.cpp文件中有GaussianBlur()的实现。这个函数的实现略有不同,是通过构建GaussianKernels来实现的,构建GaussianKernels是一个单独的函数,内部又通过一些其他的函数来实现…但是在文件中仍然出现了FilterEngine的相关信息。
今天我不打算阅读如何构建像OpenCV那样的滤波函数。所以,今天我们自己写一个滤波函数。
其实滤波并不困难。比如我来自己实现一个均值滤波,在简化的情况下,固定kernel_size = (5,5) ,那么,我只需要将每个像素周围的5*5个像素的值加起来再乘以0.04,得到的值就是滤波后的像素值。
下面直接贴代码:
void myblur(InputArray _src, OutputArray _dst) {
//均值滤波,滤波器Size(5,5)
//准备Mat类型的src,和Mat类型的dst
Mat src = _src.getMat();
_dst.create(src.size(), src.type());
Mat dst = _dst.getMat();
//开始滤波操作
for (int i = 0; i < src.rows; i++) {
for (int j = 0; j < src.cols; j++) {
for (int k = 0; k < 3; k++) {
if (i > 4 && j >4 && i < src.rows-4 && j < src.cols-4) {
double v = 0;
for (int x = -2; x <= 2; x++) {
for (int y = -2; y <= 2; y++) {
v += src.at<Vec3b>(i + x, j + y)[k] * 0.04;
}
}
dst.at<Vec3b>(i, j)[k] = v;
}
else {
//考虑边缘像素
dst.at<Vec3b>(i, j)[k] = src.at<Vec3b>(i, j)[k];
}
}
}
}
}
最后,展示一下各滤波函数的效率、效果。
Mat img = imread("E:/program/image/1.jpg");
Mat average_dst, box_dst, gauss_dst, my_dst;
double time0 = (double)getTickCount();
boxFilter(img, box_dst, -1, Size(5, 5));
time0 = ((double)getTickCount() - time0) / getTickFrequency();
cout << time0 << endl;
time0 = (double)getTickCount();
blur(img, average_dst, Size(5, 5));
time0 = ((double)getTickCount() - time0) / getTickFrequency();
cout << time0 << endl;
time0 = (double)getTickCount();
GaussianBlur(img, gauss_dst, Size(5, 5),0,0);
time0 = ((double)getTickCount() - time0) / getTickFrequency();
cout << time0 << endl;
time0 = (double)getTickCount();
myblur(img, my_dst);
time0 = ((double)getTickCount() - time0) / getTickFrequency();
cout << time0 << endl;
imshow("src", img);
imshow("box", box_dst);
imshow("average", average_dst);
imshow("gauss", gauss_dst);
imshow("myblur", my_dst);
waitKey();