一、什么是图像滤波
我们在尽量保留图像原有信息的情况下,过滤掉图像内部的噪声,这一过程称为 图像的平滑处理,所得的图像称为 平滑图像。图像平滑处理,是指在一幅图片中,如果一个像素点与周围像素点的像素点差异较大,则将其值调整为周围临近像素点的近似值(例如均值等)。图像平滑处理通常伴随图像模糊操作,因此图像平滑处理有时也被称为 图像模糊处理。
如果针对图像内的每一个像素点都进行上述平滑处理,就能够对整幅图片完成平滑处理,有效地去除图像内的噪声信息。图像平滑处理的基本原理是,将噪声所在像素点的像素质处理为其周围临近像素点的近似值。
在 OpenCV 中建立平滑图像的函数有分为了 线性滤波器 和 非线性滤波器 两类。滤波器,通常是由一幅图像根据像素点 (x, y) 邻近的区域计算得到另一幅新图像的算法。因此,滤波器 是右邻域即预定义的操作构成阿。滤波器规定了滤波时所采用的形状以及该预取内像素值的组成规律。
滤波器也被称为 “核”、“模板”、“窗口”、“算子”、“掩膜(掩模,掩码)”等。一般信号领域系那个其称为 “滤波器”,数学领域将其称为 “核”。
我们可以在终端中使用 pip 安装 OpenCV 模块。默认是从国外的主站上下载,因此,我们可能会遇到网络不好的情况导致下载失败。我们可以在 pip 指令后通过 -i 指定国内镜像源下载。
pip install opencv-python -i https://mirrors.aliyun.com/pypi/simple
国内常用的 pip 下载源列表:
- 阿里云 https://mirrors.aliyun.com/pypi/simple
- 中国科技大学 https://pypi.mirrors.ustc.edu.cn/simple
- 清华大学 https://pypi.tuna.tsinghua.edu.cn/simple
- 中国科学技术大学 http://pypi.mirrors.ustc.edu.cn/simple
二、2D卷积
我们以某个像素为中心,这个像素与周围的像素可以组成 M 行 N 列的矩阵。这个 M 行 N 列的矩阵矩阵,我们就可以称为 卷积核。一般情况下,M 和 N 是相等的。M 和 N 的值越大,参与运算的像素点数量就越多,当前像素点的计算结果就受到周围越多像素点的影响。因此,卷积核越大,去噪效果越好,同时也可能让图像失真越严重。在实际处理过程中,要在失真和去噪效果取得平衡,选择合适大小的卷积核。
在 OpenCV 中,我们可以使用 cv2.filter2D() 函数 自定义卷积核实现卷积操作。
cv2.filter2D(src: cv2.typing.MatLike, ddepth: int, kernel: cv2.typing.MatLike, dst: cv2.typing.MatLike | None = ..., anchor: cv2.typing.Point = ..., delta: float = ..., borderType: int = ...) -> cv2.typing.MatLike: ...
其中,参数 src 是 原始图像。参数 ddepth 是 处理结果图像的图像深度,一般采用 -1 表示 与原始图像使用相同的图像深度。参数 kernel 是 卷积核,是一个单通道的数组。如果想在处理彩色图像时,让每个通道使用不同的核,则必须将彩色图像分解后使用不同的和完成操作。参数 dst 表示 进行均值滤波后得到的处理结果。参数 anchor 是 锚点,其默认值是 (-1, -1),表示 当前计算均值点位于核的中心点位置。参数 delta 是 修正值。如果存在会,会在基础滤波的结果上加上改制作为最终的滤波处理结果。参数 borderType 是 边界样式,该值决定了以何种方式处理边界。
OpenCV 中提供了多种边界处理方式,我们可以根据实际需要选用不同的边界处理模式。常用的边界处理方式如下:
cv2.BORDER_CONSTANT # 常量填充
cv2.BORDER_REPLICATE # 边缘复制
cv2.BORDER_REFLECT # 镜像
cv2.BORDER_WRAP # 简单复制
cv2.BORDER_REFLECT_101 # 改进镜像
- 常量填充:该方式在扩充边界区域时,使用一个特定值来完成填充。对应到图像上,相当于给图像加了某一特定颜色的边界。
- 边缘复制:该方式在填充扩充边界区域时,使用边界值完成填充。
- 上方值:使用原始图像内第 1 行的数值在上方扩充边界区域重复复制。
- 下方值:使用原始图像内最后一行的数值在下方填充边界区域重复复制。
- 左侧值:使用完成上下扩充边界填充的原始图像内最左边一列的值在左侧填充扩充边界区域复制。
- 右侧值:使用完成上下扩充边界填充的原始图像内最右边一列的值在右侧填充扩充边界区域复制。
- 镜像:该方式在填充扩充边界区域时,使用镜像方式完成填充。对应到图像上,相当于在其边界上进行了原始图像的镜像。
- 简单复制:该方式在填充扩充边界区域时,使用简单复制方式完成填充。对应到图像上,相当于在其边界简单地复制原始图像。
- 上方扩充边界:从原始图像的最下端开始依次选取一行复制,然后依次在上端的扩充边界内的下端开始粘贴,以此类推直至完成填充。
- 下方扩充边界:从原始图像的最上端开始依次选取一行复制,然后依次在下端的扩充边界内的顶端开始粘贴,依次类推直至完成填充。
- 左侧扩充边界:从完成上下填充的原始图像的最右端开始依次选取一列复制,然后依次从左端的扩充边界内的最右端开始粘贴,依次类推直至完成填充。
- 右侧扩充边界:从完成上下填充的原始图像的最左端开始依次选取一列复制,然后依次从右端的扩充边界内的最左侧开始粘贴,依次类推直至完成填充。
- 改进镜像:该方式与镜像类似,区别在于使用该方式时,原始图像的边界不再参与复制。
import sys
import cv2
import numpy as npif __name__ == '__main__':src = cv2.imread("assets/images/1.jpg")if src is None:print("加载图片失败")sys.exit(0)# kernel 卷积核,必须是 np.float32 类型kernel = np.array([[-1, -1, -1], [-1, 8, -1], [-1, -1, -1]], dtype=np.float32)dst = cv2.filter2D(src, -1, kernel)cv2.imshow("dst", dst)cv2.waitKey(0)cv2.destroyAllWindows()sys.exit(0)

