5.17. 标准卷积核大全

经典图像处理积累了一份篇幅可观的卷积核权重模式大全,它们反复出现——边缘检测器、锐化器、浮雕、平滑器、运动模糊——而且每一种都通过 morph() 来运行。每一种都很简短,每一种都只做一件事,并且一旦理解了权重的基本逻辑,大多数都很容易读懂。

下面的卷积核除非另有说明,否则都是 3×3 的,因此它们在调用时都使用 size=1。每个卷积核的权重结构都在其旁边作了描述,因为读懂权重正是建立直觉的关键——理解为什么某个卷积核会产生浮雕效果,而另一个会产生锐化效果。

5.17.1. 恒等卷积核

最简单的卷积核是 恒等 核——中心为 1,其余处处为 0:

identity = [0, 0, 0,
            0, 1, 0,
            0, 0, 0]

img.morph(1, identity)

每个输出像素从其邻域的中心取值,也就是同一位置上的输入像素。图像原样通过,毫无变化。恒等核作为滤波器没有任何实际用途,但它是理解其他所有卷积核的有用基准:任何非恒等核都是恒等核加上某种修改。

如果某个卷积核的中心权重很大,而周围是较小的负权重,那么它会从中心 减去 周围的值。如果某个卷积核的中心权重为 0,它就会忽略像素本身,只对其邻域之间的差异作出响应。用这种方式去读卷积核——中心权重对像素做了什么,周围的权重又增加或减去了什么——是预测其效果最快的方法。

5.17.2. 边缘检测

边缘检测 卷积核会在亮度沿某个特定方向快速变化的位置产生强烈响应,而在亮度均匀的地方产生接近 0 的输出。它们属于权重之和为 0 的那一类:一块平坦区域(每个像素都是相同的值)会产生 0 输出,因为每个正权重都恰好被一个等量级的负权重所抵消。

Sobel-x 是经典示例。它检测 垂直 边缘(左右亮度过渡):

sobel_x = [-1,  0,  1,
           -2,  0,  2,
           -1,  0,  1]

img.morph(1, sobel_x, mul=0.25, add=128)

与之对应的 Sobel-y 是同一模式旋转 90 度后的结果;它检测水平边缘(上下亮度过渡):

sobel_y = [-1, -2, -1,
            0,  0,  0,
            1,  2,  1]

Sobel-x 的中间一行权重为 -22,而不是 -11。中心行上额外的权重使卷积核在 沿着 边缘的方向上自带少量平滑作用,这使得它比更简单的、舍弃了这些额外量级的 Prewitt 算子更能抵抗噪声:

prewitt_x = [-1, 0, 1,
             -1, 0, 1,
             -1, 0, 1]

prewitt_y = [-1, -1, -1,
              0,  0,  0,
              1,  1,  1]

Prewitt 对每一行的权重都相同,因此它的响应比 Sobel 略微更锐利,代价是对单像素噪声更敏感(运行卷积核的开销是相同的——无论权重如何,卷积所做的工作都一样)。在边缘强烈的干净图像上,它是 Sobel 完全可用的替代品。

Scharr 则走向另一个方向。它的权重更大,并经过调校以便在更细的角度上准确检测边缘方向:

scharr_x = [-3,   0,  3,
            -10,  0, 10,
            -3,   0,  3]

img.morph(1, scharr_x, mul=0.0625, add=128)

除数 mul=0.0625(即 1/16)在较大的乘积之和之后,将输出拉回到 0 -- 255 区间内。当应用需要几何上最忠实的梯度响应,并愿意为此付出略多一点的算术运算代价时,Scharr 就是正确的选择。

5.17.3. 拉普拉斯算子

拉普拉斯(Laplacian) 卷积核会同时对 任意 方向上的边缘作出响应。Sobel 各自检测沿某一个轴的亮度变化,而拉普拉斯算子对称的权重模式无论边缘朝哪个方向都以同样的方式作出响应:

laplacian_4 = [ 0, -1,  0,
               -1,  4, -1,
                0, -1,  0]

img.morph(1, laplacian_4, add=128)

其结构为:中心权重 4,四个水平/垂直邻居权重为 -1,四个对角线邻居权重为 0。该卷积核之和为 0,因此平坦区域产生 0 输出。在亮度变化的地方,中心值与其四个正交邻居的平均值不同,输出就是这个差值的大小。

8 连通的变体则包含对角线邻居:

laplacian_8 = [-1, -1, -1,
               -1,  8, -1,
               -1, -1, -1]

每个卷积核检测的东西略有不同。4 连通版本在水平和垂直边缘上产生更干净的输出;8 连通版本则更各向同性——它在每个方向上的响应都同样好——但产生的输出略带噪声。8 连通卷积核还以 轮廓(outline) 之名流传,缘于它被用于边缘的可视化。

5.17.4. 锐化

锐化 卷积核就是恒等核加上一个边缘响应卷积核。输出是原始图像加上一份边缘的副本,因此高频特征相对于平滑的内部区域得到放大。

标准的 4 连通锐化卷积核是把 4 连通拉普拉斯算子加到恒等核上:

sharpen = [ 0, -1,  0,
           -1,  5, -1,
            0, -1,  0]

img.morph(1, sharpen)

读这个卷积核:中心权重是 identity (1) + Laplacian centre (4) = 5,周围权重与拉普拉斯算子一致。平坦区域产生 5 * 1 - 4 * 1 = 1 倍的中心值——即恒等。边缘则产生原值加上拉普拉斯响应。权重之和为 1,因此 muladd 保持各自的默认值。

