5.16. 自定义卷积核¶
到目前为止介绍的邻域滤波器,每一个在每个位置对窗口应用的都是一个内置的统计量——均值、高斯加权平均值、中值。morph() 是唯一一个允许应用程序以卷积核形式自行提供统计量的滤波器:卷积核是一个小型权重矩阵,描述了滤波器应如何将邻域像素组合成单个输出值。
其机制是经典的卷积运算。在每个输出位置,每个邻域像素都与卷积核中对应的权重相乘,将这些乘积相加,对结果进行可选的缩放和偏移,然后将该值写入输出像素。不同的卷积核会从相同的输入产生不同的结果。权重全部相等且为正的卷积核可重现 mean() 滤波器;钟形的卷积核则可重现 gaussian()。超出这些模式的其他模式会产生边缘响应、浮雕、梯度、锐化、运动模糊,以及经典图像处理领域中一大批其他效果——凡是经典图像处理曾想用单次线性遍历完成的事情,都可以做到。
5.16.1. morph 方法¶
其函数签名与其他邻域滤波器类似,只是多了一个参数:
img.morph(size, kernel, mul=1.0, add=0.0)
size 与其他地方一样表示半径,因此卷积核必须恰好为 (2 * size + 1) 行乘以 (2 * size + 1) 列。卷积核本身是一个由这么多数字组成的扁平 Python 列表,按行主序排列——前 (2 * size + 1) 个条目是第一行,接下来 (2 * size + 1) 个是第二行,依此类推,直到最后一行。mul 在乘积之和写入输出像素之前对其进行缩放,add 则加上一个常量。默认的 mul=1.0 和 add=0.0 会保持卷积输出不变。
有一个值得明确说明的细节:该方法在写入输出之前,会自动将乘积之和除以卷积核条目之和。这种自动除法意味着,一个条目之和为九的平均化卷积核——例如 3×3 的方框模糊——无需任何额外操作就会以九分之一的比例输出;一个条目之和为十六的高斯近似卷积核则会以十六分之一的比例输出,二者都无需应用程序自行计算除法。只有当应用程序希望在自动归一化的基础上再进行额外缩放时,才会设置 mul——或者更常见的是,当卷积核之和为零(边缘响应卷积核)时,自动除法将变成除以无的情况。框架在这种情况下会将和视为一,于是 mul 就成了唯一可用于将未缩放的乘积之和保持在范围内的调节旋钮。
来自自适应阈值章节的 threshold=True / offset=N 这一对参数同样适用于 morph(),因此同一套自定义卷积核框架可以生成一个截止值由自定义统计量计算得出的二值阈值。
5.16.2. 卷积核布局¶
一个 3×3 的卷积核(size=1)是由九个数字组成的扁平列表,按从左到右、从上到下的顺序排列。如果将列表分成三行 Python 代码来写,这种约定读起来就很自然:
sobel_x = [-1, 0, 1,
-2, 0, 2,
-1, 0, 1]
这是 Sobel-x 梯度算子——任何应用程序都会首先用到的标准卷积核,也是一个值得从头到尾走一遍的例子。其模式很简单:左列为负权重,右列为正权重,中列为零。行权重 -1, -2, -1(或右侧的 1, 2, 1)中间比四角更大,这使得中间行比四角的行对结果有更大的影响。
当卷积核扫过一条垂直边缘时——即一列从左侧暗到右侧亮的像素——负权重会拾取暗的一侧,正权重会拾取亮的一侧。乘积之和是一个很大的正数,滤波器会将其写为一个明亮的输出像素。一块均匀亮度的水平区域产生的结果为零,因为每个正权重都被一个作用于相同值像素、大小相同的负权重所抵消。
运行卷积核:
img.morph(1, sobel_x, mul=0.25)
Sobel 卷积核之和为零——左侧每个负权重都被右侧一个相等的正权重所抵消——因此自动除法不会除以任何东西,mul 就成了对乘积之和唯一的缩放。mul=0.25 可使响应保持在范围内:Sobel-x 从 3×3 区域所能产生的最大绝对和约为 4 * 255 = 1020(八个明亮像素,权重最高达 2),将其除以四后,极端情况会落在 255,格式可在此处干净地将其截断。
与之对应的 Sobel-y 卷积核通过将同样的权重模式旋转 90 度来检测水平边缘:
sobel_y = [-1, -2, -1,
0, 0, 0,
1, 2, 1]
想要检测任意方向边缘的应用程序,通常会同时运行两个 Sobel 算子并将其响应结合起来。
5.16.3. 对输出进行偏移¶
add 是缩放故事的另一半。零和卷积核的响应是有符号的——边缘的一侧为正,另一侧为负——而当写入无符号像素时,负的那一半会被截断为零。add=128 会将响应平移到以中灰为中心,于是负响应会以低于 128 的值得以保留,正响应则落在其之上:边缘响应或浮雕在两个方向上都变得可见,代价是每个方向各损失一半的范围。
卷积核期望使用 mul 和 add 的哪种组合,是卷积核设计的一部分;标准卷积核目录 列出了每种常见卷积核的正确设置。
5.16.4. 更大的卷积核¶
本页所有内容都是以 3×3 卷积核(size=1)来描述的,因为这是标准目录所使用的尺寸,也因为在该尺寸下行主序布局很容易手写出来。不过,该机制本身并没有将卷积核限制为 3×3。size=2 运行一个 5×5 的卷积核,扁平列表中有二十五个条目;size=3 运行一个 7×7 的卷积核,有四十九个条目;依此类推,直至应用程序愿意承担的任意半径。框架可在任意奇数尺寸下处理扁平列表或嵌套行布局。
采用更大卷积核的理由,与在任何内置滤波器上采用更大邻域的理由相同:更多的平均化、更广的特征检测、对单像素噪声更低的敏感度。其代价随半径的平方增长——一个 5×5 卷积核每像素的工作量大约是 3×3 的 2.8 倍,7×7 约为 5.4 倍——而这个倍数会直接体现在帧率上。
实用的做法是:对标准目录保持 size=1,仅当算法需要更大的邻域时才采用更大的尺寸。边缘检测器很少能从超过 3×3 的尺寸中获益;平滑滤波器有时可以;合适的尺寸取决于应用程序试图强调或抑制的特征的尺度。
5.16.5. 何时使用 morph¶
对于日常平滑,mean()、gaussian() 和 bilateral() 更快也更简洁。对于边缘检测,laplacian() 和 find_edges() 是专门设计的。直接使用 morph() 的理由在于:应用程序需要某种内置滤波器未提供的特定卷积——一个方向性的 Sobel、一个自定义边缘模板、一个针对管线后续步骤将要查找的特定纹理而调校的卷积核,或者经典图像处理几十年来积累的任何标准实用卷积核目录中的卷积核。任意卷积核的完全灵活性都可供使用;代价是应用程序需要负责选择能产生其所需结果的卷积核数值。