API

照例,我们搬一下官网的 API:

C++

void cv::filter2D(InputArray   src,
                  OutputArray  dst,
                  int          ddepth,
                  InputArray   kernel,
                  Point        anchor=Point(-1, -1),
                  double       delta=0,
                  int          borderType=BORDER_DEFAULT
                  )

Python

dst=cv.filter2D(src, ddepth, kernel[, dst[, anchor[, delta[, borderType]]]])

函数详解

这个函数一般是用于图像的卷积,但 OpenCV 文档里说这个函数不完全等于图像卷积。这一点说实话我看到的时候也震惊到了,我一直是拿它当卷积来用的。但是仔细考虑后,我认为这一点完全是定义上的差异,日常用的图像卷积定义和这个函数的功能实际上是一致的。

HelloWorld

直接上手做往往能给人最直观的感受。因此,最开始这里我要放一个使用这个函数的最小化程序,可以称之为该函数的 hello world 程序。这个程序跑通了,就可以很方便地尝试其他参数的作用了。

# HelloWrold Program of cv.filter2D
# by Aling on 2021/1/18
import numpy as np
import cv2 as cv


def main():
    img = cv.imread("你想读的图片")
    
    # 定义卷积核
    kernel = np.ones((10, 10)) / 100
    
    # 执行滤波
    avg_filtered = cv.filter2D(img, -1, kernel)

    # 显示图片
    cv.imshow("Average filtered", avg_filtered)
    cv.waitKey(0)
    cv.destroyAllWindows()


if __name__ == "__main__":
    main()

这个程序读取一张图片,并将其通过10 x 10 的卷积核作均值滤波并显示结果。

cvFilter2D cvfilter2d源码_opencv

参数详解

同样的,这里把各个参数打一张表:

参数

类型

是否必须指定(默认值)

具体含义

src

numpy.ndarray


原图像

ddepth

int


目标图像深度(指数据类型)

kernel

numpy.ndarray


卷积核

anchor

tuple

否(卷积核中心)

卷积锚点

delta

是数据类就行

否(0)

偏移量,卷积结果要加上这个数字

borderType

int(实际上是 enum 类)

否(cv.BORDER_DEFALUT)

边缘类型

src

这个参数没什么好说的,就是原图像。它可以是任何色彩模式,这就意味着如果你把原本送到这个函数里的图片从黑白变成了彩色(单通道变成了 3 通道),你并不需要更改其他参数。本身,对多通道的图像,卷积就是以通道为单位进行的。

ddepth

这个参数有点费解了。大部分情况下不需要管它是干嘛的,直接把它设成 -1 就没有任何问题。

参数名 ddepth ,英文是 desired depth,即期望深度。什么意思呢?我们来看它的可能取值表(来自这里):

输入深度(src.depth())

