用傅里叶方法进行断层重建的这段代码有什么问题?

信息处理 图像处理 傅里叶变换 Python
2022-01-16 01:13:06

我最近一直在玩断层重建算法。我已经有了很好的 FBP、ART、类似 SIRT/SART 的迭代方案的工作实现,甚至使用直线代数(慢!)。 这个问题与任何这些技术无关“为什么有人会这样做,这里有一些 FBP 代码”形式的答案不是我想要的。

我想用这个程序做的下一件事是“完成集合”并实现所谓的“傅里叶重建方法”。我对此的理解基本上是您将一维 FFT 应用于正弦图“曝光”,将它们排列为二维傅里叶空间中的径向“轮辐”(这是直接从中心切片定理得出的有用的事情) ,从这些点插入到该二维空间中的规则网格,然后应该可以进行傅里叶逆变换以恢复原始扫描目标。

听起来很简单,但我没有太多运气得到任何看起来像原始目标的重建。

下面的 Python (numpy/SciPy/Matplotlib) 代码是关于我想要做的最简洁的表达。运行时,它显示以下内容:

图1:目标 图。1

图2:目标的正弦图 图2

图 3:FFT-ed 正弦图行 图3

图 4:顶行是从傅里叶域正弦图行内插的 2D FFT 空间;底行是(用于比较目的)目标的直接 2D FFT。这是我开始怀疑的地方。从正弦图 FFT 插值的图看起来类似于直接对目标进行 2D-FFT 处理的图......但又有所不同。 图4

图 5:图 4 的傅里叶逆变换。我希望这比实际更容易识别为目标。 图5

任何想法我做错了什么?不确定我对傅立叶方法重建的理解是否存在根本缺陷,或者我的代码中只是存在一些错误。

import math
import matplotlib
import matplotlib.pyplot as plt
import numpy as np

import scipy.interpolate
import scipy.fftpack
import scipy.ndimage.interpolation

S=256  # Size of target, and resolution of Fourier space
A=359  # Number of sinogram exposures

# Construct a simple test target
target=np.zeros((S,S))
target[S/3:2*S/3,S/3:2*S/3]=0.5
target[120:136,100:116]=1.0

plt.figure()
plt.title("Target")
plt.imshow(target)

# Project the sinogram
sinogram=np.array([
        np.sum(
            scipy.ndimage.interpolation.rotate(
                target,a,order=1,reshape=False,mode='constant',cval=0.0
                )
            ,axis=1
            ) for a in xrange(A)
        ])

plt.figure()
plt.title("Sinogram")
plt.imshow(sinogram)

# Fourier transform the rows of the sinogram
sinogram_fft_rows=scipy.fftpack.fftshift(
    scipy.fftpack.fft(sinogram),
    axes=1
    )

plt.figure()
plt.subplot(121)
plt.title("Sinogram rows FFT (real)")
plt.imshow(np.real(np.real(sinogram_fft_rows)),vmin=-50,vmax=50)
plt.subplot(122)
plt.title("Sinogram rows FFT (imag)")
plt.imshow(np.real(np.imag(sinogram_fft_rows)),vmin=-50,vmax=50)

# Coordinates of sinogram FFT-ed rows' samples in 2D FFT space
a=(2.0*math.pi/A)*np.arange(A)
r=np.arange(S)-S/2
r,a=np.meshgrid(r,a)
r=r.flatten()
a=a.flatten()
srcx=(S/2)+r*np.cos(a)
srcy=(S/2)+r*np.sin(a)

# Coordinates of regular grid in 2D FFT space
dstx,dsty=np.meshgrid(np.arange(S),np.arange(S))
dstx=dstx.flatten()
dsty=dsty.flatten()

# Let the central slice theorem work its magic!
# Interpolate the 2D Fourier space grid from the transformed sinogram rows
fft2_real=scipy.interpolate.griddata(
    (srcy,srcx),
    np.real(sinogram_fft_rows).flatten(),
    (dsty,dstx),
    method='cubic',
    fill_value=0.0
    ).reshape((S,S))