填充扩充边界,主要是为了让原始图像中处于边界附近的像素值能够更好地得到处理。因此,不同的扩充边界方式,会对处于边界附近像素点的处理产生不同的影响。
三、均值滤波
均值滤波 是指用当前像素点周围 N x N 个像素值的均值来代替当前像素点。使用该方法便利处理图像内的每一个像素点,即可完成整幅图像的均值滤波。
在进行均值滤波时,首先要考虑需要对周围多少个像素点取平均值。通常情况下,我们会以当前像素点为中心,对行数和列数相等的一块区域内的所有像素点的像素值求平均的。
针对边缘像素点,我们可以只取图像内存在的周围邻域点的像素值均值。例如对于左上角(第 1 行第 1 列)的像素点,我们取第 1 ~ 3 列与第 1 ~ 3 行交汇所包含的 3 x 3 邻域内的像素点的像素值均值。除此之外,我们还可以扩充边界(扩展当前图图像的周围像素点)。完成图像边缘扩展后,可以在新增的行列内填充不同的像素值。在此基础上在计算像素点的像素值均值。
在 OpenCV 中,实现均值滤波的函数是 cv2.blur(),它的定义如下:
cv2.blur(src: cv2.typing.MatLike, ksize: cv2.typing.Size, dst: cv2.typing.MatLike | None = ..., anchor: cv2.typing.Point = ..., borderType: int = ...) -> cv2.typing.MatLike: ...
其中,参数 src 是 原始图像。参数 ksize 是 卷积核的大小。卷积核的大小是指在均值处理过程中,其邻域图像的 高度 和 宽度。参数 dst 表示 进行均值滤波后得到的处理结果。参数 anchor 是 锚点,其默认值是 (-1, -1),表示 当前计算均值点位于核的中心点位置。参数 borderType 是 边界样式,该值决定了以何种方式处理边界。
import sys
import cv2
import numpy as npif __name__ == '__main__':src = cv2.imread("assets/images/5.png")if src is None:print("加载图片失败")sys.exit(0)dst = cv2.blur(src, (5, 5)) # 均值滤波cv2.imshow("image", np.hstack((src, dst))) # 连接两个图像显示cv2.waitKey(0)cv2.destroyAllWindows()sys.exit(0)

