查看全课大纲(第 8 / 93 节)
  1. 1.计算机视觉概述
  2. 2.计算机视觉应用场景
  3. 3.困难与挑战
  4. 4.开源库介绍
  5. 5.图像显示与存储
  6. 6.CLAHE
  7. 7.腐蚀膨胀与开闭运算
  8. 8.滤波与边缘填充
  9. 9.各种滤波
  10. 10.图像金字塔
  11. 11.傅里叶变换
  12. 12.短时傅里叶变换与小波变换
  13. 13.颜色特征与颜色直方图
  14. 14.几何特征_边缘
  15. 15.几何特征-角点与斑点
  16. 16.局部特征-SIFT特征描述子
  17. 17.局部特征-SURF特征描述子
  18. 18.局部特征-ORB特征描述子
  19. 19.其他特征描述子
  20. 20.传统图像分割技术
  21. 21.GraphCut
  22. 22.GrabCut
  23. 23.人脸检测的传统方法
  24. 24.行人检测的传统方法HOG+SVM
  25. 25.行人检测DPM
  26. 26.人工神经网络的架构
  27. 27.目标函数与梯度下降
  28. 28.误差反向传播
  29. 29.可手算的神经网络
  30. 30.改进目标函数与克服梯度消散
  31. 31.神经网络之传统神经网络与深度网络的差异
  32. 32.各种梯度下降算法
  33. 33.如何选择合适的梯度下降算法
  34. 34.避免过适应
  35. 35.CNN的架构
  36. 36.CNN的误差反向传播_卷积层
  37. 37.CNN的误差反向传播_池化层
  38. 38.分类所用的数据集
  39. 39.Alexnet详解
  40. 40.VGG详解
  41. 41.GoogLeNet_v1
  42. 42.BatchNormalization
  43. 43.GoogLeNet_v3v4
  44. 44.ResNet
  45. 45.ResNeXt
  46. 46.CNN设计准则
  47. 47.图像检索与推土机距离
  48. 48.基于颜色相似性
  49. 49.基于形状相似性
  50. 50.基于纹理相似性
  51. 51.基于局部特征相似性
  52. 52.DL的二值哈希编码
  53. 53.高维空间中检索KDTree
  54. 54.高维空间中检索LSH
  55. 55.目标识别任务综述
  56. 56.RCNN
  57. 57.SPP-Net
  58. 58.Fast R-CNN
  59. 59.Faster R-CNN
  60. 60.R-CNN系列模型回顾
  61. 61.R-FCN的设计思路
  62. 62.R-FCN的实现
  63. 63.YOLOv1
  64. 64.YOLOv2_9000
  65. 65.YOLOv3
  66. 66.图像分割应用场景与数据集
  67. 67.上采样的实现方式
  68. 68.FCN网络架构的由来
  69. 69.FCN构建细节
  70. 70.DeepLabv1
  71. 71.CRF
  72. 72.DeepLabv2
  73. 73.DeepLabv3
  74. 74.特定的医学影像分割任务
  75. 75.U-Net
  76. 76.3D U-Net
  77. 77.V-Net
  78. 78.DenseNet
  79. 79.病理切片分析任务及数据集
  80. 80.病理切片分析
  81. 81.图说任务及其传统处理方法
  82. 82.深度语言模型与RNN
  83. 83.LSTM及其变种
  84. 84.NIC模型
  85. 85.SAT模型
  86. 86.图说的性能指标
  87. 87.判别模型与生成模型
  88. 88.KL散度与JS散度
  89. 89.VAE
  90. 90.GAN的基本原理
  91. 91.DCGAN与CGAN
  92. 92.Wasserstein GAN
  93. 93.SRGAN

滤波与边缘填充

小象实战讲义 · 基于深度学习的计算机视觉

在图像预处理中,我们常常需要对图像进行局部处理,例如平滑去噪、锐化或边缘检测。这些操作的核心是邻域运算,即一个像素点的输出值不仅取决于它自身,还取决于其周围像素的值。本节我们将深入讲解邻域运算的核心——滤波(卷积),以及处理图像边界时必须面对的边缘填充问题。掌握这些基础,是理解后续深度学习卷积神经网络的关键。

💡 核心导读

  • 滤波即卷积:理解在图像每个位置上,基于邻域像素和权重模板进行加权求和的运算本质。
  • 扫描窗与边界问题:卷积核如何逐像素滑动,以及当它移动到图像边界时,如何处理“越界”的像素。
  • 边缘填充策略:学习补零、边界复制、镜像和块复制四种填充方式,并了解为何“补零”最常用。
  • 均值滤波示例:通过一个具体的均值滤波例子,直观感受卷积运算过程及其对图像的影响。

