Cosa c'è di sbagliato in questo codice per la ricostruzione tomografica con il metodo di Fourier?


19

Di recente ho giocato con algoritmi di ricostruzione tomografica. Ho già buone implementazioni funzionanti di FBP, ART, uno schema iterativo simile a SIRT / SART e persino usando un'algebra lineare diritta (lenta!). Questa domanda non riguarda nessuna di quelle tecniche ; le risposte del modulo "perché qualcuno dovrebbe farlo in questo modo, ecco un po 'di codice FBP invece" non sono quello che sto cercando.

La prossima cosa che volevo fare con questo programma era " completare il set " e implementare il cosiddetto " metodo di ricostruzione di Fourier ". La mia comprensione di questo è fondamentalmente che si applica un FFT 1D alle "esposizioni" del sinogramma, si organizzano quelli come "raggi di una ruota" radiali nello spazio di Fourier 2D (che questa è una cosa utile da fare segue direttamente dal teorema della fetta centrale) , interpolare da quei punti a una griglia regolare in quello spazio 2D, e quindi dovrebbe essere possibile invertire la trasformata di Fourier per recuperare l'obiettivo di scansione originale.

Sembra semplice, ma non ho avuto molta fortuna a ottenere ricostruzioni che assomigliano al bersaglio originale.

Il codice Python (numpy / SciPy / Matplotlib) di seguito è l'espressione più concisa che potrei trovare di ciò che sto cercando di fare. Quando eseguito, visualizza quanto segue:

Figura 1: l'obiettivo Fig. 1

Figura 2: un sinogramma del bersaglio fig2

Figura 3: le righe del sinogramma FFT fig3

Figura 4: la riga superiore è lo spazio FFT 2D interpolato dalle righe del sinogramma del dominio Fourier; la riga inferiore è (a fini di confronto) la FFT 2D diretta del target. Questo è il punto in cui sto iniziando a diventare sospettoso; le trame interpolate dai sinogrammi FFT sembrano simili alle trame create direttamente dal 2D-FFT del bersaglio ... eppure diverse. fig4

Figura 5: la trasformata inversa di Fourier di Figura 4. Avrei sperato che questo sarebbe un po 'più riconoscibile come bersaglio di quanto non sia in realtà. fig5

Qualche idea su cosa sto facendo di sbagliato? Non sono sicuro che la mia comprensione della ricostruzione del metodo di Fourier sia fondamentalmente imperfetta o che ci sia qualche bug nel mio codice.

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


... perché c'è del codice per questo qui Le cose che dovrebbero essere al centro sono ai bordi e le cose che dovrebbero essere ai bordi sono al centro, come se ci fosse uno sfasamento di 90 gradi da qualche parte che non dovrebbe esserci?
endolith

1
Il codice collegato è per il metodo di retroproiezione filtrata (FBP). Che si basa sulla stessa matematica della sezione centrale, ma non tenta mai esplicitamente di costruire l'immagine del dominio Fourier 2D. Puoi vedere la soppressione del filtro FBP delle basse frequenze come compensazione per una maggiore densità dei "raggi" della sezione centrale al centro. Nel metodo di ricostruzione di Fourier che sto tentando di implementare, questo si manifesta solo come una maggiore densità di punti da cui interpolare. Ammetto liberamente che sto cercando di implementare una tecnica poco usata e la cui copertura è limitata in letteratura,
giorno

Oops, sì, hai ragione. Ecco una versione C . Ci ho guardato un po 'e ho pubblicato alcune cose. Guarderò più tardi.
endolith

Risposte:


15

OK, l'ho risolto finalmente.

Il trucco fondamentalmente si riduceva a mettere alcuni fftshift/ ifftshifts nel posto giusto, quindi la rappresentazione spaziale 2D di Fourier non era selvaggiamente oscillatoria e destinata a essere impossibile da interpolare con precisione. Almeno questo è quello che penso risolto. La maggior parte della comprensione limitata della teoria di Fourier si basa sulla formulazione integrale continua e trovo sempre il dominio discreto e le FFT un po '... eccentriche.

Mentre trovo il codice matlab piuttosto criptico, devo dare credito a questa implementazione per almeno darmi la sicurezza che questo algoritmo di ricostruzione possa essere espresso in modo abbastanza compatto in questo tipo di ambiente.

Per prima cosa mostrerò i risultati, quindi codice:

Figura 1: un nuovo obiettivo più complesso. Fig. 1

Figura 2: il sinogramma (OK OK, è la trasformazione del Radon) del bersaglio. Fig2

Figura 3: le file del sinogramma con editazione FFT (tracciate con DC al centro). fig3

Figura 4: il sinogramma FFT-ed trasformato in spazio FFT 2D (DC al centro). Il colore è una funzione di valore assoluto. Fig4

Figura 4a: ingrandisci al centro dello spazio FFT 2D solo per mostrare meglio la natura radiale dei dati del sinogramma. Fig4a

Figura 5: Riga superiore: lo spazio FFT 2D interpolato dalle righe sinogramma FFT disposte radialmente. Riga in basso: l'aspetto previsto dal semplice FFT 2D sul target.
fig5