四、方框滤波
OpenCV 还提供了方框滤波方式,与均值滤波的不同在于,方框滤波不仅能计算周围像素均值,还可能计算周围像素值和。在均值滤波中,滤波结果的像素值是任意一个点的邻域平均值,等于各邻域像素值之和除以邻域面积。而在方框滤波中,可以自由选择是否对均值滤波的结果进行 归一化,即可以自由选择滤波结果是邻域像素值之和的平均值还是领域像素值之和。
在 OpenCV 中,我们可以使用 cv2.boxFilter() 函数 实现方框滤波,它的定义如下:
cv2.boxFilter(src: cv2.typing.MatLike, ddepth: int, ksize: cv2.typing.Size, dst: cv2.typing.MatLike | None = ..., anchor: cv2.typing.Point = ..., normalize: bool = ..., borderType: int = ...) -> cv2.typing.MatLike: ...
其中,参数 src 是 原始图像。参数 ddepth 是 处理结果图像的图像深度,一般采用 -1 表示 与原始图像使用相同的图像深度。参数 ksize 是 卷积核的大小。卷积核的大小是指在均值处理过程中,其邻域图像的 高度 和 宽度。参数 dst 表示 进行均值滤波后得到的处理结果。参数 anchor 是 锚点,其默认值是 (-1, -1),表示 当前计算均值点位于核的中心点位置。参数 normalize 表示 在滤波时是否归一化处理,默认为 True,进行归一化处理。参数 borderType 是 边界样式,该值决定了以何种方式处理边界。
当 normalize = False 时,不进行归一化处理,即采用领域像素值之和。如果没有对图像的深度进行调整,滤波得到的结果很可能超过当前图像像素值范围的最大值,从而被截断为最大值。这样,就会得到一幅纯白色的图像。所以,当 normalize = 0 时,通常需要将参数 ddepth 设定为一个有效的范围值。
import sys
import cv2
import numpy as npif __name__ == '__main__':src = cv2.imread("assets/images/5.png")if src is None:print("加载图片失败")sys.exit(0)dst = cv2.boxFilter(src, -1, (5, 5)) # 方框滤波cv2.imshow("image", np.hstack((src, dst))) # 连接两个图像显示cv2.waitKey(0)cv2.destroyAllWindows()sys.exit(0)

五、高斯滤波
在进行均值滤波和方框滤波时,其邻域内每个像素的权重是相等的。在高斯滤波中,会将中心点的权重加大,远离中心点的权重值减少,在此基础上计算邻域各个像素值不同权重的和。在高斯滤波中,卷积核的值不再都是 1。
假定中心点的坐标是 (0, 0),那么取距离它最近的 8 个点坐标。为了方便计算,需要设置 \(\sigma\) 的值,假定 \(\sigma\)= 1.5,则模糊半径为 1 的高斯模糊计算如下:
在 OpenCV 中,我们可以使用 cv2.GaussianBlur() 函数 实现高斯滤波。
cv2.GaussianBlur(src: cv2.typing.MatLike, ksize: cv2.typing.Size, sigmaX: float, dst: cv2.typing.MatLike | None = ..., sigmaY: float = ..., borderType: int = ..., hint: AlgorithmHint = ...) -> cv2.typing.MatLike: ...
其中,参数 src 是 原始图像。参数 ksize 是 卷积核的大小,值必须是 奇数。卷积核的大小是指在均值处理过程中,其邻域图像的 高度 和 宽度。参数 sigmaX 是 卷积核在水平方向上的标准差,其控制的是 权重比例。参数 dst 表示 进行均值滤波后得到的处理结果。参数 sigmaY 是 卷积核在垂直方向上的标准差。参数 borderType 是 边界样式,该值决定了以何种方式处理边界。
如果 sigmaY = 0,则只采用 sigmaX 的值。如果 sigmaX 和 sigmaY 都是 0,则通过 ksize.width 和 ksize.height 计算得到。其中:
一般来说,当核大小固定时:
simga值越大,权值分布越平缓。邻域内的值对输出值的影响越大,图像越模糊。此时,周围值大小变化不到。在某些极端的情况下,邻域权重都是 1。sigma值越小,权值分布越突出。邻域内的值对输出值的影响越小,图像变化越小。此时,周围值变化较大。极端情况下:中心点权值是 1,周围点都是 0。
import sys
import cv2
import numpy as npif __name__ == '__main__':src = cv2.imread("assets/images/3.png")if src is None:print("加载图片失败")sys.exit(0)dst = cv2.GaussianBlur(src, (5, 5), 3) # 高斯滤波cv2.imshow("image", np.hstack((src, dst))) # 连接两个图像显示cv2.waitKey(0)cv2.destroyAllWindows()sys.exit(0)