fft2_imag=scipy.interpolate.griddata(
    (srcy,srcx),
    np.imag(sinogram_fft_rows).flatten(),
    (dsty,dstx),
    method='cubic',
    fill_value=0.0
    ).reshape((S,S))

plt.figure()
plt.suptitle("FFT2 space")
plt.subplot(221)
plt.title("Recon (real)")
plt.imshow(fft2_real,vmin=-10,vmax=10)
plt.subplot(222)
plt.title("Recon (imag)")
plt.imshow(fft2_imag,vmin=-10,vmax=10)

# Show 2D FFT of target, just for comparison
expected_fft2=scipy.fftpack.fftshift(scipy.fftpack.fft2(target))

plt.subplot(223)
plt.title("Expected (real)")
plt.imshow(np.real(expected_fft2),vmin=-10,vmax=10)
plt.subplot(224)
plt.title("Expected (imag)")
plt.imshow(np.imag(expected_fft2),vmin=-10,vmax=10)

# Transform from 2D Fourier space back to a reconstruction of the target
fft2=scipy.fftpack.ifftshift(fft2_real+1.0j*fft2_imag)
recon=np.real(scipy.fftpack.ifft2(fft2))

plt.figure()
plt.title("Reconstruction")
plt.imshow(recon,vmin=0.0,vmax=1.0)

plt.show()
3个回答

好的,我终于破解了这个。

诀窍基本上归结为将一些fftshift/ ifftshifts 放在正确的位置,因此 2D 傅里叶空间表示不会剧烈振荡,并且注定不可能准确插值。至少那是我认为修复它的原因。我对傅里叶理论的有限理解大多基于连续积分公式,我总是发现离散域和 FFT 有点……古怪。

虽然我发现 matlab 代码相当神秘,但我必须相信这个实现至少让我相信这个重建算法可以在这种环境中合理紧凑地表达。

首先我会显示结果,然后是代码:

图 1:一个新的、更复杂的目标。 图。1

图 2:目标的正弦图(OK OK,这是 Radon 变换)。 图2

图 3:正弦图的 FFT 行(以 DC 为中心绘制)。 图3

图 4:FFT 正弦图转换为 2D FFT 空间(中心为 DC)。颜色是绝对值的函数。 图4

图 4a:放大 2D FFT 空间的中心,以更好地显示正弦图数据的径向性质。 图4a

图 5:顶行:从径向排列的 FFT 正弦图行内插的 2D FFT 空间。底行:从简单的 2D FFT-ing 目标的预期外观。
图5

图 5a:放大图 5 中子图的中心区域,以显示这些看起来在质量上非常一致。 图5a

图 6:酸性测试:插值 FFT 空间的逆 2D FFT 恢复目标。尽管我们刚刚让莉娜经历了一切,但她看起来仍然很好(可能是因为有足够的正弦图“辐条”来相当密集地覆盖 2D FFT 平面;如果你减少曝光角度的数量,事情会变得有趣,所以这不再是真的)。 在此处输入图像描述

这是代码;在 i7 上使用 Debian/Wheezy 的 64 位 SciPy 在不到 15 秒的时间内绘制出图。

import math
import matplotlib
import matplotlib.pyplot as plt
import numpy as np

import scipy.interpolate
import scipy.fftpack
import scipy.misc
import scipy.ndimage.interpolation

S=256 # Size of target, and resolution of Fourier space
N=259 # Number of sinogram exposures (odd number avoids redundant direct opposites)

V=100 # Range on fft plots

# Convenience function
def sqr(x): return x*x

# Return the angle of the i-th (of 0-to-N-1) sinogram exposure in radians.
def angle(i): return (math.pi*i)/N

# Prepare a target image
x,y=np.meshgrid(np.arange(S)-S/2,np.arange(S)-S/2)
mask=(sqr(x)+sqr(y)<=sqr(S/2-10))
target=np.where(
    mask,
    scipy.misc.imresize(
        scipy.misc.lena(),
        (S,S),
        interp='cubic'
        ),
    np.zeros((S,S))
    )/255.0