要获得更强的锐化效果,8 连通的变体走得更远:

sharpen_strong = [-1, -1, -1,
                  -1,  9, -1,
                  -1, -1, -1]

img.morph(1, sharpen_strong)

中心权重 9identity (1) + Laplacian-8 centre (8)。逻辑相同,放大更多,同时也更可能放大传感器噪声。

强锐化卷积核本质上就是带 unsharp=Truegaussian(),只不过是直接以卷积核的形式表达,而不是通过非锐化掩模标志。像素级的行为是相同的;选择在于:是要命名方法的便利,还是要手工调校卷积核所带来的精细控制。

5.17.5. 浮雕

浮雕(emboss) 卷积核会产生经典图像编辑器中那种侧光照射的效果。输出看起来就像图像被挤压成浮雕,然后再从一个角落打光:

emboss = [-2, -1,  0,
          -1,  1,  1,
           0,  1,  2]

img.morph(1, emboss, add=128)

其诀窍在于 沿对角线的非对称性。左上角具有最负的权重,右下角具有最正的权重,而从一角到另一角的对角线则从负值经过 1 过渡到正值。在每个像素处,该卷积核本质上计算的是“我右下方的亮度减去我左上方的亮度”,在图像沿该方向变亮处为正,在变暗处为负。加上 128 把带符号的输出重新居中到中灰,从而让效果可见。

把这种非对称性旋转到另一条对角线上,就会从相反方向产生浮雕效果:

emboss_alt = [ 0,  1,  2,
              -1,  1,  1,
              -2, -1,  0]

img.morph(1, emboss_alt, add=128)

当应用需要检测方向时,这两个浮雕方向组合使用很有用——把其中一个从另一个中减去,或者在同一幅图像上分别运行并比较其响应。

5.17.6. 平滑

平滑卷积核属于权重之和为 1(且全部非负)的那一类。一块平坦区域经过这样的卷积核会产生同样平坦的亮度,因为该卷积核是把像素值平均到一起,而不是放大它们之间的差异。

最简单的是 盒式模糊(box blur),它正是 mean() 所计算的内容:

box_blur = [1, 1, 1,
            1, 1, 1,
            1, 1, 1]

img.morph(1, box_blur)

该卷积核之和为 9,因此按卷积核之和的自动除法会把乘积之和转化为对邻域九个像素的真正平均。实践中 mean() 是运行这个卷积核更好的方式——它通过一条专为计算均值(且只做这件事)而优化的路径,更快地产生相同的输出,而 morph 运行的是通用卷积机制。盒式模糊之所以列入大全,是因为它是理解其他所有平滑卷积核的正确基准。

高斯(Gaussian) 权重的 3×3 近似对中心和正交邻居的加权高于角落:

gaussian = [1, 2, 1,
            2, 4, 2,
            1, 2, 1]

img.morph(1, gaussian)

这些权重是帕斯卡三角行 1, 2, 1 与自身作外积的结果。中心权重 4 最大,因为中心像素对其自身输出贡献最多;角落为 1,因为它们离中心最远。该卷积核之和为 16,按卷积核之和的自动除法处理归一化——无需 mul 参数。3×3 形式是真正高斯的粗略近似,在 size=1 时与 gaussian() 难以区分;morph 形式主要在应用希望把平滑与另一种操作组合在同一遍处理中时才有用。

5.17.7. 运动模糊

运动模糊(motion-blur) 卷积核沿 一个方向 对像素求平均,而垂直方向不模糊。最简单的情形是水平方向:

motion_h = [0, 0, 0,
            1, 1, 1,
            0, 0, 0]

img.morph(1, motion_h)

中间一行沿水平轴对三个像素求平均;顶行和底行为 0。该卷积核之和为 3,因此按卷积核之和的自动除法产生真正的三像素平均,无需任何 mul。输出是输入在水平方向上被抹涂的副本——也就是当被摄主体在曝光期间横向移动时摄像头所捕捉到的效果。垂直运动模糊是同一模式旋转后的结果:

motion_v = [0, 1, 0,
            0, 1, 0,
            0, 1, 0]

对角线运动模糊使用主对角线:

motion_diag = [1, 0, 0,
               0, 1, 0,
               0, 0, 1]

img.morph(1, motion_diag)

运动模糊卷积核既可作为 效果(出于视觉目的有意模糊一帧)使用,也可作为 测试图案 来检验那些需要抗运动伪影的算法(在运动模糊的输入上运行算法,检查它是否仍能产生正确的结果)。

5.17.8. 一眼读懂卷积核

几条经验法则可以让新的卷积核更容易一眼读懂:

  • 和为 1 且权重非负 ⇒ 平滑(保持平均亮度)。

  • 和为 0 且同时含正负权重 ⇒ 边缘响应(在平坦区域上为 0)。

  • 和为 1 且中心为大的正值、周围为小的负值 ⇒ 锐化(恒等加上边缘响应)。

  • 沿某条对角线非对称 且和为 1 ⇒ 浮雕(突出每处亮度过渡的一侧)。

  • 沿某一个轴集中 且和为 1 ⇒ 方向性模糊。

卷积核所匹配的上述法则中的第一条,通常就是对它作用的正确猜测。大多数有用的卷积核仅凭其权重模式的布局就能辨认出来。

没有任何 标准卷积核能满足应用需求时,下一步就是手工调校一个。上述法则与 mul / add 控制项的组合,几乎涵盖了经典机器视觉流水线所曾需要的每一种线性处理遍;从那里开始,就是尝试权重、查看输出、不断迭代的事了。