Reconstructing SIMIND Data#

[1]:
import os
from pytomography.io.SPECT import simind
from pytomography.projectors import SPECTSystemMatrix
from pytomography.transforms import SPECTAttenuationTransform, SPECTPSFTransform
from pytomography.algorithms import OSEM
from torch import poisson
import matplotlib.pyplot as plt

Set the folder location for downloaded files (you will need to modify this to be the directory where you saved the files)

[2]:
path = '/disk1/pytomography_tutorial_data/simind_tutorial/'

Case 1: Single Projection File#

When running SIMIND simulations, it is commonly the case that organs, background, and lesions are simulated seperately and then added together in projection space after the fact. For now, lets consider the case of a single SIMIND file containing all regions of the body, we’ll move to seperate files later.

[3]:
photopeak_path = os.path.join(path, 'single_projections', 'body1t2ew6_tot_w2.hdr')
upperscatter_path = os.path.join(path, 'single_projections', 'body1t2ew6_tot_w3.hdr')
lowerscatter_path = os.path.join(path, 'single_projections', 'body1t2ew6_tot_w1.hdr')
attenuation_path = os.path.join(path, 'single_projections', 'body1.hct')

Open required metadata for reconstruction

[4]:
object_meta, proj_meta = simind.get_metadata(photopeak_path)

Open the projection data corresponding to the photopeak:

[5]:
photopeak = simind.get_projections(photopeak_path)

Open the estimate for the scatter using the get_scatter_from_TEW method:

[6]:
scatter = simind.get_scatter_from_TEW(
    headerfile_peak = photopeak_path,
    headerfile_lower = lowerscatter_path,
    headerfile_upper = upperscatter_path
)

The CT data is loaded in a similar fashion

[7]:
attenuation_map = simind.get_attenuation_map(attenuation_path)

It is good practice to ensure the CT and projections are aligned * Note: the sagittal view of the CT is the mirror of the projection at 90 degrees; this has to do with the coordinate system defined in the manual.

[8]:
attenuation_map_slice_coronal = attenuation_map [0,:,64,:].T.cpu()
photopeak_0 = photopeak[0][0].T.cpu()
attenuation_map_slice_sagittal = attenuation_map [0,64,:,:].T.cpu()
photopeak_90 = photopeak[0][30].T.cpu()
[9]:
plt.subplots(1, 4, figsize=(14,7))
plt.subplot(141)
plt.pcolormesh(attenuation_map_slice_coronal, cmap='Greys_r')
plt.title('CT; Coronal View')
plt.subplot(142)
plt.pcolormesh(photopeak_0, cmap='nipy_spectral')
plt.title(r'Spect; $\beta=0^{\circ}$')
plt.subplot(143)
plt.pcolormesh(attenuation_map_slice_sagittal, cmap='Greys_r')
plt.title('CT; Sagittal View')
plt.subplot(144)
plt.pcolormesh(photopeak_90, cmap='nipy_spectral')
plt.title(r'Spect; $\beta=90^{\circ}$')
plt.show()
../_images/notebooks_t_siminddata_16_0.png

Now we need to define all the required mappings to model \(g = Hf\)

Attenuation Modeling

  • This transform corresponds to a mapping \(A_i(\theta)\) as specified in the user manual and takes in an object/corresponding projection angle and returns a modified object

[10]:
att_transform = SPECTAttenuationTransform(attenuation_map)

PSF Modeling

  • This transform corresponds to a mapping \(A_i(\theta)\) as specified in the users manual. The PSF collimator information is obtained from the SIMIND header file

[11]:
psf_meta = simind.get_psfmeta_from_header(os.path.join(path, 'single_projections', 'body1t2ew6_tot_w2.hdr'))
psf_transform = SPECTPSFTransform(psf_meta)

Now we can build our system matrix \(H\):

  • The forward method system_matrix.forward(f) corresponds to \(Hf\) where \(H = \sum_{\theta} P(\theta)A_1(\theta)A_2(\theta) \otimes \hat{\theta}\)

  • The backward method system_matrix.backward(g) corresponds to \(H^T g\)

  • The optional argument n_parallel can be used to speed up forward and back projection by allowing multiple projection angles in a subset to be operated on in parallel (note: this does not affect the final prediction). It defaults to 1 if not given. Doubling this parameter does not necessarily reduce reconstruction time by half, as not all transforms take full advantage of parallel operations.

[12]:
system_matrix = SPECTSystemMatrix(
    obj2obj_transforms = [att_transform,psf_transform],
    proj2proj_transforms = [],
    object_meta = object_meta,
    proj_meta = proj_meta,
    n_parallel=2)

The system matrix is then used to create the reconstruction algorithm, which in this case is OSEM:

[13]:
reconstruction_algorithm = OSEM(
    projections = photopeak,
    system_matrix = system_matrix,
    scatter = scatter)

Get the reconstructed object

[14]:
reconstructed_object = reconstruction_algorithm(n_iters=10, n_subsets=8)

Plot the reconstructed object next to the CT. * The reconstructed object has units of counts, and would need to be adjusted by a proportionality factor if one wants to obtain units of MBq

[15]:
plt.subplots(1,2,figsize=(8,6))
plt.subplot(121)
plt.pcolormesh(reconstructed_object[0].cpu()[:,70].T, cmap='nipy_spectral')
plt.colorbar()
plt.axis('off')
plt.title('Reconstructed Object')
plt.subplot(122)
plt.pcolormesh(attenuation_map.cpu()[0][:,70].T, cmap='Greys_r')
plt.colorbar()
plt.axis('off')
plt.title('$\mu$ (CT scan)')
plt.show()
../_images/notebooks_t_siminddata_29_0.png

Case 2: Multiple Regions#

When running simulations with SIMIND, it is standard to run different regions of the body seperately and then combine them together in projection space after the fact. This enables one to create arbtrary organ activities using the same set of simulated data. A few noteworthy things

  • SIMIND projection data is typically given in units of CPS/MBq where CPS=counts/second. Real life SPECT data consists of units of counts. In order to simulate a realistic scenario, therefore, we need to multiply the projections by some value in MBq, and some value in seconds. The value in MBq corresponds to the total activity of the organ/region you’ve simulated. The value in seconds corresponds to the total projection time.

We’ll start by opening up SIMIND projections corresponding to 8 distinct regions of the body, each with activities representative of a true clinical case

[16]:
organs = ['bkg', 'liver', 'l_lung', 'r_lung', 'l_kidney', 'r_kidney','salivary', 'bladder']
activities = [2500, 450, 7, 7, 100, 100, 20, 90] # MBq
headerfiles = [os.path.join(path, 'multi_projections', organ, 'photopeak.h00') for organ in organs]
headerfiles_lower = [os.path.join(path, 'multi_projections', organ, 'lowerscatter.h00') for organ in organs]
headerfiles_upper = [os.path.join(path, 'multi_projections', organ, 'upperscatter.h00') for organ in organs]

Open the projection/scatter data

[17]:
object_meta, proj_meta = simind.get_metadata(headerfiles[0]) #assumes the same for all
photopeak = simind.combine_projection_data(headerfiles, activities)
scatter = simind.combine_scatter_data_TEW(headerfiles, headerfiles_lower, headerfiles_upper, activities)

Note that our weights were in units of MBq, so our projection data currently has units of counts/second.

[18]:
plt.subplots(1, 2, figsize=(6,6))
plt.subplot(121)
plt.pcolormesh(photopeak[0,0].cpu().T, cmap='nipy_spectral')
plt.axis('off')
plt.colorbar(label='Counts/Second')
plt.subplot(122)
plt.pcolormesh(scatter[0,0].cpu().T, cmap='nipy_spectral')
plt.axis('off')
plt.colorbar(label='Counts/Second')
[18]:
<matplotlib.colorbar.Colorbar at 0x7f7fc03024c0>
../_images/notebooks_t_siminddata_36_1.png

Now we need to decide how long the scan was taken for. We’ll assume each projection was taken for 15 seconds

[19]:
dT = 15 #s
photopeak *= dT
scatter *= dT
[20]:
plt.subplots(1, 2, figsize=(6,6))
plt.subplot(121)
plt.pcolormesh(photopeak[0,0].cpu().T, cmap='nipy_spectral')
plt.axis('off')
plt.colorbar(label='Counts')
plt.subplot(122)
plt.pcolormesh(scatter[0,0].cpu().T, cmap='nipy_spectral')
plt.axis('off')
plt.colorbar(label='Counts')
[20]:
<matplotlib.colorbar.Colorbar at 0x7f7fc00777f0>
../_images/notebooks_t_siminddata_39_1.png

In realtity, SPECT projection data is noisy. Provided enough events are ran during the SIMIND simulation, the SIMIND data is essentially noise-free. We need to add Poisson noise if we are to simulate a realistic clinical imaging scenario

[21]:
photopeak_poisson = poisson(photopeak)
scatter_poisson = poisson(scatter)

Now can look at our realistic noisy data:

[22]:
plt.subplots(1, 2, figsize=(6,6))
plt.subplot(121)
plt.pcolormesh(photopeak_poisson[0,0].cpu().T, cmap='nipy_spectral')
plt.axis('off')
plt.colorbar(label='Counts')
plt.subplot(122)
plt.pcolormesh(scatter_poisson[0,0].cpu().T, cmap='nipy_spectral')
plt.axis('off')
plt.colorbar(label='Counts')
[22]:
<matplotlib.colorbar.Colorbar at 0x7f7fc0318e50>
../_images/notebooks_t_siminddata_43_1.png

Now the projection and scatter data is representative of a realistic clinical scenario. Now we reconstruct as we did earlier.

[23]:
attenuation_map = simind.get_attenuation_map(os.path.join(path, 'multi_projections', 'mu208.hct'))
att_transform = SPECTAttenuationTransform(attenuation_map)
psf_meta = simind.get_psfmeta_from_header(headerfiles[0])
psf_transform = SPECTPSFTransform(psf_meta)
system_matrix = SPECTSystemMatrix(
    obj2obj_transforms = [att_transform,psf_transform],
    proj2proj_transforms = [],
    object_meta = object_meta,
    proj_meta = proj_meta)
reconstruction_algorithm = OSEM(
    projections = photopeak_poisson,
    system_matrix = system_matrix,
    scatter = scatter_poisson)

Now we reconstruct:

[24]:
reconstructed_object = reconstruction_algorithm(n_iters=10, n_subsets=8)

Lets see the reconstructed object

[25]:
plt.subplots(1,2,figsize=(6,6))
plt.subplot(121)
plt.pcolormesh(reconstructed_object[0].cpu()[:,70].T, cmap='nipy_spectral')
plt.colorbar()
plt.axis('off')
plt.title('Reconstructed Object')
plt.subplot(122)
plt.pcolormesh(attenuation_map.cpu()[0][:,70].T, cmap='Greys_r')
plt.colorbar()
plt.axis('off')
plt.title('$\mu$ (CT scan)')
plt.show()
../_images/notebooks_t_siminddata_49_0.png