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

目标

在本节中,我们将学习到

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

理论

傅里叶变换用于分析各种滤波器的频率特性。对于图像,2D 离散傅里叶变换 (DFT) 用于查找频域。一种称为快速傅里叶变换 (FFT) 的快速算法用于计算 DFT。有关它们的详细信息可以在任何图像处理或信号处理教科书中找到。请参阅其他资源_ 部分。

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

更直观地说,对于正弦信号,如果幅度在短时间内变化得非常快,你可以说它是一个高频信号。如果它变化缓慢,则它是一个低频信号。你可以将相同的概念扩展到图像。图像中的哪些地方幅度变化很大?在边缘点或噪声处。因此我们可以说,边缘和噪声是图像中的高频内容。如果幅度没有太大变化,则它是一个低频分量。(其他资源_ 中添加了一些链接,其中通过示例直观地解释了频率转换)。

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

Numpy 中的傅里叶变换

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

现在,一旦你得到了结果,零频率分量(DC 分量)将在左上角。如果要将其移至中心,则需要在两个方向上将结果移动 \(\frac{N}{2}\)。这只需使用函数 np.fft.fftshift() 即可完成。(分析起来更容易)。一旦你找到频率变换,就可以找到幅度谱。

导入 cv2 作为 cv
导入 numpy 作为 np
matplotlib 导入 pyplot 作为 plt
img = cv.imread('messi5.jpg', cv.IMREAD_GRAYSCALE)
断言 img 不是 , "无法读取文件,请使用 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)
从文件中加载图像。

结果如下所示

图像

请看,你可以看到中心有更多白色区域,表明低频内容更多。

所以你找到了频率变换,现在你可以在频域中执行一些操作,例如高通滤波并重建图像,即查找逆 DFT。为此,你只需通过使用 60x60 的矩形窗口遮罩来去除低频。然后使用 np.fft.ifftshift() 应用逆移位,以便 DC 分量再次出现在左上角。然后使用 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('HPF 处理后的图像'), 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。我们将看到如何执行此操作。

导入 numpy 作为 np
导入 cv2 作为 cv
matplotlib 导入 pyplot 作为 plt
img = cv.imread('messi5.jpg', cv.IMREAD_GRAYSCALE)
断言 img 不是 , "无法读取文件,请使用 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)
计算 2D 向量的幅度。
void dft(InputArray src, OutputArray dst, int flags=0, int nonzeroRows=0)
对 1D 或 2D 浮点数组执行正向或反向离散傅里叶变换。
注意
你也可以使用cv.cartToPolar(),它一次返回幅度和相位

因此,现在我们必须进行反向 DFT。在上一节中,我们生成了一个 HPF,这一次我们将看到如何去除图像中的高频内容,即我们对图像应用 LPF。它实际上模糊了图像。为此,我们首先使用低频的高值 (1) 创建一个掩码,即我们传递 LF 内容,而 HF 区域为 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)
计算 1D 或 2D 阵列的反向离散傅里叶变换。

查看结果

图像
注意
与往常一样,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, "file could not be read, check with 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 次循环,最佳 3 次循环:每个循环 40.9 毫秒
In [23]: %timeit fft2 = np.fft.fft2(img,[nrows,ncols])
100 次循环,最佳 3 次循环:每个循环 10.4 毫秒

它显示了 4 倍加速。现在我们将使用 OpenCV 函数尝试相同的操作。

In [24]: %timeit dft1= cv.dft(np.float32(img),flags=cv.DFT_COMPLEX_OUTPUT)
100 次循环,最佳 3 次循环:每个循环 13.5 毫秒
In [27]: %timeit dft2= cv.dft(np.float32(nimg),flags=cv.DFT_COMPLEX_OUTPUT)
100 次循环,最佳 3 次循环:每个循环 3.11 毫秒

它还显示了 4 倍的加速。你还可以看到 OpenCV 函数比 Numpy 函数快约 3 倍。这也可以对逆 FFT 进行测试,并将其作为你的练习题。

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

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

导入 cv2 作为 cv
导入 numpy 作为 np
matplotlib 导入 pyplot 作为 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]])
过滤器 = [均值滤波器、高斯滤波器、拉普拉斯滤波器、sobel_x 滤波器、sobel_y 滤波器、scharr 滤波器]
filter_name = ['均值滤波器''高斯滤波器''拉普拉斯滤波器''sobel_x 滤波器'、\
'sobel_y 滤波器''scharr_x 滤波器']
fft_filters = [np.fft.fft2(x) 对于 x 过滤器中]
fft_shift = [np.fft.fftshift(y) 对于 y fft 过滤器中]
mag_spectrum = [np.log(np.abs(z)+1) 对于 z fft 移位中]
对于 i 6 的范围内
plt.subplot(2,3,i+1),plt.imshow(mag_spectrum[i],cmap = '灰度')
plt.title(filter_name[i]), plt.xticks([]), plt.yticks([])
plt.show()
Mat getGaussianKernel(int ksize, double sigma, int ktype=CV_64F)
返回高斯滤波器系数。

查看结果

图像

从图像中,你可以看到每个内核遮挡的频率区域和通过的区域。根据该信息,我们可以说出每个内核为何是 HPF 或 LPF

其他资源

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

练习