plt.figure()
plt.title("Target")
plt.imshow(target)
plt.gray()

# Project the sinogram (ie calculate Radon transform)
sinogram=np.array([
        np.sum(
            scipy.ndimage.interpolation.rotate(
                target,
                np.rad2deg(angle(i)), # NB rotate takes degrees argument
                order=3,
                reshape=False,
                mode='constant',
                cval=0.0
                )
            ,axis=0
            ) for i in xrange(N)
        ])

plt.figure()
plt.title("Sinogram")
plt.imshow(sinogram)
plt.jet()

# Fourier transform the rows of the sinogram, move the DC component to the row's centre
sinogram_fft_rows=scipy.fftpack.fftshift(
    scipy.fftpack.fft(
        scipy.fftpack.ifftshift(
            sinogram,
            axes=1
            )
        ),
    axes=1
    )

plt.figure()
plt.subplot(121)
plt.title("Sinogram rows FFT (real)")
plt.imshow(np.real(sinogram_fft_rows),vmin=-V,vmax=V)
plt.subplot(122)
plt.title("Sinogram rows FFT (imag)")
plt.imshow(np.imag(sinogram_fft_rows),vmin=-V,vmax=V)

# Coordinates of sinogram FFT-ed rows' samples in 2D FFT space
a=np.array([angle(i) for i in xrange(N)])
r=np.arange(S)-S/2
r,a=np.meshgrid(r,a)
r=r.flatten()
a=a.flatten()
srcx=(S/2)+r*np.cos(a)
srcy=(S/2)+r*np.sin(a)

# Coordinates of regular grid in 2D FFT space
dstx,dsty=np.meshgrid(np.arange(S),np.arange(S))
dstx=dstx.flatten()
dsty=dsty.flatten()

plt.figure()
plt.title("Sinogram samples in 2D FFT (abs)")
plt.scatter(
    srcx,
    srcy,
    c=np.absolute(sinogram_fft_rows.flatten()),
    marker='.',
    edgecolor='none',
    vmin=-V,
    vmax=V
    )

# Let the central slice theorem work its magic!
# Interpolate the 2D Fourier space grid from the transformed sinogram rows
fft2=scipy.interpolate.griddata(
    (srcy,srcx),
    sinogram_fft_rows.flatten(),
    (dsty,dstx),
    method='cubic',
    fill_value=0.0
    ).reshape((S,S))

plt.figure()
plt.suptitle("FFT2 space")
plt.subplot(221)
plt.title("Recon (real)")
plt.imshow(np.real(fft2),vmin=-V,vmax=V)
plt.subplot(222)
plt.title("Recon (imag)")
plt.imshow(np.imag(fft2),vmin=-V,vmax=V)

# Show 2D FFT of target, just for comparison
expected_fft2=scipy.fftpack.fftshift(
    scipy.fftpack.fft2(
        scipy.fftpack.ifftshift(
            target
            )
        )
    )

plt.subplot(223)
plt.title("Expected (real)")
plt.imshow(np.real(expected_fft2),vmin=-V,vmax=V)
plt.subplot(224)
plt.title("Expected (imag)")
plt.imshow(np.imag(expected_fft2),vmin=-V,vmax=V)

# Transform from 2D Fourier space back to a reconstruction of the target
recon=np.real(
    scipy.fftpack.fftshift(
        scipy.fftpack.ifft2(
            scipy.fftpack.ifftshift(fft2)
            )
        )
    )

plt.figure()
plt.title("Reconstruction")
plt.imshow(recon,vmin=0.0,vmax=1.0)
plt.gray()

plt.show()

2013 年 2 月 17 日更新:如果您有足够的兴趣涉足该领域,可以通过此海报的形式找到自学计划的更多输出此存储库中的代码主体也可能很有趣(尽管请注意代码不像上面那样精简)。我可能会尝试在某个时候将其重新打包为 IPython“笔记本”。

我不知道问题出在哪里,但是切片定理意味着这两种特殊情况应该是正确的:

fft2(target)[0] = fft(sinogram[270])
fft2(target)[:,0] = fft(sinogram[0])

