Thinking of 2D convolution

Thinking of 2D convolution

数字信号处理的卷积运算 基础上,本文介绍图像处理中的卷积运算。同时本文是续接在二维傅里叶变换之后。

在实际的图像处理中,我们通常并不会在频域进行计算。即传统的先 fft, 再和频域滤波器相乘,最后 ifft 这套思路。而是会直接在空间域进行卷积计算。为什么 ?

因为我们通常使用较小的滤波核,目的是尽可能获得其频域对应内容的显著特性

如何理解这句话 ???

一般来说,当卷积核的尺寸减小时,对 RGB 分量图像进行滤波和对同一幅 HSI 图像的亮度分量进行滤波时,得到的差别也减少了。

附上还没有介绍的论文: A guide to convolution arithmetic for deep learning

1. 二维卷积

连续信号: \[
f(x, y) * g(x, y) = \int_{\tau_1 = -\infty} ^{\infty} \int_{\tau_2 = -\infty} ^{\infty} f(\tau_1, \tau_2) \cdot g(x -\tau_1, y-\tau_2) d \tau_1 d \tau_2
\]
离散信号: \[
f[x, y] * g[x, y] = \sum_{n_1 = -\infty} ^{\infty} \sum_{n_2 = -\infty} ^{\infty} f[n_1, n_2] \cdot g[x -n_1, y-n_2]
\]
显然,二维卷积用逐点求卷积的方式比较靠谱。

步骤如下:

  1. 还记得一维线性卷积的列表法吧,其中有一步是将其中一个序列翻褶,这里同样要这样做,只是变成了二维矩阵,因此要同时水平和垂直翻褶,效果就是绕原点旋转 180 度,这里请不要先入为主,认为是绕矩阵中心,想一想之前不也是绕 \(y\) 轴翻折吗?而且 \(g(-\tau_1, -\tau_2)\) 确实是 \(g(\tau_1, \tau_2)\) 绕原点翻褶得到的。
  2. 然后就是移位,并和矩阵对应点相乘求和
  3. 依次平移,可以得到所有点的卷积

但是 !!!!!!!!

如果你仔细观察 OpenCV 或者各大论文的卷积计算,你会发现它们的计算和我们熟悉的卷积有些许不同。

比如下面这个例子,下图是一个 \(3 \times 3\) 的卷积核。

请注意,上图中,我们的序列索引值并不是从常见的坐标 \((0, 0)\) 开始的,而是从 \((-1, -1)\) 开始的 !!!

这样做的目的是为了使 \((0, 0)\) 索引在卷积核中心位置,这非常有利于二维卷积的计算,同时也方便思考。

为什么可以这样做 ?

因为图像处理并不需要做实时卷积运算,我们都是在获得整张图像后再进行卷积。因此我们可以用超前数据,也就是说卷积核的索引出现负数,该索引对应的输入索引就会比我们要求的点的索引还要大,这就是超前的索引。

而这在追求高实时性的数字信号处理中是不可能的,比如我们要对传感器采集信号进行滤波处理,我们不可能让采集信号的实时输出值依赖于传感器将来会采集到的值,在数字信号处理中有个术语要因果性,就是描述的。

现在假设我们输入一个 \(5 \times 5\) 的矩阵,我们希望和上面的卷积核进行二维卷积,输出位置为 \((1,1)\) 的点的卷积值是多少呢 ?

根据卷积公式: \[
y[m ,n] = x[m ,n] \ast h[m ,n] = \sum_{j=-\infty}^{\infty} \sum_{i= -\infty}^{\infty} x[i, j] \cdot h[m -i, n-j]
\]
于是: \[
\begin {aligned}
y[1, 1] &= \sum_{j=-\infty}^{\infty} \sum_{i= -\infty}^{\infty} x[i, j] \cdot h[1 -i, 1-j] \\
&= x[0, 0] \cdot h[1, 1] + x[1, 0] \cdot h[0, 1] + x[2, 0] \cdot h[-1, 1] \\
&+ x[0, 1] \cdot h[1, 0] + x[1,1] \cdot h[0, 0] + x[2, 1] \cdot h[-1, 0] \\
&+ x[0, 2] \cdot h[1, -1] + x[1, 2] \cdot h[0, -1] + x[2, 2] \cdot h[-1, -1]
\end {aligned}
\]
看下图,是不是有更深的理解。

同样的,如果是 \(5 \times 5\) 的卷积核,我们会从 \((-2, -2)\) 开始,这样 \((0, 0)\) 索引还是在中心

下图是二维卷积的 C++ 程序:

由于 C++ 数组索引是从 \(0\) 开始的,小心其中的索引值变化,就 OK 了。

// find center position of kernel (half of kernel size)
kCenterX = kCols / 2;
kCenterY = kRows / 2;

for(i=0; i < rows; ++i)              // rows
{
    for(j=0; j < cols; ++j)          // columns
    {
        for(m=0; m < kRows; ++m)     // kernel rows
        {
            mm = kRows - 1 - m;      // row index of flipped kernel

            for(n=0; n < kCols; ++n) // kernel columns
            {
                nn = kCols - 1 - n;  // column index of flipped kernel

                // index of input signal, used for checking boundary
                ii = i + (m - kCenterY);
                jj = j + (n - kCenterX);

                // ignore input samples which are out of bound
                if( ii >= 0 && ii < rows && jj >= 0 && jj < cols )
                    out[i][j] += in[ii][jj] * kernel[mm][nn];
            }
        }
    }
}

2. 卷积和滤波

如果学过数字信号处理,自然知道,我们是先有线性移不变系统,然后有了卷积的定义。然后又发现了卷积和傅里叶变换的优良关系。于是,我们会想到先用 FFT 将信号转换到频域,然后和我们的滤波器相乘,并进行反傅里叶变换,我们就实现不同频带的滤波。这在时域看来,就是在做卷积 !!!

因此,目前的 DSP 处理,有众多现有的工具 (如 Matlab) 帮助我们直接设计好滤波器 (其实就是系数),然后直接在时域卷积。

二维卷积同样如此 !!! 下图展示了 高斯核卷积的原理

如何选择直接卷积,还是 FFT ? 这在之前数字信号处理的卷积博客有说到。主要是取决于卷积核的大小,目前主流的卷积核大小为 \(3 \times 3\) 或者 \(5 \times 5\) ,因此一般是直接卷积计算

2 thoughts on “Thinking of 2D convolution

发表评论

电子邮件地址不会被公开。 必填项已用*标注

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d 博主赞过: