KEM#
Preliminaries#
Import required packages
[1]:
from __future__ import annotations
import os
import matplotlib.pyplot as plt
import torch
from pytomography.io.SPECT import simind
from pytomography.projectors import SPECTSystemMatrix
from pytomography.transforms import SPECTAttenuationTransform, SPECTPSFTransform
from pytomography.algorithms import OSEM, KEM
from pytomography.transforms import KEMTransform
Modify the following path to the directory where you saved the tutorial data:
[2]:
path = '/disk1/pytomography_tutorial_data/simind_tutorial/'
The first cell of code is borrowed from the SPECT: Reconstructing SIMIND Data
tutorial in the multiple regions case. For a more comprehensive description about the code below, please see that tutorial.
[3]:
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]
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)
dT = 5 #s
photopeak = torch.poisson(photopeak * dT)
scatter = torch.poisson(scatter * dT)
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,
n_parallel=2)
For reference, let’s reconstruct an image using OSEM:
[4]:
reconstruction_algorithm = OSEM(
projections = photopeak,
system_matrix = system_matrix,
scatter = scatter)
recon_osem = reconstruction_algorithm(n_iters=10, n_subsets=8)
The kernelized expectation maximum algorithm can be written as
where the reconstructed object estimate is given by \(\hat{f}^{n,m+1} = K \hat{\alpha}^{n,m+1}\). This is nearly identical to OSEM, but a matrix \(K\) is now involved. Thus we need to do two things in PyTomography
Obtain the matrix \(K\)
Using the matrix \(K\) in the
KEM
reconstruction algorithm.
The matrix \(K\) is a square matrix with components
where
\(v\) corresponds to a “support object” (such as a CT scan, MRI, PET). These are denoted as
support_objects
\(S_v\) corresponds to the set of all support objects used
\(k_v\) is the support kernel for support image \(v\). These are denoted as
support_kernels
.\(x_i\) is the position of voxel \(i\)
\(k_x\) is the kernel that weights based on positional differences. This is denoted as
distance_kernel
.… are additional parameters for each kernel. They are denoted as
distance_kernel_params
orsupport_kernel_params
/
In the KEMTransform
, one gives a list of all the support objects via the support_objects
argument. By default, the support kernel for each support object will be
In this default kernel, there is one additional required argument: \(\sigma\). The values of \(\sigma\) can be given using the support_kernels_params
argument. The argument is a list of lists, where each internal list corresponds to all the additional required parameters for each kernel function. In the example below, one support object is used, and the function takes in one additional argument, so specifying \(\sigma=0.005\) cm \(^{-1}\) requires giving
support_kernels_params=[[0.005]]
. Since there is a single distance kernel, any additional parameters to that kernel are given by a single list to the distance_kernel_params
argument.
Alternative kernels can be specified using the support_kernels
and distance_kernel
arguments of the function; in this tutorial, we make use of the default ones:
Note: You may have to set
kernel_on_gpu=False
if your computer runs out of GPU memory; this means the kernel gets stored in regular RAM. Only portions of it get put on GPU during run time, but this results in a much longer run time (3x as long)
[5]:
kem_transform = KEMTransform(
support_objects=[attenuation_map],
support_kernels_params=[[0.005]],
distance_kernel_params=[0.5],
kernel_on_gpu=True
)
kem_transform_top40 = KEMTransform(
support_objects=[attenuation_map],
support_kernels_params=[[0.005]],
distance_kernel_params=[0.5],
top_N = 40,
kernel_on_gpu=True
)
We initialize the reconstruction algorithm using the KEM
class, and give the corresponding kem_transform
[6]:
reconstruction_algorithm_KEM = KEM(
projections = photopeak,
system_matrix = system_matrix,
kem_transform = kem_transform,
scatter = scatter)
reconstruction_algorithm_KEM_top40 = KEM(
projections = photopeak,
system_matrix = system_matrix,
kem_transform = kem_transform_top40,
scatter = scatter)
/data/anaconda/envs/pytomo_install_test/lib/python3.11/site-packages/torch/functional.py:504: UserWarning: torch.meshgrid: in an upcoming release, it will be required to pass the indexing argument. (Triggered internally at ../aten/src/ATen/native/TensorShape.cpp:3526.)
return _VF.meshgrid(tensors, **kwargs) # type: ignore[attr-defined]
Then we can reconstruct:
[7]:
recon_kem = reconstruction_algorithm_KEM(n_iters=20, n_subsets=4)
recon_kem_top40 = reconstruction_algorithm_KEM_top40(n_iters=20, n_subsets=4)
And compare to OSEM:
[8]:
fig, ax = plt.subplots(1,3,figsize=(7.5,5))
plt.subplot(131)
im = plt.pcolormesh(recon_osem[0].cpu()[:,70].T, cmap='magma')
plt.axis('off')
plt.title('OSEM')
plt.subplot(132)
plt.pcolormesh(recon_kem.cpu()[0][:,70].T, cmap='magma')
plt.axis('off')
plt.title('KEM (Full)')
plt.subplot(133)
plt.pcolormesh(recon_kem_top40.cpu()[0][:,70].T, cmap='magma')
plt.axis('off')
plt.title('KEM (Top 40)')
fig.colorbar(im, ax=ax, location='right', label='Counts', extend='max')
plt.show()
