Skip to content

Commit 1af866a

Browse files
author
Georg Schramm
committed
add script to produce attenuation image
1 parent cd9ecc1 commit 1af866a

File tree

3 files changed

+255
-61
lines changed

3 files changed

+255
-61
lines changed

python/01_analytic_petsird_lm_simulator.py

Lines changed: 97 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -70,55 +70,127 @@ def parse_float_tuple(arg):
7070
# %%
7171
# parse the command line for the input parameters below
7272
parser = argparse.ArgumentParser(
73-
description="Analytic simulation of PETSIRD v0.7.2 listmode data for a block PET scanner"
73+
description="Analytic simulation of PETSIRD v0.7.2 listmode data for a block PET scanner",
74+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
7475
)
7576

7677
# Output configuration group
77-
output_group = parser.add_argument_group('Output Configuration')
78-
output_group.add_argument("--fname", type=str, default="simulated_petsird_lm_file.bin", help="name of the output LM file")
79-
output_group.add_argument("--output_dir", type=str, default=None, help="directory to save output files")
80-
output_group.add_argument("--skip_writing", default=False, action="store_true", help="skip writing the LM data to a file")
78+
output_group = parser.add_argument_group("Output Configuration")
79+
output_group.add_argument(
80+
"--fname",
81+
type=str,
82+
default="simulated_petsird_lm_file.bin",
83+
help="name of the output LM file",
84+
)
85+
output_group.add_argument(
86+
"--output_dir", type=str, default=None, help="directory to save output files"
87+
)
88+
output_group.add_argument(
89+
"--skip_writing",
90+
default=False,
91+
action="store_true",
92+
help="skip writing the LM data to a file",
93+
)
8194

8295
# Image and phantom configuration group
83-
image_group = parser.add_argument_group('Image and Phantom Configuration')
96+
image_group = parser.add_argument_group("Image and Phantom Configuration")
8497
image_group.add_argument(
8598
"--phantom",
8699
type=str,
87100
default="cylinder",
88101
choices=["cylinder", "uniform_cylinder", "squares", "points"],
89102
help="phantom to simulate",
90103
)
91-
image_group.add_argument("--img_shape", type=parse_int_tuple, default=(55, 55, 19), help="shape of the image to simulate")
92-
image_group.add_argument("--voxel_size", type=parse_float_tuple, default=(2.0, 2.0, 2.0), help="voxel size in mm used in the simulation")
104+
image_group.add_argument(
105+
"--img_shape",
106+
type=parse_int_tuple,
107+
default=(55, 55, 19),
108+
help="shape of the image to simulate",
109+
)
110+
image_group.add_argument(
111+
"--voxel_size",
112+
type=parse_float_tuple,
113+
default=(2.0, 2.0, 2.0),
114+
help="voxel size in mm used in the simulation",
115+
)
93116

94117
# Physics and resolution parameters group
95-
physics_group = parser.add_argument_group('Physics / simulation parameters')
96-
physics_group.add_argument("--fwhm_mm", type=float, default=2.5, help="FWHM of the image space resolution model in mm")
97-
physics_group.add_argument("--tof_fwhm_mm", type=float, default=20.0, help="FWHM of the TOF resolution model in mm")
98-
physics_group.add_argument("--num_true_counts", type=int, default=int(4e6), help="number of true coincidences to simulate")
118+
physics_group = parser.add_argument_group("Physics / simulation parameters")
119+
physics_group.add_argument(
120+
"--fwhm_mm",
121+
type=float,
122+
default=2.5,
123+
help="FWHM of the image space resolution model in mm",
124+
)
125+
physics_group.add_argument(
126+
"--tof_fwhm_mm",
127+
type=float,
128+
default=20.0,
129+
help="FWHM of the TOF resolution model in mm",
130+
)
131+
physics_group.add_argument(
132+
"--num_true_counts",
133+
type=int,
134+
default=int(4e6),
135+
help="number of true coincidences to simulate",
136+
)
99137

100138
# Efficiency parameters group
101-
efficiency_group = parser.add_argument_group('Detection Efficiency Parameters')
102-
efficiency_group.add_argument("--uniform_crystal_eff", action="store_true", help="use uniform crystal efficiencies, otherwise use a random distribution")
103-
efficiency_group.add_argument("--uniform_sg_eff", action="store_true", help="use uniform symmetry group (module pair) efficiencies, otherwise pseudo random pattern is used")
139+
efficiency_group = parser.add_argument_group("Detection Efficiency Parameters")
140+
efficiency_group.add_argument(
141+
"--uniform_crystal_eff",
142+
action="store_true",
143+
help="use uniform crystal efficiencies, otherwise use a random distribution",
144+
)
145+
efficiency_group.add_argument(
146+
"--uniform_sg_eff",
147+
action="store_true",
148+
help="use uniform symmetry group (module pair) efficiencies, otherwise pseudo random pattern is used",
149+
)
104150

