フーリエ法による断層再構成のこのコードの何が問題になっていますか?


19

私は最近、断層撮影再構成アルゴリズムで遊んでいます。私はすでに、FBP、ART、SIRT / SARTのような反復スキーム、および直線線形代数(遅い!)を使用した素晴らしい実装を持っています。 この質問はそれらのテクニックのどれについてもありません ; 「誰がそのようにするのか、代わりにいくつかのFBPコードがある」という形式の答えは、私が探しているものではありません。

このプログラムで次にやりたいことは、「セットを完了し」、いわゆる「フーリエ再構成法」を実装することでした。これについての私の理解は、1D FFTをサイノグラムの「露出」に適用し、それらを2Dフーリエ空間の放射状の「車輪のスポーク」として配置することです(これは、中央スライス定理から直接従うのが便利なことです) 、それらのポイントからその2D空間の通常のグリッドに内挿し、逆フーリエ変換して元のスキャンターゲットを復元できるようにする必要があります。

簡単に聞こえますが、元のターゲットのように見える再構築を得ることができなかったのです。

以下のPython(numpy / SciPy / Matplotlib)コードは、私がやろうとしていることを思いつくことができる最も簡潔な表現です。実行すると、次が表示されます。

図1:ターゲット fig1

図2:ターゲットのサイノグラム fig2

図3:FFT処理されたサイノグラムの行 fig3

図4:一番上の行は、フーリエ領域のサイノグラムの行から補間された2D FFT空間です。一番下の行は(比較のため)ターゲットの直接2D FFTです。これが、私が疑わしくなり始めた時点です。サイノグラムFFTから補間されたプロットは、ターゲットを直接2D-FFTすることによって作成されたプロットに似ていますが、まだ異なっています。 fig4

図5:図4の逆フーリエ変換。これが実際よりもターゲットとしてもう少し認識できることを望んでいました。 fig5

私が間違っていることは何ですか?フーリエ法の再構築に関する私の理解が根本的に不備なのか、コードにバグがあるのか​​はわかりません。

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()

1
これは、FFTを使用して逆ラドン変換を計算することと同等ですか?
endolith

... ここにはそのためのコードがあるので 、中央にあるべきものは端にあり、端にあるべきものは中央にあります、90度の位相シフトがあるべきではないのですか?
エンドリス

1
リンクしたコードは、フィルターバックプロジェクション(FBP)メソッド用です。これは同じ中央スライスの数学に基づいていますが、2Dフーリエドメインイメージを明示的に構築しようとすることはありません。FBPフィルターの低周波数の抑制は、中央の中央スライス「スポーク」の高密度を補うものとして見ることができます。私が実装しようとしているフーリエ再構成法では、これは補間するポイントの密度がより高いことを示しています。私は自由に私は少し使用される技術を実装しようとしている認め、それの限定された範囲は、文献にあります
timday

おっと、そうだね。これがCのバージョンです。私はそれを少し見て、いくつかのことを投稿しました。後で詳しく見ていきます。
エンドリス

回答:


15

OK、ついにこれをクラックしました。

基本的には、いくつかのfftshift/ ifftshiftsを適切な場所に配置することにトリックがかかったため、2Dフーリエ空間表現は激しく振動せず、正確に補間することは不可能であると運命づけられました。少なくともそれは私がそれを修正したと思うものです。フーリエ理論について私が持っている限られた理解のほとんどは、連続積分定式化に基づいており、常に離散領域とFFTを少し見つけます...風変わりです。

matlabコードはかなりわかりにくいと思いますが、少なくともこの種の環境でこの再構成アルゴリズムを合理的にコンパクトに表現できる自信を与えてくれたため、この実装を信用しなければなりません。

最初に結果を表示し、次にコーディングします。

図1:新しい、より複雑なターゲット。 図1

図2:ターゲットのサイノグラム(OK OK、これはラドン変換です)。 図2

図3:サイノグラムのFFT処理された行(DCを中心にプロット)。 図3

図4:2次元FFT空間に変換されたFFTサイノグラム(DC中央)。色は絶対値の関数です。 図4

図4a:サイノグラムデータの放射状の性質をよりよく示すために、2D FFT空間の中心を拡大します。 図4a

図5:上の行:放射状に配置されたFFTされたサイノグラムの行から補間された2D FFT空間。最下行:ターゲットを単純に2D FFTすることで予想される外観。
図5

図5a:図5のサブプロットの中央領域を拡大して、これらの外観が定性的にかなり一致していることを示します。 図5a

図6:酸テスト:補間されたFFTスペースの逆2D FFTはターゲットを回復します。レナは、私たちがたった今通したすべてにもかかわらず、まだかなりよく見えます(おそらく、2D FFT平面をかなり密にカバーするのに十分なサイノグラム「スポーク」があるため、露出角度の数を減らすと、物事が面白くなります。 )。 ここに画像の説明を入力してください

コードは次のとおりです。Debian / Wheezyのi7上の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-02-17:あなたがそのロットを歩き回るのに十分興味を持っているなら、それがその一部であった自習プログラムからのいくつかの出力はこのポスターの形で見つけることができます。このリポジトリのコード本体も興味深い場合があります(ただし、上記のコードほど合理化されていないことに注意してください)。ある時点で、それをIPythonの「ノートブック」として再パッケージ化することがあります。


3

問題がどこにあるのか正確にはわかりませんが、スライス定理は、これらの2つの特殊なケースが当てはまることを意味します。

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))であり、問​​題ではありません)


2

私は最初の解決策が機能しなかった理由は、実際の理論的な理由は、回転がオフセット誘導、画像の中心に関して行われているという事実から来ていると信じている[S/2, S/2]あなたの行のそれぞれがいることをどの手段、sinogramからのものではない0Sのではなく、から-S/2S/2。あなたの例では、オフセットは実際にoffset = np.floor(S/2.)です。これはS偶数または奇数で機能し、コードで行ったことと同等であることに注意してくださいS/2(ただし、より明示的にするSと問題が回避されますが、floatれますなどの場合)。

私の推測では、このシフトがフーリエ変換(FT)で導入する位相遅延は、2番目のメッセージで説明することの起源にあります。フェーズが台無しになり、そのシフトを補正する必要があるためです。ラドン変換の反転を適用します。逆が期待どおりに機能するために正確に必要なものを確実にするために、その理論をさらに掘り下げる必要があります。

そのオフセットを補正するには、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のようなプログラムでそのような機能を持っているといいですね。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.