使用np.fft.fft2和cv2.dft重现相位谱,结果为何不同?
另一个问题在问如何正确地使用cv2.dft
来获取幅度和相位谱。
我之前的回答主要是基于numpy的方法,后来我觉得用OpenCV来做这个会更好。目前我正在尝试复现相同的结果,但我发现相位谱有明显的差异。
以下是我的导入代码:
%matplotlib notebook
import matplotlib.pyplot as plt
import numpy as np
import cv2
im = np.zeros((50, 50), dtype = np.float32) # create empty array
im[2:10, 2:10] = 255 # draw a rectangle
这是使用numpy的示例和结果:
imFFTNumpy = np.fft.fft2(im)
imFFTNumpyShifted = np.fft.fftshift(imFFTNumpy)
magSpectrumNumpy = np.abs(imFFTNumpyShifted)
phaseSpectrumNumpy = np.angle(imFFTNumpyShifted)
fig, ax = plt.subplots(nrows = 1, ncols = 3)
ax[0].imshow(im)
ax[1].imshow(magSpectrumNumpy)
ax[2].imshow(phaseSpectrumNumpy)
plt.suptitle("Using Numpy np.fft.fft2 and np.abs/ np.angle")
这是使用OpenCV的示例和结果:
imFFTOpenCV = cv2.dft(im, flags=cv2.DFT_COMPLEX_OUTPUT)
imFFTOpenCVShifted = np.fft.fftshift(imFFTOpenCV)
magSpectrumOpenCV, phaseSpectrumOpenCV = cv2.cartToPolar(imFFTOpenCVShifted[:,:,0], imFFTOpenCVShifted[:,:,1])
fig, ax = plt.subplots(nrows = 1, ncols = 3)
ax[0].imshow(im)
ax[1].imshow(magSpectrumOpenCV)
ax[2].imshow(phaseSpectrumOpenCV)
plt.suptitle("Using OpenCV cv2.dft and cv2.cartToPolar")
从上面的结果可以看出,虽然幅度谱看起来差不多(因为浮点运算的原因,可能会有一些预期的偏差),但相位谱却有明显不同。我查了一下,发现OpenCV通常返回的相位范围是从0到2π,而np.angle
返回的相位范围是从-π到+π。不过,从OpenCV的相位中减去π并不能解决这个差异。
这可能是什么原因呢?有没有可能用这两种方法得到几乎相同的相位,就像幅度一样?
2 个回答
主要问题是,numpy.angle
和 cv2.phase
这两个函数不能直接比较,因为它们的底层实现方式不同。
numpy.angle
是在一个固定的范围内工作,也就是 (-π, π]。cv2.phase
使用的是 arctan2,这是一个分段定义的函数,它的输出结果会根据输入的符号(正负)而变化。因此,原作者提到“OpenCV 通常返回的相位范围是从 0 到 2π”,就是这个原因。
为了能比较这两个库中复数的相位,最简单的方法是忘掉 numpy.angle
,直接使用 numpy.arctan2
:
im = np.array([[1, 0, 1], [1, 0, 1], [1, 0, 1]], dtype=np.uint8)
# numpy
fft_np = np.fft.fft2(im)
phase_np = np.mod(np.arctan2(np.real(fft_np), np.imag(fft_np)) * 180/np.pi, 360)
# opencv
fft_cv = cv2.dft(np.float32(im), flags=cv2.DFT_COMPLEX_OUTPUT)
phase_cv = cv2.phase(fft_cv[:,:,1], fft_cv[:,:,0], angleInDegrees=True)
# display results
print(phase_np)
print(phase_cv)
输出结果
[[ 90. 30. 150.]
[ 0. 0. 0.]
[ 0. 0. 0.]]
[[ 90. 29.997692 150.0023 ]
[ 0. 0. 0. ]
[ 0. 0. 0. ]]
但这还不是结束!numpy.arctan2
可能会返回负角度,而 cv2.phase
始终是正的……所以你需要对结果进行一次完整周期的 取模 操作,可以是 360 或 2π。这可以通过 numpy.mod
来实现。
你可以在上面的例子中使用 [[1, 0, 1, 1], [0, 1, 1, 0], [1, 0, 1, 1], [0, 0, 1, 0]]
来验证这一点。
这里是 cv2.phase
的 numpy
对应实现:
def phase_np(x, y, angleInDegrees=False): # angle_np2cv
if angleInDegrees:
return np.mod(np.arctan2(x, y) * 180/np.pi, 360)
return np.mod(np.arctan2(x, y), 2*np.pi)
另一个区别可能在于数据类型:np.fft2
使用的是 complex128
,而在其上的计算(np.angle
、np.arctan2
等)通常是 float64
类型。
总结:cv2.cartToPolar
也是基于 arctan2 的,和 cv2.phase
一样,所以结论是相同的。
因为 imFFTOpenCV
是一个三维数组,OpenCV 不支持复数,所以 np.fft.fftshift(imFFTOpenCV)
会把实部和虚部的位置互换。也就是说,这个位置的交换会在数组的三个维度上都发生。
所以在计算相位和幅度的时候,你需要考虑到这个位置的交换:
magSpectrumOpenCV, phaseSpectrumOpenCV = cv2.cartToPolar(imFFTOpenCVShifted[:,:,1], imFFTOpenCVShifted[:,:,0])
另外,你也可以告诉 NumPy 你想要交换哪些轴,这样可能会更容易理解:
imFFTOpenCVShifted = np.fft.fftshift(imFFTOpenCV, axes=(0, 1))