105151
# Time block configuration group
106-
time_group = parser.add_argument_group('Time Block Configuration')
107-
time_group.add_argument("--num_time_blocks", type=int, default=3, help="number of time blocks to split the data into")
108-
time_group.add_argument("--event_block_duration", type=int, default=100, help="duration of each time block in ms")
152+
time_group = parser.add_argument_group("Time Block Configuration")
153+
time_group.add_argument(
154+
"--num_time_blocks",
155+
type=int,
156+
default=3,
157+
help="number of time blocks to split the data into",
158+
)
159+
time_group.add_argument(
160+
"--event_block_duration",
161+
type=int,
162+
default=100,
163+
help="duration of each time block in ms",
164+
)
109165

110166
# Reconstruction and analysis group
111-
recon_group = parser.add_argument_group('Reconstruction and Analysis')
112-
recon_group.add_argument("--num_epochs_mlem", type=int, default=0, help="number of epochs for MLEM reconstruction of histogrammed data, 0 means no MLEM reconstruction")
113-
recon_group.add_argument("--check_backprojection", default=False, action="store_true", help="check the backprojection of the TOF histogram and LM data")
167+
recon_group = parser.add_argument_group("Reconstruction and Analysis")
168+
recon_group.add_argument(
169+
"--num_epochs_mlem",
170+
type=int,
171+
default=0,
172+
help="number of epochs for MLEM reconstruction of histogrammed data, 0 means no MLEM reconstruction",
173+
)
174+
recon_group.add_argument(
175+
"--check_backprojection",
176+
default=False,
177+
action="store_true",
178+
help="check the backprojection of the TOF histogram and LM data",
179+
)
114180

115181
# Visualization and debugging group
116-
visual_group = parser.add_argument_group('Visualization and Debugging')
117-
visual_group.add_argument("--skip_plots", action="store_true", help="skip plotting the scanner geometry and TOF profile")
182+
visual_group = parser.add_argument_group("Visualization and Debugging")
183+
visual_group.add_argument(
184+
"--skip_plots",
185+
action="store_true",
186+
help="skip plotting the scanner geometry and TOF profile",
187+
)
118188

119189
# General options group
120-
general_group = parser.add_argument_group('General Options')
121-
general_group.add_argument("--seed", type=int, default=0, help="random seed for reproducibility")
190+
general_group = parser.add_argument_group("General Options")
191+
general_group.add_argument(
192+
"--seed", type=int, default=0, help="random seed for reproducibility"
193+
)
122194

123195
args = parser.parse_args()
124196

python/02_reconstruct_petsird.py

Lines changed: 78 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from pathlib import Path
44
import parallelproj
55
import petsird
6+
import hashlib
67

78
# for the parallelproj recon we can use cupy as array backend if available
89
# otherwise we fall back to numpy
@@ -21,13 +22,44 @@
2122
)
2223

2324

25+
def get_params_hash(
26+
img_shape: tuple[int, int, int],
27+
voxel_size: tuple[float, float, float],
28+
tof: bool,
29+
att_img: np.ndarray | None = None,
30+
) -> str:
31+
"""Generate a hash string for all sensitivity image parameters."""
32+
# Create a hash based on all parameters that affect sensitivity image calculation
33+
hash_obj = hashlib.sha256()
34+
35+
# Add image shape
36+
hash_obj.update(str(img_shape).encode())
37+
38+
# Add voxel size (convert to string with fixed precision to avoid floating point issues)
39+
voxel_size_str = f"{voxel_size[0]:.6f},{voxel_size[1]:.6f},{voxel_size[2]:.6f}"
40+
hash_obj.update(voxel_size_str.encode())
41+
42+
# Add TOF flag
43+
hash_obj.update(str(tof).encode())
44+
45+
# Add attenuation image data if present
46+
if att_img is not None:
47+
hash_obj.update(att_img.tobytes())
48+
else:
49+
hash_obj.update(b"no_attenuation")
50+
51+
# Return first 12 characters to reduce collision probability with more parameters
52+
return hash_obj.hexdigest()[:6]
53+
54+
2455
# %%
2556
################################################################################
2657
#### PARSE THE COMMAND LINE ####################################################
2758
################################################################################
2859