从邻域运算到卷积

在前面的知识点中,我们介绍了点运算和形态学运算。现在,我们进入邻域运算。顾名思义,邻域运算是指,图像中某个像素点经过变换后的值,不仅由该点自身的原始值决定,还受到其周围(邻域)像素点值的影响。这种运算方式非常强大,是实现图像平滑、锐化、边缘检测等效果的基础。

在邻域运算中,最核心的操作就是卷积(Convolution)。有时我们也会听到“滤波”(Filtering)这个词。强调运算的具体操作时,我们多用“卷积”;而强调运算达到的效果(如平滑、边缘提取)时,我们则多用“滤波”。但从实际操作层面看,两者基本是等价的。

卷积的数学定义如下:

h[x,y]=∑k,lf[k,l]⋅I[x+k,y+l]h[x, y] = \sum_{k, l} f[k, l] \cdot I[x + k, y + l]

其中:

  • II 是原始输入图像。
  • ff 是卷积核(Kernel)、也叫滤波器、卷积模板或滤波模板。它是一个小矩阵,定义了邻域内每个位置的权重。
  • hh 是在图像位置 (x,y)(x, y) 处的卷积结果。
  • kk 和 ll 是卷积核内部的坐标索引,其变化范围覆盖了整个卷积核。

关键词解析:

  • 卷积核/滤波模板:一个带有权重的矩阵,是卷积运算的核心,决定了运算的效果(如平滑、边缘检测)。
  • 扫描窗:因为卷积核需要从图像的左上角开始,逐行逐列地滑动到每一个像素位置进行计算,这个过程就像用一个窗口在图像上扫描。

不同的卷积核可以实现截然不同的功能:

  • 平滑/去噪:如均值滤波、高斯滤波。
  • 梯度/锐化:如Sobel、Prewitt算子,用于边缘检测或增强细节。
  • 特征提取:识别边缘、角点、纹理等。
  • 模式检测:匹配特定的图案。

这里有一个非常重要的联系:深度学习中的卷积操作,其概念完全来源于这里所讲的图像处理中的卷积。只不过在深度学习中,卷积核里的权重值不是人工设计的,而是通过训练数据自动学习得到的。

卷积运算的直观理解

让我们通过一个具体的例子来直观理解卷积。

假设我们有一小块图像(蓝色部分)和一个3x3的卷积核(黄色部分),卷积核内每个权重都是1。

import numpy as np

# 定义一小块图像(3x3)
image_patch = np.array([[4, 1, 6],
                        [7, 2, 3],
                        [9, 5, 8]], dtype=np.float32)

# 定义一个3x3的全1卷积核(均值滤波核的未归一化形式)
kernel = np.ones((3, 3), dtype=np.float32)

# 手动计算卷积:对应位置相乘后求和
result = np.sum(image_patch * kernel)
print(f"卷积运算结果(对应位置乘加):{result}")
卷积运算结果(对应位置乘加):45.0

运算过程是:将卷积核中心对准图像的一个像素,让卷积核的每个权重与图像对应位置的像素值相乘,然后将所有乘积结果相加,得到该位置卷积后的输出值。上例中,4*1 + 1*1 + 6*1 + 7*1 + 2*1 + 3*1 + 9*1 + 5*1 + 8*1 = 45。

接着,卷积核会像扫描窗一样,向右移动一个像素(步长为1),重复上述计算,得到下一个输出值。如此循环,直至遍历整张图像。

# 使用OpenCV的filter2D函数演示扫描窗式的卷积(这里使用相关运算,对于全1核,相关与卷积结果相同)
import numpy as np
import cv2

kernel = np.ones((3, 3), dtype=np.float32)

# 为了演示,我们构造一个稍大的图像;filter2D 会在内部对越界位置补零
demo_image = np.array([[4, 1, 6, 10],
                       [7, 2, 3, 11],
                       [9, 5, 8, 12],
                       [13,14,15,16]], dtype=np.float32)

# cv2.filter2D默认执行的是相关(correlation),但对于对称核,相关等于卷积。
# anchor 使用默认值 (-1,-1)(即核中心),并用 BORDER_CONSTANT 以 0 填充越界位置,以模拟我们描述的过程。
conv_result = cv2.filter2D(demo_image, -1, kernel, borderType=cv2.BORDER_CONSTANT)
print("使用cv2.filter2D对整个图像进行‘卷积’(相关)的结果:")
print(conv_result)
使用cv2.filter2D对整个图像进行‘卷积’(相关)的结果:
[[14. 23. 33. 30.]
 [28. 45. 58. 50.]
 [50. 76. 86. 65.]
 [41. 64. 70. 51.]]

