Basics of Dynamic PET

This notebook illustrates basic image input/output functionality of Dynamic PET.

First, we download a 4-D PET image with its PET-BIDS json sidecar from OpenNeuro:

from pathlib import Path

import requests


outdir = Path.cwd() / "nb_data"
outdir.mkdir(exist_ok=True)

petjson_fname = outdir / "pet.json"
pet_fname = outdir / "pet.nii"

baseurl = "https://s3.amazonaws.com/openneuro.org/ds001705/sub-000101/ses-baseline/"

peturl = (
    baseurl
    + "pet/sub-000101_ses-baseline_pet.nii"
    + "?versionId=rMjWUWxAIYI46DmOQjulNQLTDUAThT5o"
)

if not petjson_fname.exists():
    r = requests.get(
        baseurl
        + "pet/sub-000101_ses-baseline_pet.json"
        + "?versionId=Gfkc8Y71JexOLZq40ZN4BTln_4VObTJR",
        timeout=10,
    )
    r.raise_for_status()
    with open(petjson_fname, "wb") as f:
        f.write(r.content)

if not pet_fname.exists():
    with requests.get(peturl, timeout=10, stream=True) as r:
        r.raise_for_status()
        with open(pet_fname, "wb") as f:
            for chunk in r.iter_content(chunk_size=8192):
                f.write(chunk)

Load 4-D PET image

We read in this 4-D PET image (and its accompanying json) using the load function from dynamicpet.petbids.petbidsimage:

from dynamicpet.petbids.petbidsimage import load


pet = load(pet_fname)

Basic attributes/properties

The variable pet is an instance of Dynamic PET’s PETBIDSImage class. This object stores image data in an attribute called img (i.e., pet.img), which is an instance of a subclass of nibabel.spatialimages.SpatialImage. (In this example, it happens to be an nibabel.nifti1.Nifti1Image.) We can get the dimensions of the 4-D image (3-D image dimensions and number of time frames) using the shape property:

pet.shape
(182, 218, 182, 23)

The variable pet also stores the PET-BIDS json as a dictionary, in an attribute called json_dict:

pet.json_dict
{'Manufacturer': 'Siemens',
 'ManufacturersModelName': 'Biograph mMr',
 'Units': 'kBq/mL',
 'TracerName': 'LondonPride',
 'TracerRadionuclide': 'C11',
 'BodyPart': 'brain',
 'InjectedRadioactivity': 400.0,
 'InjectedRadioactivityUnits': 'MBq',
 'InjectedMass': 5.0,
 'InjectedMassUnits': 'ug',
 'SpecificRadioactivity': 35.0,
 'SpecificRadioactivityUnits': 'GBq/ug',
 'ModeOfAdministration': 'bolus',
 'TimeZero': '09:45:00',
 'ScanStart': 0,
 'InjectionStart': 0,
 'FrameTimesStart': [0,
  15,
  30,
  45,
  60,
  120,
  180,
  240,
  300,
  450,
  600,
  900,
  1200,
  1500,
  1800,
  2100,
  2400,
  2700,
  3000,
  3300,
  3600,
  4200,
  4800],
 'FrameDuration': [15,
  15,
  15,
  15,
  60,
  60,
  60,
  60,
  150,
  150,
  300,
  300,
  300,
  300,
  300,
  300,
  300,
  300,
  300,
  300,
  600,
  600,
  600],
 'InjectionEnd': 30,
 'AcquisitionMode': '3D',
 'ImageDecayCorrected': True,
 'ImageDecayCorrectionTime': 0,
 'ReconMethodName': 'MLEM',
 'ReconMethodParameterLabels': ['iterations'],
 'ReconMethodParameterUnits': ['none'],
 'ReconMethodParameterValues': [100],
 'ReconFilterType': 'PSF',
 'ReconFilterSize': 2.5,
 'AttenuationCorrection': 'Activity decay corrected'}

Frame timing information

Dynamic PET includes properties/functions such as frame_start, frame_end, frame_mid, frame_duration to extract frame timing information.

