Що не в цьому коді для томографічної реконструкції методом Фур'є?


19

Я нещодавно грав з алгоритмами томографічної реконструкції. У мене вже є приємні робочі реалізації FBP, ART, SIRT / SART-подібної ітеративної схеми і навіть з використанням прямолінійної алгебри (повільно!). Це питання не стосується жодної з цих методик ; відповіді форми "чому хто-небудь зробив би це так, ось тут якийсь код FBP" - це не те, що я шукаю.

Наступне, що я хотів зробити з цією програмою - це " завершити набір " та реалізувати так званий " метод реконструкції Фур'є ". Моє розуміння цього в основному полягає в тому, що ви застосовуєте 1D FFT до синограмних "експозицій", розташовуйте їх як радіальні "спиці колеса" в просторі 2D Фур'є (що це корисна річ слід безпосередньо з теореми центрального зрізу) , інтерполіруйте з цих точок у звичайну сітку у цьому двовимірному просторі, і тоді слід отримати можливість зворотного перетворення Фур'є для відновлення вихідної цілі сканування.

Звучить просто, але мені не пощастило отримати будь-які реконструкції, схожі на оригінальну ціль.

Код Python (numpy / SciPy / Matplotlib) нижче - це найкоротший вираз, який я міг би придумати, що я намагаюся зробити. Під час запуску він відображає наступне:

Фіг.1: ціль фіг1

Малюнок 2: синограма цілі фіг2

Малюнок 3: рядки синограми з FFT-ed фіг3

Фіг.4: верхній ряд - 2D простір FFT, інтерпольований з рядків синограми домену Фур'є; нижній ряд - це (для порівняння) прямий 2D FFT цілі. Це момент, коли я починаю ставати підозрілим; Діаграми, інтерпольовані із синограмних БПП, схожі на графіки, зроблені безпосередньо за допомогою 2D-FFTing цілі ... та все ж різними. фіг.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()


... тому що тут є код для цього. Речі, які повинні бути в центрі, знаходяться в краях, а речі, які повинні бути на краях, знаходяться в центрі, як, наприклад, дефазний зсув 90 градусів десь не повинен бути?
ендоліт

1
Код, який ви пов’язали, призначений для методу відфільтрованої задньої проекції (FBP). Він заснований на одній і тій же математиці центрального фрагмента, але ніколи прямо не намагається побудувати зображення домену 2D Фур'є. Ви можете розглядати придушення фільтрів FBP на низьких частотах як компенсацію за більш високу щільність «спиць» центрального зрізу посередині. У методі реконструкції Фур'є, який я намагаюся реалізувати, це просто проявляється як більша щільність точок для інтерполяції. Я вільно визнаю, що намагаюся реалізувати трохи використану техніку, і в літературі її висвітлюється обмежено,
timday

На жаль, ви маєте рацію. Ось версія в C . Я трохи переглянув це і розмістив деякі речі. Подивлюся пізніше.
ендоліт

Відповіді:


15

Добре, я зламав це остаточно.

Трюк, в основному, зводився до того, щоб поставити деякі fftshift/ ifftshiftс у потрібне місце, щоб представлення 2D-простору Фур'є не було дико коливальним і приречене неможливо точно інтерполювати. Принаймні, це я вважаю, що це виправило. Більшість того, що я розумію в теорії Фур'є, базується на безперервному інтегральному формулюванні, і я завжди знаходжу дискретний домен і FFT трохи ... химерним.

Хоча я вважаю, що код matlab є досить виразним, я маю заслужити цю реалізацію принаймні, що надає мені впевненості, що цей алгоритм відновлення може бути виражений досить компактно в такому середовищі.

Спочатку я покажу результати, потім код:

Малюнок 1: нова, більш складна ціль. Фіг1

Малюнок 2: синограма (добре, це перетворення Радона) цілі. Фіг2

Малюнок 3: рядки синограми (накреслені з постійним струмом у центрі), рядки FFT-ed. Фіг.3

Малюнок 4: Синограма, що перебуває у FFT, трансформується у 2D простір FFT (DC в центрі). Колір - це функція абсолютного значення. Фіг.4

Малюнок 4а: Збільшити центр 2D простору FFT просто для кращого відображення радіального характеру даних синограми. Fig4a

Малюнок 5: Верхній ряд: 2D простір FFT, інтерпольований з радіально розташованих рядків синограми FFT. Нижній ряд: очікувана поява від просто 2D FFT-ї цілі.
Фіг.5

Малюнок 5a: Збільшити розмір центральної області підплотів на фіг.5, щоб показати, що вони виглядають в хорошій якості. Fig5a

Малюнок 6: Тест на кислоту: обернена 2D FFT інтерпольованого простору FFT відновлює ціль. Лена все ще виглядає досить добре, незважаючи на все, що ми тільки що поставили їй (можливо, тому, що є достатньо синограмних "спиць", щоб досить щільно покрити площину 2D FFT; все стає цікавим, якщо зменшити кількість кутів опромінення, тому це вже не відповідає дійсності ). введіть тут опис зображення

Ось код; відображає сюжети менш ніж за 15 секунд на 64-бітній SciPy на Debian / Wheezy на 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()

Оновлення 2013-02-17: Якщо ви були достатньо зацікавлені, щоб пройтись через цю партію, ще кілька результатів програми самонавчання, частиною якої вона була, можна знайти у формі цього плаката . Тіло коду в цьому сховищі також може представляти інтерес (хоча зауважте, що код не настільки настільки спрощений, як вище). Я можу спробувати перепакувати його як «ноутбук» IPython в якийсь момент.


3

Я точно не знаю, де проблема, але теорема зрізу означає, що ці два особливі випадки повинні бути правдивими:

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 градусів від того, що має бути?

Я б запропонував працювати з величиною та фазою, а не реальною та уявною, щоб ви могли легше бачити, що відбувається:

введіть тут опис зображення

(Білі кути - якщо log(abs(0))вони не роблять , це не проблема)


2

Я вважаю , що фактична теоретичній причина , чому першим рішення не робота виходить з того , що сівозміна зроблені щодо центрів зображень, викликаючи зсув [S/2, S/2], що означає , що кожен з рядків з вашого sinogramнемає від 0до S, а точніше від -S/2до S/2. У вашому прикладі компенсація насправді offset = np.floor(S/2.). Зверніть увагу, що це працює для Sпарного або непарного і є еквівалентом тому, що ви робили у своєму коді S/2(хоча більш чітко уникає проблем, наприклад, коли Sце є a float).

Я здогадуюсь, що затримка фаз, яку цей зсув вводить у перетворення Фур'є (FT), є початком того, про що ви говорите у своєму другому повідомленні: фази переплутані, і потрібно компенсувати цей зсув, щоб мати можливість застосувати інверсію перетворення Радона. Потрібно більше зануритися в цю теорію, щоб бути впевненим у тому, що саме потрібно для того, щоб обернений працював так, як очікувалося.

Щоб компенсувати це зміщення, ви можете скористатись зрушенням (як це робиться в центрі кожного ряду на початку, а оскільки використання 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, чи не так?

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.