理论
傅立叶变换用于分析各种滤波器的频率特性。对于图像,2D离散傅里叶变换(DFT)用于找到频域。称为快速傅里叶变换(FFT)的快速算法用于计算DFT。有关这些的详细信息可以在任何图像处理或信号处理教科书中找到。
对于正弦信号,x(t)= Asin(2πft),我们可以说f是信号的频率,如果采用其频域,我们可以看到f处的尖峰。如果对信号进行采样以形成离散信号,则我们得到相同的频域,但在[-π,π]或[0,2π](或对于N点DFT的[0,N])范围内是周期性的。你可以将图像视为在两个方向上采样的信号。因此,在X和Y方向上进行傅里叶变换可以得到图像的频率表示。
更直观地说,对于正弦信号,如果幅度在短时间内变化如此之快,则可以说它是高频信号。如果变化缓慢,则为低频信号。你可以将相同的想法扩展到图像。幅度在图像中的幅度变化很大?在边缘点,或噪音。我们可以说,边缘和噪声是图像中的高频内容。如果幅度没有太大变化,则它是低频分量。 (一些链接被添加到Additional Resources_,它通过示例直观地解释了频率变换)。
现在我们将看到如何找到傅立叶变换。
Numpy中的傅里叶变换
首先,我们将看到如何使用Numpy找到傅立叶变换。Numpy有一个FFT包来做到这一点。np.fft.fft2()为我们提供了一个复杂数组的频率变换。它的第一个参数是输入图像,它是灰度。第二个参数是可选的,它决定了输出数组的大小。如果它大于输入图像的大小,则在计算FFT之前用零填充输入图像。如果小于输入图像,则将裁剪输入图像。如果没有传递参数,则输出数组大小将与输入相同。
现在,一旦得到结果,零频率分量(DC分量)将位于左上角。如果要将其置于中心位置,则需要在两个方向上将结果移动$$\frac{N}{2}$$。这只是通过函数np.fft.fftshift()完成的。找到频率变换后,你可以找到幅度谱。
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
img = cv.imread(r'C:\Users\yuyalong\Pictures\Saved Pictures\opera.jpg', 0)
# 傅里叶变换 需要是灰度图像噢
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('Input Image'), plt.xticks([]), plt.yticks([])
plt.subplot(122),plt.imshow(magnitude_spectrum, cmap = 'gray')
plt.title('Magnitude Spectrum'), plt.xticks([]), plt.yticks([])
plt.show()
请注意,你可以在中心看到更多更白的区域,显示低频内容更多。
所以你找到了频率变换现在你可以在频域做一些操作,比如高通滤波和重建图像,即找到逆DFT。 为此,你只需通过使用尺寸为60x60的矩形窗口进行遮罩来移除低频。 然后使用np.fft.ifftshift()应用反向移位,以便DC组件再次出现在左上角。 然后使用np.ifft2()函数找到逆FFT。 结果再次是一个复杂的数字。 你可以采取它的绝对价值。
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
img = cv.imread(r'C:\Users\yuyalong\Pictures\Saved Pictures\opera.jpg', 0)
# 快速傅里叶变换 需要是灰度图像噢
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('Input Image'), plt.xticks([]), plt.yticks([])
#
# plt.subplot(122), plt.imshow(magnitude_spectrum, cmap='gray')
# plt.title('Magnitude Spectrum'), plt.xticks([]), plt.yticks([])
# plt.show()
rows, cols = img.shape
print(1111, rows/2, cols/2)
# 使用60 * 60的块遮盖低频区域
crow, ccol = int(rows / 2), int(cols / 2)
fshift[crow - 30:crow + 30, ccol - 30:ccol + 30] = 0
# 反向位移
f_ishift = np.fft.ifftshift(fshift)
# 逆傅里叶变换
img_back = np.fft.ifft2(f_ishift)
# 取绝对值
img_back = np.abs(img_back)
plt.subplot(121), plt.imshow(img, cmap='gray')
plt.title('Input Image'), plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(img_back)
plt.title('Result in JET'), plt.xticks([]), plt.yticks([])
plt.show()
结果显示高通滤波是边缘检测操作。这是我们在4.7. Canny边缘检测章节中看到的。这也表明大多数图像数据存在于光谱的低频区域。无论如何,我们已经看到如何在Numpy中找到DFT,IDFT等。现在让我们看看如何在OpenCV中完成它。
如果你仔细观察结果,特别是JET颜色的最后一个图像,你可以看到一些纹物(我用红色箭头标记的一个实例)。它在那里显示出一些类似波纹的结构,它被称为振铃效应。它是由我们用于遮蔽的矩形窗口引起的。此掩膜转换为sinc形状,这会导致此问题。因此矩形窗口不用于过滤。更好的选择是高斯窗口。
OpenCV中的傅里叶变换
OpenCV为此提供了cv.dft()和cv.idft()函数。它返回与之前相同的结果,但有两个通道。第一个通道将具有结果的实部,第二个通道将具有结果的虚部。输入图像应首先转换为np.float32。我们将看到如何做到这一点。cv2.dft(src, dst=None, flag=None, nonzeroRows=0)
- src:输入图像,要求 np.float32 格式;
- dst:输出图像,双通道(实部+虚部),大小和类型取决于第三个参数 flags;
- flags:表示转换标记,默认为 0,存在多种取值,参见后文;
- nonzeroRows:默认为 0,暂时不考虑。
flags 取值如下:
- DFT_INVERSE:用一维或二维逆变换取代默认的正向变换;
- DFT_SCALE: 缩放比例标识符,根据数据元素个数平均求出其缩放结果,如有 N 个元素,则输出结果以 1/N 缩放输出,常与 DFT_INVERSE 搭配使用;
- DFT_ROWS:对输入矩阵的每行进行正向或反向的傅里叶变换;此标识符可在处理多种适量的的时候用于减小资源的开销,这些处理常常是三维或高维变换等复杂操作;
- DFT_COMPLEX_OUTPUT:对一维或二维的实数数组进行正向变换,这样的结果虽然是复数阵列,但拥有复数的共轭对称性(CCS),可以以一个和原数组尺寸大小相同的实数数组进行填充,这是最快的选择也是函数默认的方法。你可能想要得到一个全尺寸的复数数组(像简单光谱分析等等),通过设置标志位可以使函数生成一个全尺寸的复数输出数组;
- DFT_REAL_OUTPUT:对一维二维复数数组进行逆向变换,这样的结果通常是一个尺寸相同的复数矩阵,但是如果输入矩阵有复数的共轭对称性(比如是一个带有 DFT_COMPLEX_OUTPUT标识符的正变换结果),便会输出实数矩阵。
总结下来就是:
- DFT_COMPLEX_OUTPUT:得到一个复数形式的矩阵;
- DFT_REAL_OUTPUT:只输出复数的实部;
- DFT_INVERSE:进行傅里叶逆变换;
- DFT_SCALE:是否除以 MxN (M 行 N 列的图片,共有有 MxN 个像素点);
- DFT_ROWS:输入矩阵的每一行进行傅里叶变换或者逆变换。
最后需要注意的是,输出的频谱结果是一个复数,需要调用 cv2.magnitude() 函数将傅里叶变换的双通道结果转换为 0 到 255 的范围。
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
img = cv.imread(r'C:\Users\yuyalong\Pictures\Saved Pictures\opera.jpg', 0)
# 傅里叶变换
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('Input Image'), plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(magnitude_spectrum, cmap='gray')
plt.title('Magnitude Spectrum'), plt.xticks([]), plt.yticks([])
plt.show()
所以,现在我们必须进行逆DFT。 在之前的会话中,我们创建了一个HPF,这次我们将看到如何去除图像中的高频内容,即我们将LPF应用于图像。 它实际上模糊了图像。 为此,我们首先在低频处创建具有高值(1)的掩模,即我们传递LF内容,并且在HF区域传递0。
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
img = cv.imread(r'C:\Users\yuyalong\Pictures\Saved Pictures\opera.jpg', 0)
# 傅里叶变换
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('Input Image'), plt.xticks([]), plt.yticks([])
# plt.subplot(122), plt.imshow(magnitude_spectrum, cmap='gray')
# plt.title('Magnitude Spectrum'), plt.xticks([]), plt.yticks([])
# plt.show()
rows, cols = img.shape
crow, ccol = int(rows / 2), int(cols / 2)
# create a mask first, center square is 1, remaining all zeros
mask = np.zeros((rows, cols, 2), np.uint8)
mask[crow - 30:crow + 30, ccol - 30:ccol + 30] = 1
# apply mask and inverse 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('Input Image'), plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(img_back, cmap='gray')
plt.title('Magnitude Spectrum'), plt.xticks([]), plt.yticks([])
plt.show()
注意:像往常一样,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 magic命令timeit检查它们的性能。
In [16]: img = cv.imread('messi5.jpg',0)
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
看,大小(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 #just to avoid line breakup in PDF file
nimg = cv.copyMakeBorder(img,0,bottom,0,right,bordertype, value = 0)
现在我们计算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是HPF? 第一个答案就是傅立叶变换。 只需将拉普拉斯算子的傅里叶变换用于更高尺寸的FFT。 分析一下:
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
# simple averaging filter without scaling parameter
mean_filter = np.ones((3,3))
# creating a gaussian filter
x = cv.getGaussianKernel(5,10)
gaussian = x*x.T
# different edge detecting filters
# scharr in x-direction
scharr = np.array([[-3, 0, 3],
[-10,0,10],
[-3, 0, 3]])
# sobel in x direction
sobel_x= np.array([[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]])
# sobel in y direction
sobel_y= np.array([[-1,-2,-1],
[0, 0, 0],
[1, 2, 1]])
# laplacian
laplacian=np.array([[0, 1, 0],
[1,-4, 1],
[0, 1, 0]])
filters = [mean_filter, gaussian, laplacian, sobel_x, sobel_y, scharr]
filter_name = ['mean_filter', 'gaussian','laplacian', '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 xrange(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()