可以看到,输出图像左上角第一个值是 14。这是因为卷积核中心对准 (0,0) 时,核的左上部分已经越出图像边界,cv2.filter2D 按 BORDER_CONSTANT 把这些越界位置当作 0,于是参与计算的 3x3 区域是 [0,0,0; 0,4,1; 0,7,2],乘加后正好是 0+0+0+0+4+1+0+7+2=14。这也直观说明了:卷积核在图像上滑动,在每个位置进行乘加运算;当核移动到边界时,部分权重会落在图像外部,这就引出了下一个核心问题——边缘填充。

# 手动验证左上角(0,0)位置的输出
# 假设图像外围补了一圈0,那么参与计算的3x3区域是:
# [0,0,0; 0,4,1; 0,7,2] 与 全1核 相乘相加
manual_calc = 0+0+0 + 0+4+1 + 0+7+2
print(f"手动计算图像左上角经过补零后卷积的结果:{manual_calc}")
手动计算图像左上角经过补零后卷积的结果:14

手动计算结果(14)与filter2D的输出(14)完全一致,说明在默认锚点(核中心)与补零边界下,卷积核的扫描乘加过程正如我们所描述。当卷积核移动到边界时,部分权重会落在图像外部,这就引出了下一个核心问题——边缘填充。

边界填充(Padding)

观察卷积的扫描过程,你会发现一个问题:对于一个 N x N 的图像和一个 K x K 的卷积核,如果不做任何处理,输出图像会变小。因为卷积核中心需要对准每个像素,当中心对准图像最边缘的像素时,卷积核的一部分已经超出了图像范围。

为了得到与输入图像尺寸相同的输出,或者更一般地,为了控制输出尺寸,我们必须在图像的边缘外侧“虚构”出一些像素值,供越界的卷积核权重使用。这就是边界填充。

常见的填充策略有以下四种:

  1. 补零(Zero-padding):在图像外围填充数值0。这是最常用、最简单的方法。可以将其理解为假设图像外部是全黑的区域。

    原图 [1,2,3]  补零后 [0,0,0,0,0]
         [4,5,6]        [0,1,2,3,0]
         [7,8,9]        [0,4,5,6,0]
                        [0,7,8,9,0]
                        [0,0,0,0,0]
  2. 边界复制(Replication):用图像边缘像素的值来填充外部区域。

    原图 [1,2,3]  边界复制 [1,1,2,3,3]
         [4,5,6]          [1,1,2,3,3]
         [7,8,9]          [4,4,5,6,6]
                          [7,7,8,9,9]
                          [7,7,8,9,9]
  3. 镜像(Reflection):以图像边界为轴,镜像反射内部的像素值来填充。

    原图 [1,2,3]      镜像填充(BORDER_REFLECT_101,四周各 1 层)
         [4,5,6]
         [7,8,9]      [5,4,5,6,5]
                      [2,1,2,3,2]
                      [5,4,5,6,5]
                      [8,7,8,9,8]
                      [5,4,5,6,5]
  4. 块复制(Wraparound):假设图像在平面上是无限周期平铺的,用图像另一侧的像素来填充。类似于Windows桌面背景的“平铺”效果。

    原图 [1,2,3]  块复制 [9,7,8,9,7]
         [4,5,6]          [3,1,2,3,1]
         [7,8,9]          [6,4,5,6,4]
                          [9,7,8,9,7]
                          [3,1,2,3,1]

在实际应用中,尤其是深度学习中,补零(Zero-padding) 因其实现简单且不会引入图像外部先验信息而成为最主流的选择。虽然它可能在边界处引入不真实的黑色边缘,但对于网络训练而言,这是可接受的代价。

均值滤波:一个完整的例子

现在我们用一个具体的例子——均值滤波,来串联起卷积和边缘填充的概念。均值滤波使用一个所有元素值相等的卷积核,其作用是取邻域内像素的平均值,从而达到平滑图像的效果。

假设我们有一张6x6的原始图像,其像素值分布如下,具有非常清晰的边界:

原始图像 (6x6):
9   9   9   18  18  18
9   9   9   18  18  18
9   9   9   18  18  18
18  18  18  9   9   9
18  18  18  9   9   9
18  18  18  9   9   9

我们使用一个3x3的均值滤波核,其每个权重为 1/9。为了得到相同尺寸的输出,我们采用补零填充。因此,实际参与计算的图像是外围补了一圈零的8x8图像。

# 构造原始图像(6x6)
import numpy as np
import cv2

original_img = np.array([
    [9, 9, 9, 18, 18, 18],
    [9, 9, 9, 18, 18, 18],
    [9, 9, 9, 18, 18, 18],
    [18, 18, 18, 9, 9, 9],
    [18, 18, 18, 9, 9, 9],
    [18, 18, 18, 9, 9, 9]
], dtype=np.float32)

