OpenCV 4.11.0
开源计算机视觉库
加载中…
搜索中…
无匹配项
傅里叶变换

目标

在本节中,我们将学习

  • 使用OpenCV查找图像的傅里叶变换
  • 利用Numpy中提供的FFT函数
  • 傅里叶变换的一些应用
  • 我们将看到以下函数:cv.dft()cv.idft()

理论

傅里叶变换用于分析各种滤波器的频率特性。对于图像,使用二维离散傅里叶变换 (DFT) 来查找频域。一种称为快速傅里叶变换 (FFT) 的快速算法用于计算DFT。有关这些内容的详细信息,可以在任何图像处理或信号处理教科书中找到。请参阅“附加资源”部分。

对于正弦信号,\(x(t) = A \sin(2 \pi ft)\),我们可以说\(f\)是信号的频率,如果对其进行频域变换,我们可以在\(f\)处看到一个尖峰。如果对信号进行采样以形成离散信号,我们将获得相同的频域,但在\([- \pi, \pi]\)或\([0,2\pi]\)(或\([0,N]\)对于N点DFT)范围内是周期性的。您可以将图像视为在两个方向上采样的信号。因此,在X和Y方向上进行傅里叶变换将为您提供图像的频率表示。

更直观地说,对于正弦信号,如果幅度在短时间内变化很快,可以说它是高频信号。如果变化缓慢,则是低频信号。您可以将相同的概念扩展到图像。图像中幅度变化剧烈的地方在哪里?在边缘点或噪声处。因此,我们可以说,边缘和噪声是图像中的高频成分。如果幅度变化不大,则它是低频分量。(一些链接添加到“附加资源”部分,这些链接以示例直观地解释了频率变换)。

现在我们将了解如何找到傅里叶变换。

Numpy中的傅里叶变换

首先,我们将了解如何使用Numpy查找傅里叶变换。Numpy有一个FFT包可以做到这一点。np.fft.fft2() 为我们提供频率变换,这将是一个复数数组。它的第一个参数是输入图像,它是灰度图像。第二个参数是可选的,它决定输出数组的大小。如果它大于输入图像的大小,则在计算FFT之前,输入图像将用零填充。如果它小于输入图像,则输入图像将被裁剪。如果没有传递参数,则输出数组大小将与输入相同。

现在,一旦您获得了结果,零频率分量(直流分量)将位于左上角。如果要将其移到中心,需要在两个方向上将结果偏移\(\frac{N}{2}\)。这可以通过函数np.fft.fftshift()简单地完成。(这更容易分析)。一旦找到频率变换,就可以找到幅度谱。

import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
img = cv.imread('messi5.jpg', cv.IMREAD_GRAYSCALE)
assert img is not None, "文件无法读取,请使用os.path.exists()检查"
f = np.fft.fft2(img)
fshift = np.fft.fftshift(f)
magnitude_spectrum = 20*np.log(np.abs(fshift))
plt.subplot(121),plt.imshow(img, cmap = 'gray')
plt.title('输入图像'), plt.xticks([]), plt.yticks([])
plt.subplot(122),plt.imshow(magnitude_spectrum, cmap = 'gray')
plt.title('幅度谱'), plt.xticks([]), plt.yticks([])
plt.show()
CV_EXPORTS_W Mat imread(const String &filename, int flags=IMREAD_COLOR_BGR)
从文件中加载图像。

结果如下所示

图像

看,您可以看到中心区域更白的区域,表明低频成分更多。

所以您找到了频率变换。现在您可以在频域中进行一些操作,例如高通滤波并重建图像,即找到逆DFT。为此,您可以通过使用大小为60x60的矩形窗口进行掩蔽来去除低频。然后使用np.fft.ifftshift()应用逆移位,以便直流分量再次出现在左上角。然后使用np.ifft2()函数找到逆FFT。结果仍然是一个复数。您可以取其绝对值。

rows, cols = img.shape
crow, ccol = rows//2, cols//2
fshift[crow-30:crow+31, ccol-30:ccol+31] = 0
f_ishift = np.fft.ifftshift(fshift)
img_back = np.fft.ifft2(f_ishift)
img_back = np.real(img_back)
plt.subplot(131),plt.imshow(img, cmap = 'gray')
plt.title('输入图像'), plt.xticks([]), plt.yticks([])
plt.subplot(132),plt.imshow(img_back, cmap = 'gray')
plt.title('高通滤波后图像'), plt.xticks([]), plt.yticks([])
plt.subplot(133),plt.imshow(img_back)
plt.title('JET颜色结果'), plt.xticks([]), plt.yticks([])
plt.show()

