Note
Go to the end to download the full example code.
TOF listmode MLEM with projection data
This example demonstrates the use of the listmode MLEM algorithm to minimize the negative Poisson log-likelihood function.
subject to
using the listmode linear forward model
and data stored in listmode format (event by event).
Tip
parallelproj is python array API compatible meaning it supports different
array backends (e.g. numpy, cupy, torch, …) and devices (CPU or GPU).
Choose your preferred array API xp and device dev below.
32 from __future__ import annotations
33 from parallelproj import Array
34
35 import array_api_compat.numpy as xp
36
37 # import array_api_compat.cupy as xp
38 # import array_api_compat.torch as xp
39
40 import parallelproj
41 from array_api_compat import to_device, size
42 import array_api_compat.numpy as np
43 import matplotlib.pyplot as plt
44 import matplotlib.animation as animation
45 from copy import copy
46
47 # choose a device (CPU or CUDA GPU)
48 if "numpy" in xp.__name__:
49 # using numpy, device must be cpu
50 dev = "cpu"
51 elif "cupy" in xp.__name__:
52 # using cupy, only cuda devices are possible
53 dev = xp.cuda.Device(0)
54 elif "torch" in xp.__name__:
55 # using torch valid choices are 'cpu' or 'cuda'
56 if parallelproj.cuda_present:
57 dev = "cuda"
58 else:
59 dev = "cpu"
Simulation of PET data in sinogram space
In this example, we use simulated listmode data for which we first need to setup a sinogram forward model to create a noise-free and noisy emission sinogram that can be converted to listmode data.
Sinogram forward model setup
We setup a linear forward operator \(A\) consisting of an image-based resolution model, a non-TOF PET projector and an attenuation model
78 num_rings = 5
79 scanner = parallelproj.RegularPolygonPETScannerGeometry(
80 xp,
81 dev,
82 radius=65.0,
83 num_sides=12,
84 num_lor_endpoints_per_side=15,
85 lor_spacing=2.3,
86 ring_positions=xp.linspace(-10, 10, num_rings),
87 symmetry_axis=2,
88 )
89
90 # setup the LOR descriptor that defines the sinogram
91
92 img_shape = (40, 40, 8)
93 voxel_size = (2.0, 2.0, 2.0)
94
95 lor_desc = parallelproj.RegularPolygonPETLORDescriptor(
96 scanner,
97 radial_trim=10,
98 max_ring_difference=2,
99 sinogram_order=parallelproj.SinogramSpatialAxisOrder.RVP,
100 )
101
102 proj = parallelproj.RegularPolygonPETProjector(
103 lor_desc, img_shape=img_shape, voxel_size=voxel_size
104 )
105
106 # setup a simple test image containing a few "hot rods"
107 x_true = xp.ones(proj.in_shape, device=dev, dtype=xp.float32)
108 c0 = proj.in_shape[0] // 2
109 c1 = proj.in_shape[1] // 2
110 x_true[(c0 - 2) : (c0 + 2), (c1 - 2) : (c1 + 2), :] = 5.0
111 x_true[4, c1, 2:] = 5.0
112 x_true[c0, 4, :-2] = 5.0
113
114 x_true[:2, :, :] = 0
115 x_true[-2:, :, :] = 0
116 x_true[:, :2, :] = 0
117 x_true[:, -2:, :] = 0
Attenuation image and sinogram setup
124 # setup an attenuation image
125 x_att = 0.01 * xp.astype(x_true > 0, xp.float32)
126 # calculate the attenuation sinogram
127 att_sino = xp.exp(-proj(x_att))
Complete PET forward model setup
We combine an image-based resolution model, a non-TOF or TOF PET projector and an attenuation model into a single linear operator.
137 # enable TOF - comment if you want to run non-TOF
138 proj.tof_parameters = parallelproj.TOFParameters(
139 num_tofbins=13, tofbin_width=12.0, sigma_tof=12.0
140 )
141
142 # setup the attenuation multiplication operator which is different
143 # for TOF and non-TOF since the attenuation sinogram is always non-TOF
144 if proj.tof:
145 att_op = parallelproj.TOFNonTOFElementwiseMultiplicationOperator(
146 proj.out_shape, att_sino
147 )
148 else:
149 att_op = parallelproj.ElementwiseMultiplicationOperator(att_sino)
150
151 res_model = parallelproj.GaussianFilterOperator(
152 proj.in_shape, sigma=4.5 / (2.35 * proj.voxel_size)
153 )
154
155 # compose all 3 operators into a single linear operator
156 pet_lin_op = parallelproj.CompositeLinearOperator((att_op, proj, res_model))
Simulation of sinogram projection data
We setup an arbitrary ground truth \(x_{true}\) and simulate noise-free and noisy data \(y\) by adding Poisson noise.
165 # simulated noise-free data
166 noise_free_data = pet_lin_op(x_true)
167
168 # generate a contant contamination sinogram
169 contamination = xp.full(
170 noise_free_data.shape,
171 0.5 * float(xp.mean(noise_free_data)),
172 device=dev,
173 dtype=xp.float32,
174 )
175
176 noise_free_data += contamination
177
178 # add Poisson noise
179 np.random.seed(1)
180 y = xp.asarray(
181 np.random.poisson(parallelproj.to_numpy_array(noise_free_data)),
182 device=dev,
183 dtype=xp.int16,
184 )
Conversion of the emission sinogram to listmode
Using RegularPolygonPETProjector.convert_sinogram_to_listmode() we can convert an
integer non-TOF or TOF sinogram to an event list for listmode processing.
Note: The create event list is sorted and should be shuffled running LM-MLEM.
195 event_start_coords, event_end_coords, event_tofbins = proj.convert_sinogram_to_listmode(
196 y
197 )
Setup of the LM projector and LM forward model
203 lm_proj = parallelproj.ListmodePETProjector(
204 event_start_coords,
205 event_end_coords,
206 proj.in_shape,
207 proj.voxel_size,
208 proj.img_origin,
209 )
210
211 # recalculate the attenuation factor for all LM events
212 # this needs to be a non-TOF projection
213 att_list = xp.exp(-lm_proj(x_att))
214 lm_att_op = parallelproj.ElementwiseMultiplicationOperator(att_list)
215
216 # enable TOF in the LM projector
217 lm_proj.tof_parameters = proj.tof_parameters
218 if proj.tof:
219 lm_proj.event_tofbins = event_tofbins
220 lm_proj.tof = proj.tof
221
222 # create the contamination list
223 contamination_list = xp.full(
224 event_start_coords.shape[0],
225 float(xp.reshape(contamination, (size(contamination),))[0]),
226 device=dev,
227 dtype=xp.float32,
228 )
229
230 lm_pet_lin_op = parallelproj.CompositeLinearOperator((lm_att_op, lm_proj, res_model))
LM MLEM reconstruction
The EM update that can be used in LM-MLEM is given by
to calculate the minimizer of \(f(x)\) iteratively.
244 def lm_em_update(
245 x_cur: Array,
246 op: parallelproj.LinearOperator,
247 s: Array,
248 adjoint_ones: Array,
249 ) -> Array:
250 """LM EM update
251
252 Parameters
253 ----------
254 x_cur : Array
255 current solution
256 op : parallelproj.LinearOperator
257 listmode linear forward operator
258 s : Array
259 contamination list
260 adjoint_ones : Array
261 adjoint of ones of the non-LM (the complete) operator
262
263 Returns
264 -------
265 Array
266 """
267 ybar = op(x_cur) + s
268 return x_cur * op.adjoint(1 / ybar) / adjoint_ones
Run LM MLEM iterations
275 # number of MLEM iterations
276 num_iter = 10
277
278 # initialize x
279 x = xp.ones(pet_lin_op.in_shape, dtype=xp.float32, device=dev)
280 # calculate A^H 1
281 adjoint_ones = pet_lin_op.adjoint(
282 xp.ones(pet_lin_op.out_shape, dtype=xp.float32, device=dev)
283 )
284
285 for i in range(num_iter):
286 print(f"MLEM iteration {(i + 1):03} / {num_iter:03}", end="\r")
287 x = lm_em_update(x, lm_pet_lin_op, contamination_list, adjoint_ones)
Calculate the negative Poisson log-likelihood function of the reconstruction
293 # calculate the negative Poisson log-likelihood function of the reconstruction
294 exp = pet_lin_op(x) + contamination
295 # calculate the relative cost and distance to the optimal point
296 cost = float(xp.sum(exp - xp.astype(y, xp.float32) * xp.log(exp)))
297 print(f"\nMLEM cost {cost:.6E} after {num_iter:03} iterations")
Visualize the results
304 def _update_img(i):
305 img0.set_data(x_true_np[:, :, i])
306 img1.set_data(x_np[:, :, i])
307 ax[0].set_title(f"true image - plane {i:02}")
308 ax[1].set_title(f"LM MLEM iteration {num_iter} - plane {i:02}")
309 return (img0, img1)
310
311
312 x_true_np = parallelproj.to_numpy_array(x_true)
313 x_np = parallelproj.to_numpy_array(x)
314
315 fig, ax = plt.subplots(1, 2, figsize=(10, 5))
316 vmax = x_np.max()
317 img0 = ax[0].imshow(x_true_np[:, :, 0], cmap="Greys", vmin=0, vmax=vmax)
318 img1 = ax[1].imshow(x_np[:, :, 0], cmap="Greys", vmin=0, vmax=vmax)
319 ax[0].set_title(f"true image - plane {0:02}")
320 ax[1].set_title(f"LM MLEM iteration {num_iter} - plane {0:02}")
321 fig.tight_layout()
322 ani = animation.FuncAnimation(fig, _update_img, x_np.shape[2], interval=200, blit=False)
323 fig.show()
Related examples