使用np.fft.fft2和cv2.dft重现相位谱,结果为何不同?

3 投票
2 回答
103 浏览
提问于 2025-04-14 15:49

另一个问题在问如何正确地使用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")

Numpy结果

这是使用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结果

从上面的结果可以看出,虽然幅度谱看起来差不多(因为浮点运算的原因,可能会有一些预期的偏差),但相位谱却有明显不同。我查了一下,发现OpenCV通常返回的相位范围是从0到2π,而np.angle返回的相位范围是从-π到+π。不过,从OpenCV的相位中减去π并不能解决这个差异。

这可能是什么原因呢?有没有可能用这两种方法得到几乎相同的相位,就像幅度一样?

2 个回答

2

主要问题是,numpy.anglecv2.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.phasenumpy 对应实现:

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.anglenp.arctan2 等)通常是 float64 类型。


总结:cv2.cartToPolar 也是基于 arctan2 的,和 cv2.phase 一样,所以结论是相同的。

4

因为 imFFTOpenCV 是一个三维数组,OpenCV 不支持复数,所以 np.fft.fftshift(imFFTOpenCV) 会把实部和虚部的位置互换。也就是说,这个位置的交换会在数组的三个维度上都发生。

所以在计算相位和幅度的时候,你需要考虑到这个位置的交换:

magSpectrumOpenCV, phaseSpectrumOpenCV = cv2.cartToPolar(imFFTOpenCVShifted[:,:,1], imFFTOpenCVShifted[:,:,0])

另外,你也可以告诉 NumPy 你想要交换哪些轴,这样可能会更容易理解:

imFFTOpenCVShifted = np.fft.fftshift(imFFTOpenCV, axes=(0, 1))

撰写回答