:exclamation: Note: the default unit in PET-BIDS json files is seconds. Dynamic PET converts these to minutes, so all of the timing functions in Dynamic PET return values in minutes.

Dynamic PET internally performs certain data checks, so using these properties/functions are preferred over directly extracting the values from the json files.

import matplotlib.pyplot as plt


plt.figure()
plt.bar(
    pet.frame_start,
    pet.frame_duration,
    width=pet.frame_duration,
    edgecolor="black",
    align="edge",
)
plt.xlabel("Frame start (minutes)")
plt.ylabel("Frame duration (minutes)")
plt.title("Frame duration vs. frame start");
Matplotlib is building the font cache; this may take a moment.
../_images/544991721cc79aec34cbd319141c2e88596758558f3614ea7e435ed049f0a09c.png

Basic visualization

We find and plot the first 5-minute long frame using nilearn:

from nilearn import plotting
from nilearn.image import index_img


frame_index = (pet.frame_duration == 5).argmax()
plotting.plot_anat(index_img(pet.img, frame_index), colorbar=True, draw_cross=False);
../_images/7bad2c7a299aa1a6b863bbfb8943a29d72c7eeb7b4b7af4ce37d0101d0bb4839.png

We can also plot a weighted mean of all time frames, where each frame is weighted according to its duration:

plotting.plot_anat(
    pet.dynamic_mean(weight_by="frame_duration"),
    colorbar=True,
    draw_cross=False,
);
../_images/c6d48053b29f5de365be76c49281bbafa74f48dc673868af575704304ed72637.png

Time activity curve

We can extract the time series data (called the time activity curve, or TAC) for a single voxel:

voxel_index = (100, 100, 100)  # an arbitrarily selected voxel

voxel_tac = pet.dataobj[*voxel_index, ...]
time = pet.frame_mid

plt.figure()
plt.plot(time, voxel_tac)
plt.xlabel("Time (minutes)")
plt.ylabel(f'Radioactivity ({pet.json_dict["Units"]})')
plt.title("Time activity curve (TAC) for a single voxel");
../_images/0e506625c182c1d60a18e03a545602c4286b289fc5d993e6f6bb979297cd876e.png

Temporal split

We can split the 4-D PET image (in time). This operation splits the data matrix and generates PET-BIDS jsons for the split images, and yields two PETBIDSImage objects.

pet_0to60, pet_60to90 = pet.split(split_time=60)  # split_time specified in min
pet_60to90.json_dict
{'Manufacturer': 'Siemens',
 'ManufacturersModelName': 'Biograph mMr',
 'Units': 'kBq/mL',
 'TracerName': 'LondonPride',
 'TracerRadionuclide': 'C11',
 'BodyPart': 'brain',
 'InjectedRadioactivity': 400.0,
 'InjectedRadioactivityUnits': 'MBq',
 'InjectedMass': 5.0,
 'InjectedMassUnits': 'ug',
 'SpecificRadioactivity': 35.0,
 'SpecificRadioactivityUnits': 'GBq/ug',
 'ModeOfAdministration': 'bolus',
 'TimeZero': '09:45:00',
 'ScanStart': 0,
 'InjectionStart': 0,
 'FrameTimesStart': [3600.0, 4200.0, 4800.0],
 'FrameDuration': [600.0, 600.0, 600.0],
 'InjectionEnd': 30,
 'AcquisitionMode': '3D',
 'ImageDecayCorrected': True,
 'ImageDecayCorrectionTime': 0,
 'ReconMethodName': 'MLEM',
 'ReconMethodParameterLabels': ['iterations'],
 'ReconMethodParameterUnits': ['none'],
 'ReconMethodParameterValues': [100],
 'ReconFilterType': 'PSF',
 'ReconFilterSize': 2.5,
 'AttenuationCorrection': 'Activity decay corrected'}

Save 4-D PET image

We save the 60-to-90 min image (along with its PET-BIDS json):

out_fname = pet_fname.with_name(pet_fname.stem + "_60to90min").with_suffix(
    pet_fname.suffix
)
pet.to_filename(out_fname)