结果如下所示

图像

结果表明,高通滤波是一种边缘检测操作。这就是我们在图像梯度章节中看到的。这也表明大部分图像数据存在于频谱的低频区域。无论如何,我们已经看到了如何在Numpy中找到DFT、IDFT等。现在让我们看看如何在OpenCV中做到这一点。

如果您仔细观察结果,尤其是JET颜色中的最后一张图像,您可以看到一些伪影(我在红箭头中标记了一个实例)。它显示了一些波纹状结构,称为振铃效应。这是由我们用于掩蔽的矩形窗口引起的。此掩码转换为sinc形状,从而导致此问题。因此,矩形窗口不用于滤波。更好的选择是高斯窗口。

OpenCV中的傅里叶变换

OpenCV 提供了cv.dft()cv.idft() 函数来实现这一点。它返回与之前相同的结果,但有两个通道。第一个通道将包含结果的实部,第二个通道将包含结果的虚部。输入图像应首先转换为 np.float32。我们将了解如何操作。

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('messi5.jpg', cv.IMREAD_GRAYSCALE)
assert img is not None, "文件无法读取,请使用os.path.exists()检查"
dft = cv.dft(np.float32(img),flags = cv.DFT_COMPLEX_OUTPUT)
dft_shift = np.fft.fftshift(dft)
magnitude_spectrum = 20*np.log(cv.magnitude(dft_shift[:,:,0],dft_shift[:,:,1]))
plt.subplot(121),plt.imshow(img, cmap = 'gray')
plt.title('输入图像'), plt.xticks([]), plt.yticks([])
plt.subplot(122),plt.imshow(magnitude_spectrum, cmap = 'gray')
plt.title('幅度谱'), plt.xticks([]), plt.yticks([])
plt.show()
void magnitude(InputArray x, InputArray y, OutputArray magnitude)
计算二维向量的幅度。
void dft(InputArray src, OutputArray dst, int flags=0, int nonzeroRows=0)
执行一维或二维浮点数组的正向或反向离散傅里叶变换。
注意
您也可以使用cv.cartToPolar(),它可以一次性返回幅度和相位。

所以,现在我们要进行逆 DFT 变换。在上一个环节中,我们创建了一个高通滤波器 (HPF),这次我们将了解如何去除图像中的高频成分,即我们将低通滤波器 (LPF) 应用于图像。这实际上会模糊图像。为此,我们首先创建一个掩码,在低频处具有高值 (1),即我们通过低频成分,在高频区域为 0。

rows, cols = img.shape
crow, ccol = rows//2, cols//2
# 首先创建一个掩码,中心正方形为 1,其余均为零
mask = np.zeros((rows,cols,2),np.uint8)
mask[crow-30:crow+30, ccol-30:ccol+30] = 1
# 应用掩码和逆 DFT
fshift = dft_shift*mask
f_ishift = np.fft.ifftshift(fshift)
img_back = cv.idft(f_ishift)
img_back = cv.magnitude(img_back[:,:,0],img_back[:,:,1])
plt.subplot(121),plt.imshow(img, cmap = 'gray')
plt.title('输入图像'), plt.xticks([]), plt.yticks([])
plt.subplot(122),plt.imshow(img_back, cmap = 'gray')
plt.title('幅度谱'), plt.xticks([]), plt.yticks([])
plt.show()
void idft(InputArray src, OutputArray dst, int flags=0, int nonzeroRows=0)
计算一维或二维数组的逆离散傅里叶变换。

查看结果

图像
注意
通常,OpenCV 函数cv.dft()cv.idft() 比 NumPy 对应的函数更快。但是 NumPy 函数更易于使用。有关性能问题的更多详细信息,请参见下面的部分。

DFT 的性能优化

DFT 计算的性能对于某些数组大小更好。当数组大小为 2 的幂时,速度最快。大小为 2、3 和 5 的乘积的数组也能被相当有效地处理。因此,如果您担心代码的性能,可以在查找 DFT 之前将数组的大小修改为任何最佳大小(通过填充零)。对于 OpenCV,您必须手动填充零。但是对于 NumPy,您可以指定 FFT 计算的新大小,它会自动为您填充零。

那么我们如何找到这个最佳大小呢?OpenCV 提供了一个函数cv.getOptimalDFTSize() 来实现这一点。它适用于cv.dft()np.fft.fft2()。让我们使用 IPython 魔法命令 timeit 检查它们的性能。

