图像是获取信息的重要来源,但图像存在着噪声(过多的干扰信息),清除噪声有利于后续图像信息获取及特征提取。图像处理中,去噪的过程即模糊的过程。
图像模糊也称图像平滑处理。
在数字图像中,图像表示为像素 的二维数组,像素是组成图像的最基本单元,由亮度intensity(灰色图像仅有单个像素亮度)或色彩color(3个像素亮度值)表示。在表示图像的像素数组中,邻近像素点像素值的不同导致图像的不平滑,图像平滑便是指图像邻近像素点像素值趋近。趋近后图像的某些区域会丢失详细信息,表现出图像变模糊。图像模糊去除了图像中的高频信息,达到消除图像噪声、边缘的目标。
图像模糊是图像像素取周围像素平均值。
图像模糊是通过低通滤波卷积核与图像做卷积实现的,背后的实质是图像的卷积运算。卷积运算后图像边缘会变得模糊。所谓卷积是对图像像素的操作,使图像中的每一个像素点均为源图像与卷积核的乘积。如图所示:
卷积指两个函数通过乘法运算得到第三个函数的过程,运算时两个函数必需具有相同的维数。其物理意义是:一个函数(单位响应)在另一个函数(输入图像~~~~)上的加权叠加。
OpenCV提供了四种主要类型的模糊方式: Averaging, Gaussian Blurring,Median Blurring,Bilateral Filtering.
均值模糊也称均值滤波,邻域平均,是图像模糊处理中最简单的一种,处理时将原图中的一个像素值和它周围临近的N个像素值与卷积核做乘法运算,然后求得平均值,获得新图中该点的像素值。
opencv提供 cv.blur() 和 cv.boxFilter()两种函数用于均值模糊计算。
c:
void cv::blur(InputArray src, #输入图像
OutputArray dst, #与输入图像同大小,同类型的输出图像
Size ksize, #卷积核大小
Point anchor = Point(-1,-1), #卷积核锚点位置
int borderType = BORDER_DEFAULT #边框类型
)
Python:
cv.blur(src, ksize[, dst[, anchor[, borderType]]]) -> dst
blur函数使用的卷积核公式:
K = 1 ksize.width * ksize.height [ 1 1 1 ⋯ 1 1 1 1 1 ⋯ 1 1 ⋮ ⋮ ⋮ ⋱ ⋮ ⋮ 1 1 1 ⋯ 1 1 ] \texttt{K} = \frac{1}{\texttt{ksize.width * ksize.height}} \begin{bmatrix} 1 & 1 & 1 & \cdots & 1 & 1 \\ 1 & 1 & 1 & \cdots & 1 & 1 \\ \vdots & \vdots & \vdots & \ddots & \vdots & \vdots \\ 1 & 1 & 1 & \cdots & 1 & 1 \\ \end{bmatrix} K=ksize.width * ksize.height1⎣ ⎡11⋮111⋮111⋮1⋯⋯⋱⋯11⋮111⋮1⎦ ⎤
C:
void cv::boxFilter(InputArray src, #输入图像
OutputArray dst, #与输入图像同大小,同类型的输出图像
int ddepth, #输出图像深度,默认值为-1 此时同输入图像
Size ksize, #卷积核大小
Point anchor = Point(-1,-1), #锚点 默认(-1,-1)指卷积核中心位置
bool normalize = true, #卷积核是否归一化
int borderType = BORDER_DEFAULT #卷积时边界处理
)
Python:
cv.boxFilter(src, ddepth, ksize[, dst[, anchor[, normalize[, borderType]]]]) -> dst
boxFilter函数使用的公式为:
K = α [ 1 1 1 ⋯ 1 1 1 1 1 ⋯ 1 1 ⋮ ⋮ ⋮ ⋱ ⋮ ⋮ 1 1 1 ⋯ 1 1 ] \texttt{K} = \alpha \begin{bmatrix} 1 & 1 & 1 & \cdots & 1 & 1 \\ 1 & 1 & 1 & \cdots & 1 & 1 \\ \vdots & \vdots & \vdots & \ddots & \vdots & \vdots \\ 1 & 1 & 1 & \cdots & 1 & 1 \end{bmatrix} K=α⎣ ⎡11⋮111⋮111⋮1⋯⋯⋱⋯11⋮111⋮1⎦ ⎤
其中:
α = { 1 ksize.width * ksize.height when normalize=true 1 otherwise \alpha = \begin{cases} \frac{1}{\texttt{ksize.width * ksize.height}} & \texttt{when } \texttt{normalize=true} \\1 & \texttt{otherwise}\end{cases} α={ksize.width * ksize.height11when normalize=trueotherwise
从公式可见,boxFilter函数提供了比blur更丰富的形式,在卷积核做归一化时两函数是一致的。
实现代码: C++
void ImageBlur::image_blur(const Mat &img) {
logger_info("======image_blur===========");
// 均值模糊
averaging_blur(img);
averaging_boxFilter(img);
averaging_boxFilter_unnormalize(img);
}
void ImageBlur::averaging_blur(const Mat &img) {
logger_info("======averaging_blur===========");
Mat dst;
// 卷积和大小
Size kernel_size = Size(5, 5);
// 锚点 默认卷积核中心点
Point anchor = Point(-1, -1);
// 均值模糊
blur(img, dst, kernel_size, anchor);
imshow("averaging_blur", dst);
}
void ImageBlur::averaging_boxFilter(const Mat &img) {
logger_info("======averaging_boxFilter===depth=%d========", img.depth());
Mat dst;
// 卷积和大小
Size kernel_size = Size(5, 5);
// 锚点 默认卷积核中心点
Point anchor = Point(-1, -1);
// 均值模糊
boxFilter(img, dst, img.depth(), kernel_size, anchor);
imshow("averaging_boxFilter", dst);
}
/**
* 卷积核不做归一化
* @param img
*/
void ImageBlur::averaging_boxFilter_unnormalize(const Mat &img) {
logger_info("======averaging_boxFilter_unnormalize===depth=%d========", img.depth());
Mat dst;
// 卷积和大小
Size kernel_size = Size(13, 13);
// 锚点 默认卷积核中心点
Point anchor = Point(-1, -1);
// 均值模糊
boxFilter(img, dst, img.depth(), kernel_size, anchor, false);
imshow("averaging_boxFilter_unnormalize", dst);
}
实现代码: python
def image_blur(origin_image):
# 均值模糊
average_blur_kernel_5 = averaging_blur_kernel_5(origin_image)
average_blur_kernel_11 = averaging_blur_kernel_11(origin_image)
average_boxfilter_kernel_11 = averaging_boxfilter_kernel_11(origin_image)
average_boxfilter_kernel_11_unnormalize = averaging_boxfilter_kernel_11_unnormalize(origin_image)
titles = ['Original Image',
"averaging_blur_kernel_5",
"averaging_blur_kernel_11",
"average_boxfilter_kernel_11",
"average_boxfilter_kernel_11_unnormalize"]
images = [cv.cvtColor(origin_image, cv.COLOR_BGR2RGB),
cv.cvtColor(average_blur_kernel_5, cv.COLOR_BGR2RGB),
cv.cvtColor(average_blur_kernel_11, cv.COLOR_BGR2RGB),
cv.cvtColor(average_boxfilter_kernel_11, cv.COLOR_BGR2RGB),
cv.cvtColor(average_boxfilter_kernel_11_unnormalize, cv.COLOR_BGR2RGB)]
for i in range(5):
plt.subplot(2, 3, i + 1)
plt.imshow(images[i], 'gray', vmin=0, vmax=255)
plt.title(titles[i])
plt.xticks([]), plt.yticks([])
plt.show()
key = cv.waitKey(0)
while key != ord('q'):
key = cv.waitKey(0)
cv.destroyAllWindows()
def averaging_blur_kernel_5(origin_img):
logger.log.info("averaging_blur_kernel_5")
# 卷积核大小
kernel_size = (5, 5)
# 锚点
anchor = (-1, -1)
dst = cv.blur(origin_img, kernel_size, None, anchor)
cv.imshow("averaging_blur_kernel_5", dst)
return dst
def averaging_blur_kernel_11(origin_img):
logger.log.info("averaging_blur_kernel_11")
# 卷积核大小
kernel_size = (11, 11)
# 锚点
anchor = (-1, -1)
dst = cv.blur(origin_img, kernel_size, None, anchor)
cv.imshow("averaging_blur_kernel_5", dst)
return dst
def averaging_boxfilter_kernel_11(origin_img):
logger.log.info("averaging_boxfilter_kernel_11")
# 卷积核大小
kernel_size = (11, 11)
# 锚点
anchor = (-1, -1)
dst = cv.boxFilter(origin_img, -1, kernel_size, None, anchor)
cv.imshow("averaging_boxfilter_kernel_11", dst)
return dst
def averaging_boxfilter_kernel_11_unnormalize(origin_img):
logger.log.info("averaging_boxfilter_kernel_11_unnormalize")
# 卷积核大小
kernel_size = (11, 11)
# 锚点
anchor = (-1, -1)
dst = cv.boxFilter(origin_img, -1, kernel_size, None, anchor, 0)
cv.imshow("averaging_boxfilter_kernel_11_unnormalize", dst)
return dst
处理效果如下图所示:
均值模糊中确定某个像素点的值取周围像素点的像素均值,未考虑周围像素点远近的影响。理论上讲,离得越近影响因子越大,反之亦然。为解决均值模糊未考虑周围距离的远近带来的影响引入高斯模糊。
高斯模糊特征指对图像应用高斯函数后使图像变得平滑以消除图像噪声的现象。与均值模糊不同的是高斯模糊采用了一种非均匀低通滤波器。更高地保留了图像的边缘效果,降低图像噪声,忽略图像中的细节,高斯模糊通常将图像与高斯卷积核做卷积实现。
高斯卷积核使用正态分布来确立卷积核的系数,其高斯卷积核系数公式如下所示:
G 2 D ( x , y , σ ) = 1 2 π σ 2 e − x 2 + y 2 2 σ 2 G_{2D}(x,y,\sigma) = \frac{1}{2\pi\sigma^2} e^{ - \frac{x^2 + y^2}{2\sigma^2}} G2D(x,y,σ)=2πσ21e−2σ2x2+y2
其中,x, y是卷积核元素的位置, σ \sigma σ是卷积核元素分布的标准差。
opencv提供了 cv.getGaussianKernel()函数来获取高斯卷积核系数矩阵,矩阵大小为 ksize × 1 \texttt{ksize} \times 1 ksize×1,其公式如下:
G i = α ∗ e − ( i − ( ksize − 1 ) / 2 ) 2 / ( 2 σ 2 ) G_i= \alpha *e^{-(i-( \texttt{ksize} -1)/2)^2/(2 \sigma^2)} Gi=α∗e−(i−(ksize−1)/2)2/(2σ2)
其中 ksize − 1 \texttt{ksize}-1 ksize−1与 α \alpha α为系数,使得 ∑ i G i = 1 \sum_i G_i = 1 ∑iGi=1, 即卷积核系数和为1。
Mat cv::getGaussianKernel(int ksize, # 卷积核大小,为正奇数
double sigma, #高斯分布标准差,若为负数,则置为:sigma = 0.3*((ksize-1)*0.5 - 1) + 0.8
int ktype = CV_64F #高斯卷积类型 CV_32F CV_64F .
)
Python:
cv.getGaussianKernel(ksize, sigma[, ktype]) -> retval
对图像做高斯模糊时opencv提供了 cv.GaussianBlur()。其详细说明如下:
void cv::GaussianBlur(InputArray src, #源图像
OutputArray dst, #目标图像 与源图像同大小同类型
Size ksize, #卷积核大小ksize.width ksize.height均需是正的奇数, 若为0则通过sigma计算得到
double sigmaX, #X轴高斯标准差
double sigmaY = 0, #Y轴高斯标准差
int borderType = BORDER_DEFAULT # 卷积时边界处理方式
)
Python:
cv.GaussianBlur(src, ksize, sigmaX[, dst[, sigmaY[, borderType]]]) -> dst
实现代码: C++
/**
* 高斯模糊 kernel
* @param img
*/
void ImageBlur::gaussian_blur_kernel(const Mat &img) {
logger_info("======gaussian_blur===========");
Mat dst;
for (int i = 1; i < 31; i = i + 2) {
//卷积核越大 模糊越明显
GaussianBlur(img, dst, Size(i, i), 0, 0);
Mat kernel = getGaussianKernel(i, 0);
imshow("gaussian_blur_kernel" + i, dst);
}
}
实现代码: python
# 高斯模糊 X轴标准差0
def gaussian_blur_sigma_0(origin_image):
logger.log.info("gaussian_blur")
titles = ["origin_image"]
images = [cv.cvtColor(origin_image, cv.COLOR_BGR2RGB)]
for i in range(1, 31, 2):
dst = cv.GaussianBlur(origin_image, (i, i), 0)
titles.append("gaussian_blur_" + str(i))
images.append(cv.cvtColor(dst, cv.COLOR_BGR2RGB))
for i in range(16):
plt.subplot(4, 4, i + 1)
plt.imshow(images[i], 'gray', vmin=0, vmax=255)
plt.title(titles[i])
plt.xticks([]), plt.yticks([])
plt.show()
return dst
# 高斯模糊 X轴标准差5
def gaussian_blur_sigma_5(origin_image):
logger.log.info("gaussian_blur")
titles = ["origin_image"]
images = [cv.cvtColor(origin_image, cv.COLOR_BGR2RGB)]
for i in range(1, 31, 2):
dst = cv.GaussianBlur(origin_image, (i, i), 5)
titles.append("gaussian_blur_" + str(i))
images.append(cv.cvtColor(dst, cv.COLOR_BGR2RGB))
for i in range(16):
plt.subplot(4, 4, i + 1)
plt.imshow(images[i], 'gray', vmin=0, vmax=255)
plt.title(titles[i])
plt.xticks([]), plt.yticks([])
plt.show()
return dst
不同卷积核下,X轴标准差为5时的效果:
中值模糊与均值类似,不同的是中值模糊图像中卷积核锚点对应位置的像素值使用卷积核覆盖区域下所有元素中值替换,中值模糊对处理图像中的椒盐噪声非常有效。椒盐噪声指一种随机出现的黑点(胡椒)或者白点(盐),前者是高灰度噪声,后者是低灰度噪声,一般两者同时出现在图像中。在均值模糊和高斯模糊中卷积核锚点对应图像位置的像素值是重新计算的,中值模糊中该处的值为卷积核覆盖图像区域下的某个点的值。
OpenCV提供了中值模糊函数cv.medianBlur()
void cv::medianBlur(InputArray src, #输入图像
OutputArray dst, #输出图像,与输入图像同类型,同大小
int ksize) #卷积核大小,非负的奇数
Python:
cv.medianBlur(src, ksize[, dst] )->dst
实现代码: C++
/**
* 中值模糊
* @param img
*/
void ImageBlur::median_blur(const Mat &img) {
logger_info("======median_blur===========");
Mat dst;
medianBlur(img, dst, 15);
imshow("median_blur", dst);
}
实现代码: Python
# 中值模糊
def median_blur(origin_image):
logger.log.info("median_blur")
titles = ["origin_image"]
images = [cv.cvtColor(origin_image, cv.COLOR_BGR2RGB)]
for i in range(1, 31, 2):
dst = cv.medianBlur(origin_image, i)
titles.append("median_blur_" + str(i))
images.append(cv.cvtColor(dst, cv.COLOR_BGR2RGB))
for i in range(16):
plt.subplot(4, 4, i + 1)
plt.imshow(images[i], 'gray', vmin=0, vmax=255)
plt.title(titles[i])
plt.xticks([]), plt.yticks([])
plt.show()
return dst
双边滤波能在保留图像边缘的一种模糊方式,避免边缘信息丢失,保持图像轮廓的完整。与其它模糊相比,双边滤波比较耗时。
高斯卷积核计算像素时,仅考虑与卷积核叠加的像素邻域,并依据此计算出高斯加权均值,未考虑周围像素具有相同亮度的情况,也未考虑像素处于图像边缘的场景。
图像边缘的像素亮度变化明显,双边滤波与高斯模糊相比考虑像素邻域间的差异,只对与中心像素亮度相近的进行模糊处理,因而保留边缘。
OpenCV提供cv.bilateralFilter()函数做双边滤波。
void cv::bilateralFilter(InputArray src,#8bit 单通道/三通道图像
OutputArray dst,#与输入图像同类型,同大小的图像
int d, #像素邻域直径,
double sigmaColor,#色彩空间的标准差 值越大表示模糊时涉及邻域内越远的色彩
double sigmaSpace,#坐标空间的标准差 值越大表示邻域中涉及越远的母亲节不像像素
int borderType = BORDER_DEFAULT #卷积时边界处理方式
)
Python:
cv.bilateralFilter(src, d, sigmaColor, sigmaSpace[, dst[, borderType]]) -> dst
实现代码: C++
/**
* 双边模糊
* @param img
*/
void ImageBlur::bilateral_filter(const Mat &img) {
logger_info("======bilateral_filter===========");
Mat dst;
bilateralFilter(img, dst, 20, 40, 10);
imshow("bilateral_filter", dst);
}
实现代码: Python
# 双边模糊
def bilateral_blur(origin_image):
logger.log.info("bilateral_blur")
titles = ["origin_image"]
images = [cv.cvtColor(origin_image, cv.COLOR_BGR2RGB)]
for i in range(1, 31, 2):
dst = cv.bilateralFilter(origin_image, i, i * 2, i / 2)
titles.append("bilateral_" + str(i))
images.append(cv.cvtColor(dst, cv.COLOR_BGR2RGB))
for i in range(16):
plt.subplot(4, 4, i + 1)
plt.imshow(images[i], 'gray', vmin=0, vmax=255)
plt.title(titles[i])
plt.xticks([]), plt.yticks([])
plt.show()
return dst
可以明显看出,与高斯模糊相比,图像的边缘得到很好地保留。
http://szeliski.org/Book/
https://en.wikipedia.org/wiki/Convolution
http://www.songho.ca/dsp/convolution/convolution.html#definition
http://www.cs.csi.cuny.edu/~gu/teaching/courses/csc76010/slides/Parallel_Longlong.pdf
https://homepages.inf.ed.ac.uk/rbf/CVonline/LOCAL_COPIES/MANDUCHI1/Bilateral_Filtering.html