# 定义3x3均值滤波核 (每个元素=1/9)
mean_kernel = np.ones((3, 3), dtype=np.float32) / 9.0

# 应用滤波,使用补零填充(cv2.BORDER_CONSTANT默认填充0)
filtered_img = cv2.filter2D(original_img, -1, mean_kernel, borderType=cv2.BORDER_CONSTANT)

print("原始图像 (左上角 4x4 区域):")
print(original_img[:4, :4])
print("\n经过3x3均值滤波(补零填充)后的图像 (左上角 4x4 区域):")
# 设置打印选项,使输出更紧凑易读
np.set_printoptions(precision=2, suppress=True)
print(filtered_img[:4, :4])
原始图像 (左上角 4x4 区域):
[[ 9.  9.  9. 18.]
 [ 9.  9.  9. 18.]
 [ 9.  9.  9. 18.]
 [18. 18. 18.  9.]]

经过3x3均值滤波(补零填充)后的图像 (左上角 4x4 区域):
[[ 4.  6.  8. 10.]
 [ 6.  9. 12. 15.]
 [ 8. 12. 13. 14.]
 [10. 15. 14. 13.]]

我们来手动验证第一个值 4.0 是如何计算出来的。对于输出图像位置(0,0),卷积核覆盖的输入区域(已补零)为:

[0, 0, 0]
[0, 9, 9]
[0, 9, 9]

计算:(0+0+0 + 0+9+9 + 0+9+9) / 9 = 36 / 9 = 4.0。与输出一致。

观察滤波结果,原本清晰的边界(如从9到18的突变)变得模糊了,取而代之的是一个渐变的过渡区域(如从9渐变到12、15等)。这就是均值滤波的副作用:它在平滑噪声的同时,也会模糊图像的重要细节和边缘。

因此,在实战中,单纯的均值滤波并不常用。我们将在后续知识点中介绍更优秀的平滑滤波器(如高斯滤波)和边缘检测滤波器。

📝 动手练一练

  1. 概念辨析:假设对一个5x5的图像,使用一个3x3的卷积核进行卷积,步长为1,并且希望输出图像的尺寸仍然是5x5。请问需要在原始图像的四周填充多少层像素?如果使用补零填充,请画出填充后图像的示意图(只需画出轮廓,标明填充区域)。
👉 点击查看参考答案

答案: 需要填充1层像素。

对于一个 N x N 的输入,一个 K x K 的核,步长 S=1,希望输出尺寸不变,填充层数 P 满足公式: N + 2P - K + 1 = N => 2P - K + 1 = 0 => P = (K - 1) / 2。 对于 K=3,P = (3-1)/2 = 1。

补零填充后的图像为7x7,示意图如下(I代表原图像素,0代表填充的零):

0 0 0 0 0 0 0
0 I I I I I 0
0 I I I I I 0
0 I I I I I 0
0 I I I I I 0
0 I I I I I 0
0 0 0 0 0 0 0
  1. 手算卷积:给定一个3x3的图像块 [[1,2,3],[4,5,6],[7,8,9]] 和一个3x3的卷积核 [[0,-1,0],[-1,5,-1],[0,-1,0]]。请手动计算以图像块中心像素5为卷积中心时的卷积结果(无需填充,只计算中心点对应的输出)。
👉 点击查看参考答案

答案: 卷积计算过程(对应位置相乘后求和):

(1*0) + (2*-1) + (3*0) +
(4*-1) + (5*5) + (6*-1) +
(7*0) + (8*-1) + (9*0)
= 0 + (-2) + 0 + (-4) + 25 + (-6) + 0 + (-8) + 0
= 25 - 20
= 5

因此,卷积结果为 5。这个核是一个常见的锐化核,它增强了中心像素相对于邻域的值。

本章小结

本节我们深入探讨了图像预处理中至关重要的邻域运算——滤波(卷积)。我们理解了卷积的数学定义和直观的“扫描窗”操作过程,并解决了随之而来的边界问题,学习了边缘填充的四种策略。最后,通过一个均值滤波的实例,我们完整演练了卷积过程,并认识到简单均值滤波的局限性。

📋 行动清单

  • 在脑海中复现卷积核在图像上滑动的“扫描窗”过程。
  • 理解并能够解释为什么需要进行边缘填充,以及补零填充为何最常用。
  • 尝试手动计算一个3x3图像块与一个3x3卷积核的卷积结果,以巩固乘加运算的概念。
  • 思考:如果使用边界复制填充代替补零填充,本节中的均值滤波例子在图像边缘的结果会有什么不同?

—— 小象教研组