Figura 5a: ingrandisci la regione centrale delle sottotrame in Fig5 per mostrare che questi sembrano essere abbastanza d'accordo qualitativamente. Fig5a

Figura 6: Test dell'acido: la FFT 2D inversa dello spazio FFT interpolato recupera il bersaglio. Lena sembra ancora abbastanza buona nonostante tutto ciò che le abbiamo appena fatto passare (probabilmente perché ci sono abbastanza "raggi" sinogrammi per coprire il piano FFT 2D abbastanza densamente; le cose diventano interessanti se riduci il numero di angoli di esposizione, quindi non è più vero ). inserisci qui la descrizione dell'immagine

Ecco il codice; riporta le trame in meno di 15 secondi sullo SciPy a 64 bit di Debian / Wheezy su un i7.

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

Aggiornamento 17-02-2013: Se sei stato abbastanza interessato a guadare quel lotto, puoi trovare un po 'più di output dal programma di studio autonomo di cui faceva parte sotto forma di questo poster . Anche il corpo del codice in questo repository potrebbe essere di interesse (sebbene si noti che il codice non è così semplificato come quello sopra). Potrei provare a riconfezionarlo come un "notebook" IPython ad un certo punto.


3

Non so esattamente dove sia il problema, ma il teorema della fetta indica che questi due casi speciali dovrebbero essere veri:

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

Quindi segui il tuo codice e prova a trovare il punto in cui questi smettono di essere equivalenti, lavorando in avanti dal sinogramma e indietro dalla FFT 2D generata.

Questo non sembra giusto:

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

La FFT 2D che stai generando viene ruotata di 90 gradi rispetto a quella che dovrebbe essere?

Suggerirei di lavorare con magnitudo e fase piuttosto che reale e immaginario, in modo da poter vedere più facilmente cosa sta succedendo:

inserisci qui la descrizione dell'immagine

(Gli angoli bianchi sono ininfluenti log(abs(0)), non sono un problema)


2

Credo che il vero motivo teorico per cui la prima soluzione non ha funzionato provenga dal fatto che le rotazioni sono fatte rispetto ai centri delle immagini, inducendo un offset di [S/2, S/2], il che significa che ciascuna delle righe del tuo sinogramnon è da 0a S, ma piuttosto da -S/2a S/2. Nel tuo esempio, l'offset è effettivamente offset = np.floor(S/2.). Nota che questo funziona in modo Spari o dispari ed equivale a quello che hai fatto nel tuo codice S/2(anche se essere più esplicito evita problemi, quando Sè un float, per esempio).

La mia ipotesi è che i ritardi di fase introdotti da questo cambiamento nella trasformata di Fourier (FT) siano all'origine di ciò di cui parli nel tuo secondo messaggio: le fasi sono incasinate e bisogna essere in grado di compensare tale spostamento per poter applica l'inversione della trasformata di Radon. Bisogna scavare di più in quella teoria per essere sicuri di ciò che è esattamente necessario affinché l'inverso funzioni come previsto.

Per compensare quell'offset, puoi usare fftshift come hai fatto tu (il che pone il centro di ogni riga all'inizio, e poiché l'utilizzo di DFT corrisponde effettivamente al calcolo della trasformata di Fourier di un segnale periodico S, finisci con le cose giuste ), o compensare esplicitamente questo effetto nella complessa trasformata di Fourier, quando si calcola l' sinogramFT. In pratica, invece di:

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

puoi rimuovere ifftshifte moltiplicare ogni riga per un vettore correttivo:

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)

Questo deriva dalle proprietà della trasformata di Fourier, quando si considera uno spostamento temporale (controllare la pagina FT di Wikipedia per il "teorema dello spostamento" e applicare lo spostamento uguale a - offset- perché rimettiamo l'immagine al centro).

Allo stesso modo, è possibile applicare la stessa strategia alla ricostruzione e sostituirla fftshiftcon la correzione delle fasi, in entrambe le dimensioni, ma nella direzione opposta (compensazione della schiena):

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

Bene, questo non migliora la tua soluzione, ma piuttosto getta un'altra luce sugli aspetti teorici della tua domanda. Spero possa aiutare!

Inoltre, non mi piace molto usarlo fftshiftperché tende a fare casino con il modo in cui fftviene calcolato. In questo caso, tuttavia, è necessario mettere il centro dell'FT al centro dell'immagine prima dell'interpolazione per ottenere fft2(o almeno fare attenzione durante l'impostazione r- in modo da renderlo completamente libero fftshift!) E fftshiftdavvero utile Là. Preferisco comunque mantenere l'uso di quella funzione per scopi di visualizzazione e non all'interno del "core" di calcolo. :-)

I migliori saluti,

Jean-Louis

PS: hai provato a ricostruire l'immagine senza ritagliare il cerchio? che dà un effetto sfocato piuttosto interessante sugli angoli, sarebbe bello avere una tale funzione in programmi come Instagram, non è vero?

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.