因此,请按照您的代码并尝试找到这些停止等效的点,从正弦图向前工作,从生成的 2D FFT 向后工作。

这看起来不对:

In [47]: angle(expected_fft2[127:130,127:130])
Out[47]: 
array([[-0.07101021,  3.11754929,  0.02299738],
       [ 3.09818784,  0.        , -3.09818784],
       [-0.02299738, -3.11754929,  0.07101021]])

In [48]: fft2_ = fft2_real+1.0j*fft2_imag

In [49]: angle(fft2_[127:130,127:130])
Out[49]: 
array([[ 3.13164353, -3.11056554,  3.11906449],
       [ 3.11754929,  0.        , -3.11754929],
       [ 3.11519503,  3.11056604, -2.61816765]])

您生成的 2D FFT 从它应该是什么旋转了 90 度?

我建议使用幅度和相位而不是真实和想象,这样你就可以更容易地看到正在发生的事情:

在此处输入图像描述

(白角是 -inf from doing log(abs(0)),它们不是问题)

我相信第一个解决方案不起作用的实际理论原因来自于旋转是相对于图像的中心完成的,导致偏移量为[S/2, S/2],这意味着您的每一行都sinogram不是从0S,而是从-S/2S/2在您的示例中,偏移量实际上是offset = np.floor(S/2.). 请注意,这适用于S偶数或奇数,并且等效于您在代码中所做的S/2(尽管更明确可以避免问题,例如 whenS是 a float)。

我的猜测是,这种偏移在傅里叶变换 (FT) 中引入的相位延迟是您在第二条消息中所说的内容的起源:相位搞砸了,需要补偿这种偏移以便能够应用 Radon 变换的反演。人们需要进一步深入研究该理论,以便确定逆向按预期工作所需的确切条件。

为了补偿该偏移量,您可以像以前一样使用 fftshift (它将每行的中心放在开头,并且由于使用 DFT 实际上对应于计算 S 周期信号的傅立叶变换,因此您最终得到了正确的东西),或者在计算sinogramFT 时在复数傅里叶变换中显式补偿这种影响。在实践中,而不是:

sinogram_fft_rows=scipy.fftpack.fftshift(
    scipy.fftpack.fft(
        scipy.fftpack.ifftshift(
            sinogram,
            axes=1
            )
        ),
    axes=1
    )

您可以删除ifftshift并将每一行乘以一个校正向量:

offset = np.floor(S/2.)
sinogram_fft_rows = scipy.fftpack.fftshift(
    scipy.fftpack.fft(sinogram, axis=1)
    * (np.exp(1j * 2.* np.pi * np.arange(S) * offset / S)),
    axes=1)

这来自傅里叶变换属性,在考虑时移时(检查FT 维基百科页面以获取“移位定理”,并申请移位等于- offset- 因为我们将图像放回中心周围)。

同样,您可以将相同的策略应用于重建,并fftshift在两个维度上通过相位校正替换 ,但在另一个方向(补偿回):

recon=np.real(
    scipy.fftpack.ifft2(
        scipy.fftpack.ifftshift(fft2)
        *  np.outer(np.exp(- 1j * 2.* np.pi * np.arange(S) * offset / S),
                    np.exp(- 1j * 2.* np.pi * np.arange(S) * offset / S))
        )
    )

好吧,这并没有改善您的解决方案,而是为您问题的理论方面提供了新的思路。希望有帮助!

此外,我不太喜欢使用它,fftshift因为它往往会弄乱fft计算方式。然而,在这种情况下,您需要在插值之前将 FT 的中心放在图像的中心fft2(或者至少在设置时要小心r- 这样您就可以让它完全fftshift免费!),fftshift真的很方便那里。然而,我更喜欢将该函数用于可视化目的,而不是在计算“核心”内。:-)

最好的祝福,

让-路易

PS:您是否尝试过在不裁剪圆圈的情况下重建图像?在角落上产生非常酷的模糊效果,如果在 Instagram 等程序中拥有这样的功能会很不错,不是吗?