SUV normalization of PET images

PET images are stored in DICOM as activity concentration (for example in Bq/mL). To make the values comparable across patients and scanners they are converted to the Standardized Uptake Value (SUV), which accounts for the injected dose, the radionuclide decay and the patient body weight. Radiomics.jl performs this conversion while reading the DICOM series, before the radiomic features are extracted: set suv_ibsi_normalization=true in extract_radiomic_features and pass the paths of a PET DICOM series and of the mask.

julia> using Radiomics

# PET DICOM series (folder) and mask (NIfTI, NRRD or DICOM)
julia> pet = "Path/to/PET_dicom_folder"
julia> mask = "Path/to/Mask.nii.gz"

# Normalize the PET to SUV and extract the radiomic features
julia> radiomic_features = Radiomics.extract_radiomic_features(pet, mask; suv_ibsi_normalization=true)

Without suv_ibsi_normalization=true the PET series is read as any other DICOM series, and the features are computed on the stored values (rescale slope and intercept applied).

Requirements

What the normalization needs

  • The image must be a DICOM PET series (Modality PT), either single-frame (a folder with one file per slice) or Enhanced multiframe.
  • The mask must be a path to a 3D image in NIfTI, NRRD or DICOM format, with the same size and voxel spacing as the PET.
  • The DICOM files must contain the metadata needed by the conversion: patient weight, injected dose, radionuclide half-life, administration time and acquisition times, units, and the rescale slope.
  • With arrays, NIfTI or NRRD images, or a series that is not PET, the function stops with an error: the SUV cannot be computed without the DICOM metadata.

The SUV is computed on the slices that contain the region of interest (on all the frames for Enhanced multiframe files). When a required value is missing or inconsistent, the function raises an error that reports the cause, instead of returning a wrong SUV.

Validation with the digital reference objects

The conversion is validated against the digital reference objects (DRO) of the suv_computation repository. The test suite downloads the whole set, runs the full pipeline on each case and checks the result:

DRO set Cases Expected outcome
Valid DROs 43 SUV in the region of interest equal to the nominal values of the DRO: minimum 0.20, median 1.00, maximum 4.00
Error DROs (DRO_error_*) 15 The normalization must be rejected, for the specific reason listed below

Cases that must be rejected

Each error DRO has a defect in the DICOM metadata. The test suite checks not only that the normalization fails, but that it fails for the expected reason.

DRO Reason of the rejection
DRO_error_2_0, DRO_error_2_1 Patient weight absent or not positive
DRO_error_2_2, DRO_error_2_3 Invalid patient sex (needed by the ideal body weight normalization)
DRO_error_2_4, DRO_error_2_5 Patient height absent (needed by the ideal body weight normalization)
DRO_error_2_6 Counts without a conversion factor
DRO_error_2_7 Unsupported units (proportional counts)
DRO_error_3_0, DRO_error_3_1 Injected dose absent
DRO_error_3_2 Reference time for the decay correction cannot be determined
DRO_error_4_0 Administration time absent
DRO_error_4_1, DRO_error_4_2 Administration time without a reliable date, with a long-half-life radionuclide
DRO_error_5_0 Radionuclide half-life absent

How to test the SUV normalization

The DRO tests are part of the test suite: they run with the IBSI validation test suite described in the IBSI Compliance page, and the code is available in the runtests.jl file. The DRO data are downloaded automatically.

julia> using Pkg
julia> Pkg.activate(".")
julia> Pkg.test("Radiomics")

To reproduce a single case, download a DRO from the repository and run it on its own. Each DRO has a PT folder with the DICOM series and a mask in mask/DRO_mask.nii.gz:

julia> using Radiomics

julia> pet = "DRO_0_0/PT"
julia> mask = "DRO_0_0/mask/DRO_mask.nii.gz"

julia> r = Radiomics.extract_radiomic_features(pet, mask; features=[:first_order], suv_ibsi_normalization=true)

julia> r["firstorder_minimum"], r["firstorder_median"], r["firstorder_maximum"]