期望深度(ddepth

CV_8U

-1/CV_16S/CV_32F/CV_64F

CV_16U/CV_16S

-1/CV_32F/CV_64F

CV_32F

-1/CV_32F/CV_64F

CV_64F

-1/CV_64F

这个表里的一大串 CV 打头的符号到底是什么意思呢?实际上这些符号的末尾字母对应了数据类型:

U == Unsigned int  # 无符号整型
S == Signed int    # 有符号整型
F == Float         # 浮点型

中间的数字很显然代表了数据类型所占用的空间(bit)。所以,所谓深度,其实指的是数据类型

那就好说了,你会发现这里其实是规定了输出数据的类型,包括每个通道的每个像素占用多少空间。输出数据的类型必须根据上面的表格中输入对应的类型指定。这里 -1 表示输出类型和输入相同

不过,值得注意的是,似乎有些数据类型无法通过 cv2.imshow 正常显示,可以用 matplotlib.pyplot.imshow 来代替。

但是,还是注意,没有关于数据类型的特别要求时,这个功能是不需要的,取 -1 即可。

kernel & anchor

这两个参数都是卷积相关的,因此放在一节里面讲述。接下来的内容假设你已经了解了图像卷积

这里,kernel 很显然表示的是卷积核,这是一个 numpy.ndarray 类型的矩阵。这个矩阵的生成可以用 numpy 自带的函数,但是对于复杂一些的卷积核,OpenCV 内部的一些函数显然更合适。如 getStructuringElementgetGaussianKernel,前者用于获取特定形状的核,后者则是高斯核生生成器(不过要注意生成的是一个向量)。

# 方法一示例
kernel = cv.getStructuringElement(cv.MORPH_RECT, (11, 11))

# 方法二示例
vector = cv.getGaussianKernel(11, -1)
kernel = vector @ vector.T

anchor 则表示锚点。什么叫锚点呢?看下面这张图:

cvFilter2D cvfilter2d源码_python_02

可以说,锚点 anchor 决定了卷积核相对于生成目标点的位置。虽然锚点是相对于卷积核来定义的,但是卷积的过程更像是通过锚点去寻找卷积核。遍历图像中的每一个像素,以每一个像素为锚点,按照相对位置生成卷积范围,和卷积核对应元素相乘再求和得到目标图像中对应像素的值。可以用公式表示成:
cvFilter2D cvfilter2d源码_opencv_03
这实际上就是一般的图像卷积计算方法。OpenCV 文档里面叙述的卷积定义则是需要将卷积核围绕锚点对称变换,再用上面的公式计算。这种方法更接近卷积原始的定义,但是对图像的卷积一般的应用而言(滤波、深度学习)这两种定义实际上没有什么区别。、

锚点用一个元组指定,是相对于卷积核左上角的坐标,从 0 开始

# 替换掉 HelloWorld 程序的对应行。
avg_filtered = cv.filter2D(img, -1, kernel, (1, 2))

delta

这个参数的存在其实有些费解,它的效果很简单,就是把卷积的结果加上一个固定的数字。直观上将,它将整个图像变亮或者变暗了。从应用上来说,它实际上将卷积过程扩展成了一般的线性运算(cvFilter2D cvfilter2d源码_卷积核_04 表示卷积):
cvFilter2D cvfilter2d源码_python_05
这个线性运算可以将结果限定在某一符合要求的范围内(比如大于 0),而且不会阻断梯度的传递。

borderType

这个参数更改的是 border 的生成方式。这个 border,也就是边缘,是在靠近边缘的部分卷积时用到的,参考上面那张图。无论 anchor 是什么,总有些边缘上的点对应的卷积范围无法完全落在原图内,这就需要对原图进行扩展。扩展的方法就是我们这里参数的含义。

这个参数取值是 OpenCV 里的 cv::BorderTypes enum 类定义的值,其可能取值及其对于边缘的作用结果如下图所示(图片来自 OpenCV Python 教程):

cvFilter2D cvfilter2d源码_卷积_06

要注意几点:

  • cv.BORDER_WARP 在这个函数里面是不支持的;
  • cv.BORDER_CONSTANT 会将边缘取为 0(黑色),而且没法改,因为原函数并没有留出相关的接口。

扩展——滤波相关函数

图像滤波是一个很常用的功能,对此,OpenCV 也定义了很多函数。这里介绍的 cv.filter2D 是这些函数中可控性最高的,因为你可以用自定义的核进行卷积。但是一些常用的滤波,我们可以不必自己定义相应的核,直接用定义好的函数就可以了。

均值滤波

dst = cv.blur(img, (11, 11))

# 等效于:
dst = cv.filter2D(img, -1, np.ones((11, 11))/11**2)

高斯滤波

dst = cv.GaussianBlur(img, (11, 11), -1)

# 等效于
vector = cv.getGaussianKernel(11, -1)
kernel = vector @ vector.T
dst = cv.filter2D(img, -1, kernel)

中值滤波

dst = cv.medianBlur(img, 11)

注意中值滤波是取中位数作为目标值,是一个非线性滤波子,因此无法通过线性滤波的 cv.filter2D 来等效实现。

参考链接