In [15]: img = cv.imread('messi5.jpg', cv.IMREAD_GRAYSCALE)
In [16]: assert img is not None, "文件无法读取,请使用 os.path.exists() 检查"
In [17]: rows,cols = img.shape
In [18]: print("{} {}".format(rows,cols))
342 548
In [19]: nrows = cv.getOptimalDFTSize(rows)
In [20]: ncols = cv.getOptimalDFTSize(cols)
In [21]: print("{} {}".format(nrows,ncols))
360 576
int getOptimalDFTSize(int vecsize)
返回给定向量大小的最佳 DFT 大小。

可以看到,大小 (342,548) 被修改为 (360, 576)。现在让我们用零填充它(对于 OpenCV)并找到它们的 DFT 计算性能。您可以通过创建一个新的大的零数组并将数据复制到其中来实现,或者使用cv.copyMakeBorder()

nimg = np.zeros((nrows,ncols))
nimg[:rows,:cols] = img

或者

right = ncols - cols
bottom = nrows - rows
bordertype = cv.BORDER_CONSTANT # 只是为了避免 PDF 文件中的换行
nimg = cv.copyMakeBorder(img,0,bottom,0,right,bordertype, value = 0)
void copyMakeBorder(InputArray src, OutputArray dst, int top, int bottom, int left, int right, int borderType, const Scalar &value=Scalar())
在图像周围形成边界。

现在我们计算 NumPy 函数的 DFT 性能比较

In [22]: %timeit fft1 = np.fft.fft2(img)
10 loops, best of 3: 40.9 ms per loop
In [23]: %timeit fft2 = np.fft.fft2(img,[nrows,ncols])
100 loops, best of 3: 10.4 ms per loop

这显示了 4 倍的加速。现在我们将尝试使用 OpenCV 函数。

In [24]: %timeit dft1= cv.dft(np.float32(img),flags=cv.DFT_COMPLEX_OUTPUT)
100 loops, best of 3: 13.5 ms per loop
In [27]: %timeit dft2= cv.dft(np.float32(nimg),flags=cv.DFT_COMPLEX_OUTPUT)
100 loops, best of 3: 3.11 ms per loop

这也显示了 4 倍的加速。您还可以看到 OpenCV 函数比 NumPy 函数快大约 3 倍。这也可以针对逆 FFT 进行测试,这留作练习。

为什么拉普拉斯算子是高通滤波器?

在论坛中有人提出了类似的问题。问题是,为什么拉普拉斯算子是高通滤波器?为什么 Sobel 算子是高通滤波器?等等。给出的第一个答案是根据傅里叶变换。只需对更大大小的 FFT 的拉普拉斯算子进行傅里叶变换。分析它。

import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
# 没有缩放参数的简单平均滤波器
mean_filter = np.ones((3,3))
# 创建高斯滤波器
gaussian = x*x.T
# 不同的边缘检测滤波器
# x方向 Scharr 算子
scharr = np.array([[-3, 0, 3],
[-10,0,10],
[-3, 0, 3]])
# x方向 Sobel 算子
sobel_x= np.array([[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]])
# y方向 Sobel 算子
sobel_y= np.array([[-1,-2,-1],
[0, 0, 0],
[1, 2, 1]])
# 拉普拉斯算子
laplacian=np.array([[0, 1, 0],
[1,-4, 1],
[0, 1, 0]])
filters = [mean_filter, gaussian, laplacian, sobel_x, sobel_y, scharr]
filter_name = ['均值滤波器', '高斯滤波器','拉普拉斯算子', 'Sobel_x', \
'Sobel_y', 'Scharr_x']
fft_filters = [np.fft.fft2(x) for x in filters]
fft_shift = [np.fft.fftshift(y) for y in fft_filters]
mag_spectrum = [np.log(np.abs(z)+1) for z in fft_shift]
for i in range(6)
plt.subplot(2,3,i+1),plt.imshow(mag_spectrum[i],cmap = 'gray')
plt.title(filter_name[i]), plt.xticks([]), plt.yticks([])
plt.show()
Mat getGaussianKernel(int ksize, double sigma, int ktype=CV_64F)
返回高斯滤波器系数。

查看结果

图像

从图像中,您可以看到每个核阻挡的频率区域以及它通过的区域。根据这些信息,我们可以说明为什么每个核是高通滤波器 (HPF) 或低通滤波器 (LPF)。

附加资源

  1. 傅里叶理论的直观解释 by Steven Lehar
  2. 傅里叶变换 at HIPR
  3. 在图像的情况下,频域表示什么?