2960
parser = argparse.ArgumentParser(
30-
description="PETSIRD analytic simulator reconstruction"
61+
description="PETSIRD analytic simulator reconstruction",
62+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
3163
)
3264
parser.add_argument("fname", type=str, help="Path to the PETSIRD listmode file")
3365
parser.add_argument(
@@ -210,7 +242,10 @@
210242
print("Using ones as sensitivity image ...")
211243
sens_img = xp.ones(img_shape, dtype="float32")
212244
else:
213-
sens_img_path = Path(fname).with_suffix(".sens_img.npy")
245+
# Generate hash for all sensitivity parameters
246+
params_hash = get_params_hash(img_shape, voxel_size, tof, att_img)
247+
248+
sens_img_path = Path(fname).with_name(f"{Path(fname).stem}_sens_{params_hash}.npy")
214249

215250
if sens_img_path.exists():
216251
print(f"Loading sensitivity image from {sens_img_path}")
@@ -235,25 +270,25 @@
235270

236271
xp.save(sens_img_path, sens_img)
237272

238-
# %%
239-
################################################################################
240-
### CHECK WHETHER THE CALC SENS IMAGE MATCHES THE REF. SENSE IMAGE #############
241-
################################################################################
242-
243-
ref_sens_img_path = Path(fname).parent / "reference_sensitivity_image.npy"
244-
245-
if ref_sens_img_path.exists():
246-
print(f"loading reference sensitivity image from {ref_sens_img_path}")
247-
ref_sens_img = np.load(ref_sens_img_path)
248-
if ref_sens_img.shape == sens_img.shape:
249-
if np.allclose(np.asarray(sens_img), ref_sens_img):
250-
print(
251-
f"calculated sensitivity image matches reference image {ref_sens_img_path}"
252-
)
253-
else:
254-
print(
255-
f"calculated sensitivity image does NOT match reference image {ref_sens_img_path}"
256-
)
273+
## %%
274+
#################################################################################
275+
#### CHECK WHETHER THE CALC SENS IMAGE MATCHES THE REF. SENSE IMAGE #############
276+
#################################################################################
277+
#
278+
# ref_sens_img_path = Path(fname).parent / "reference_sensitivity_image.npy"
279+
#
280+
# if ref_sens_img_path.exists():
281+
# print(f"loading reference sensitivity image from {ref_sens_img_path}")
282+
# ref_sens_img = np.load(ref_sens_img_path)
283+
# if ref_sens_img.shape == sens_img.shape:
284+
# if np.allclose(np.asarray(sens_img), ref_sens_img):
285+
# print(
286+
# f"calculated sensitivity image matches reference image {ref_sens_img_path}"
287+
# )
288+
# else:
289+
# print(
290+
# f"calculated sensitivity image does NOT match reference image {ref_sens_img_path}"
291+
# )
257292

258293
# %%
259294
################################################################################
@@ -325,25 +360,32 @@
325360

326361
non_tof_backproj = proj.adjoint(xp.ones(coords0.shape[0], dtype="float32"))
327362

328-
xp.save(Path(fname).with_suffix(".non_tof_backproj.npy"), non_tof_backproj)
363+
non_tof_backproj_path = Path(fname).with_name(
364+
f"{Path(fname).stem}_nontof_backproj_{params_hash}.npy"
365+
)
366+
xp.save(non_tof_backproj_path, non_tof_backproj)
329367

330368
#### HACK assumes same TOF parameters for all module type pairs
331-
sigma_tof = scanner_info.tof_resolution[0][0] / 2.35
332-
tof_bin_edges = scanner_info.tof_bin_edges[0][0].edges
333-
num_tofbins = tof_bin_edges.size - 1
334-
tofbin_width = float(tof_bin_edges[1] - tof_bin_edges[0])
335-
336-
tof_params = parallelproj.TOFParameters(
337-
num_tofbins=num_tofbins, tofbin_width=tofbin_width, sigma_tof=sigma_tof
338-
)
369+
if tof:
370+
sigma_tof = scanner_info.tof_resolution[0][0] / 2.35
371+
tof_bin_edges = scanner_info.tof_bin_edges[0][0].edges
372+
num_tofbins = tof_bin_edges.size - 1
373+
tofbin_width = float(tof_bin_edges[1] - tof_bin_edges[0])
374+
375+
tof_params = parallelproj.TOFParameters(
376+
num_tofbins=num_tofbins, tofbin_width=tofbin_width, sigma_tof=sigma_tof
377+
)
339378

340-
proj.tof_parameters = tof_params
341-
proj.event_tofbins = xp.asarray(signed_tof_bins).copy()
342-
proj.tof = True
379+
proj.tof_parameters = tof_params
380+
proj.event_tofbins = xp.asarray(signed_tof_bins).copy()
381+
proj.tof = True
343382

344-
tof_backproj = proj.adjoint(xp.ones(coords0.shape[0], dtype="float32"))
383+
tof_backproj = proj.adjoint(xp.ones(coords0.shape[0], dtype="float32"))
345384

346-
xp.save(Path(fname).with_suffix(".tof_backproj.npy"), tof_backproj)
385+
tof_backproj_path = Path(fname).with_name(
386+
f"{Path(fname).stem}_tof_backproj_{params_hash}.npy"
387+
)
388+
xp.save(tof_backproj_path, tof_backproj)
347389

348390
del proj
349391

@@ -412,7 +454,7 @@
412454
)
413455
recon *= tmp / sens_img
414456

415-
opath = Path(fname).parent / f"lm_osem_{num_epochs}_{num_subsets}.npy"
457+
opath = Path(fname).with_name(f"lm_osem_{num_epochs}_{num_subsets}_{params_hash}.npy")
416458
xp.save(opath, recon)
417459
print(f"LM OSEM recon saved to {opath}")
418460

0 commit comments

Comments
 (0)