Regions of interest (ROIs) drawn slice by slice in the ‘OsiriX’ DICOM viewer can be exported as an XML file. For each ROI on each slice, the file stores the polygon corner points, the value of every voxel inside the ROI and summary statistics, but not the voxel positions. osirad finds the voxel positions:
read_osirix_rois() reads the XML file.suv_factor_from_header() and
read_pet_slice() read the PET DICOM slices and convert them
to standardised uptake values (SUV).match_roi_voxels() finds, near the ROI corner points,
the pixels whose SUV equals the voxel values in the XML file.extract_roi_voxels() runs steps 1 to 3 for all ROIs and
returns one table with the position (pixels and mm) and the SUV of every
ROI voxel.pyradiomics_features() sends the PET series and the ROI
voxels to the ‘Python’ library ‘PyRadiomics’ to compute radiomics
features.The package contains a small synthetic PET phantom (32 x 32 pixels, 8 slices) with two ROIs, “Tumour” and “Node”, exported in the ‘OsiriX’ XML format.
x = extract_roi_voxels(xml_file, pet_dir)
x
#> 'OsiriX' ROI voxels: 8 ROI(s) on 6 slice(s), 222 voxels
#> ROI names: Tumour, Node
#> SUV factor: 0.000276308
#> XML values: 222 | clear match: 221 | listed twice: 0 | tie by neighbours: 1 | tie by distance: 0 | not found: 0
head(x$voxels)
#> roi_id roi_name image_index x_px y_px x_mm y_mm z_mm suv
#> 1 1 Tumour 1 15 12 -2 -14 56.73 2.343108
#> 2 1 Tumour 1 16 12 2 -14 56.73 2.226960
#> 3 1 Tumour 1 14 13 -6 -10 56.73 1.887495
#> 4 1 Tumour 1 15 13 -2 -10 56.73 2.404036
#> 5 1 Tumour 1 16 13 2 -10 56.73 2.685511
#> 6 1 Tumour 1 17 13 6 -10 56.73 2.356956The printed report shows how each XML value was matched to a pixel:
pet_dir is not the series the ROIs were drawn on;
extract_roi_voxels() then gives a warning.The match per ROI is in x$match:
x$match
#> roi_id roi_name image_index xml_values clear listed_twice tie_by_neighbours
#> 1 1 Tumour 1 14 14 0 0
#> 2 2 Tumour 2 33 32 0 1
#> 3 3 Tumour 3 54 54 0 0
#> 4 4 Tumour 4 54 54 0 0
#> 5 5 Node 4 10 10 0 0
#> 6 6 Tumour 5 33 33 0 0
#> 7 7 Node 5 10 10 0 0
#> 8 8 Tumour 6 14 14 0 0
#> tie_by_distance not_found voxels
#> 1 0 0 14
#> 2 0 0 33
#> 3 0 0 54
#> 4 0 0 54
#> 5 0 0 10
#> 6 0 0 33
#> 7 0 0 10
#> 8 0 0 14Check the voxels on the images:
Save the voxel table for other software:
pyradiomics_features() needs ‘Python’ with the modules
pyradiomics, SimpleITK and numpy,
and the R package ‘reticulate’. One way to set up ‘Python’ with ‘conda’
(in a terminal):
conda create -n radiomics python=3.9 "numpy<2"
conda activate radiomics
pip install pyradiomics
On computers where pip install pyradiomics fails (for
example Apple silicon), install it from the source code:
pip install versioneer "setuptools<70"
pip install --no-build-isolation git+https://github.com/AIM-Harvard/pyradiomics.git@v3.1.0
Then give the path of this ‘Python’ to
pyradiomics_features()
(conda activate radiomics; which python shows it). The
default settings are a fixed bin width of 0.25 SUV, no intensity
normalisation and no resampling; all ROIs form one mask.
rad = pyradiomics_features(x, pet_dir, python = Sys.getenv("OSIRAD_PYTHON"))
head(rad$features)
rad$checkrad$check compares the ‘Python’ mask with the R voxels:
the number of voxels, the largest distance between a voxel and its
slice, and the largest SUV difference between ‘Python’ and R.