目标
在本节中,我们将学习
理论
傅里叶变换用于分析各种滤波器的频率特性。对于图像,二维离散傅里叶变换 (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之前用零填充输入图像。如果它小于输入图像,则输入图像将被裁剪。如果没有传递参数,输出数组大小将与输入相同。
现在,一旦你得到结果,零频率分量(直流分量)将在左上角。如果你想把它移到中心,你需要将结果在两个方向上都移动 \(\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)
从文件加载图像。
结果如下所示
image
可以看到,中心区域的白色更亮,表明低频内容更多。
所以你找到了频率变换。现在你可以在频域中进行一些操作,例如高通滤波和图像重建,即找到逆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()
结果如下所示
image
结果表明高通滤波是一种边缘检测操作。这正是我们在“图像梯度”章节中看到的内容。这也表明大部分图像数据存在于频谱的低频区域。总之,我们已经看到了如何在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
mask = np.zeros((rows,cols,2),np.uint8)
mask[crow-30:crow+30, ccol-30:ccol+30] = 1
fshift = dft_shift*mask
f_ishift = np.fft.ifftshift(fshift)
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)
计算一维或二维数组的逆离散傅里叶变换。
查看结果
image
- 注意
- 像往常一样,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 [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
void copyMakeBorder(InputArray src, OutputArray dst, int top, int bottom, int left, int right, int borderType, const Scalar &value=Scalar())
Forms a border around an image.
现在我们计算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
scharr = np.array([[-3, 0, 3],
[-10,0,10],
[-3, 0, 3]])
sobel_x= np.array([[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]])
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)
返回高斯滤波器系数。
查看结果
image
从图像中,您可以看到每个核阻挡了哪些频率区域,以及它通过了哪些区域。根据这些信息,我们可以说为什么每个核是HPF(高通滤波器)或LPF(低通滤波器)
附加资源
- 傅里叶理论的直观解释 作者:Steven Lehar
- 傅里叶变换 (来自HIPR)
- 在图像中,频域表示什么?