六、中值滤波
中值滤波不再采用加权求均值的方式计算滤波结果。它用邻域内所有像素值的中间值来替代当前像素点的像素值。中值滤波会取当前像素点及其周围临近像素点(通常取奇数个像素点)的像素值,将这些像素值排序,然后将位于中间位置的像素值作为当前像素点的像素值。
在 OpenCV 中,我们可以使用 cv2.medianBlur() 函数实现中值滤波。
cv2.medianBlur(src: cv2.typing.MatLike, ksize: int, dst: cv2.typing.MatLike | None = ...) -> cv2.typing.MatLike: ...
其中,参数 src 是 原始图像。参数 ksize 是 卷积核的大小,值必须是 奇数。卷积核的大小是指在均值处理过程中,其邻域图像的 高度 和 宽度。参数 dst 表示 进行均值滤波后得到的处理结果。
import sys
import cv2
import numpy as npif __name__ == '__main__':src = cv2.imread("assets/images/5.png")if src is None:print("加载图片失败")sys.exit(0)dst = cv2.medianBlur(src, 5) # 中值滤波cv2.imshow("image", np.hstack((src, dst))) # 连接两个图像显示cv2.waitKey(0)cv2.destroyAllWindows()sys.exit(0)

七、双边滤波
双边滤波是综合考虑空间信息和色彩信息的滤波方式,在滤波过程中能够有效地保护图像内的边缘信息。在均值滤波、方框滤波、高斯滤波中,都会计算边缘各个像素点的加权平均值,从而模糊边缘信息。双边滤波在计算某一个像素点的新值时,不仅考虑距离信息(距离越远,权重越小),还考虑色彩信息(色彩差别越大,权重越小)。双边滤波综合考虑距离和色彩的权重结果,即能够有效地去除噪声,又能够较好地保护边缘信息。
在双边滤波中,当处于边缘时,与当前点色彩相近的 像素点(颜色值距离很近)会被给予较大的权重值,而与当前色彩差别较大的像素点(颜色值距离很远)会被给予较小的权重值(极端情况下权重可能为 0,直接忽略该点),这样就保护了边缘信息。
在 OpenCV 中,我们可以使用 cv2.bilateralFilter() 函数实现双边滤波。
cv2.bilateralFilter(src: cv2.typing.MatLike, d: int, sigmaColor: float, sigmaSpace: float, dst: cv2.typing.MatLike | None = ..., borderType: int = ...) -> cv2.typing.MatLike: ...
其中,参数 src 是 原始图像。参数 d 是 滤波时选取的空间距离参数,这里表示以当前像素点为中心点的直径。如果该值为非正数,会自动从参数 sigmaSpace 计算得到。如果滤波空间较大(d > 5),则速度较慢。参数 sigmaColor 是 滤波处理时选取的颜色差值范围,与当前像素点的像素值小于 sigmaColor 的像素点,能够参与到当前的滤波中。当值为 0 时,滤波失去意义。当值为 255 时,指定直径内的所有点都能够参与运算。参数 sigmaSpace 是 坐标空间中滤波器的 sigma 值(高斯函数的标准差)。它的值越大,说明有越多的点能够参与到滤波计算中来。参数 dst 表示 进行均值滤波后得到的处理结果。参数 borderType 是 边界样式,该值决定了以何种方式处理边界。
当 d > 0 时,有 d 指定邻域大小,sigmaSpace 的值不起作用。我们也可以将 d 的值设置为 -1。此时,d 与 sigmaSpace 的值成正比,函数根据 sigmaSpace 的值自动计算 d 值。
两个 sigma(sigmaColor 和 sigmaSpace)值越大,就有越多的元素参与运算。如果它们的值比较小,则滤波的效果将不太明显如果它们的值比较大,则滤波效果比较明显,会产生卡通效果。
import sys
import cv2
import numpy as npif __name__ == '__main__':src = cv2.imread("assets/images/4.png")if src is None:print("加载图片失败")sys.exit(0)dst = cv2.bilateralFilter(src, 10, 50, 50) # 双边滤波cv2.imshow("image", np.hstack((src, dst))) # 连接两个图像显示cv2.waitKey(0)cv2.destroyAllWindows()sys.exit(0)
