2. NIfTI I/O
1. Setup: Download Sample Data¶
We'll use the test dataset from HuggingFace which includes NIfTI format radiotherapy data.
from huggingface_hub import snapshot_download
from pathlib import Path
import numpy as np
# Download the dataset (cached locally after first download)
data_path = snapshot_download(
repo_id="contouraid/dosemetrics-data",
repo_type="dataset"
)
data_path = Path(data_path)
print(f"✓ Data downloaded to: {data_path}")
print(f"\nAvailable datasets:")
for item in data_path.iterdir():
if item.is_dir():
print(f" - {item.name}")
/Users/amithkamath/Repositories/ContourAId/dosemetrics/.venv/lib/python3.11/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html from .autonotebook import tqdm as notebook_tqdm Fetching 165 files: 100%|██████████| 165/165 [00:00<00:00, 523891.11it/s]
✓ Data downloaded to: /Users/amithkamath/.cache/huggingface/hub/datasets--contouraid--dosemetrics-data/snapshots/839ceab7ba71766265fd6a637fe799341bb0364f Available datasets: - test_subject - longitudinal - dicom
2. Basic Data Loading¶
The simplest way to load NIfTI data is using load_structure_set(). This function:
- Automatically detects the NIfTI format
- Loads the dose file and all structure masks
- Returns a StructureSet object with convenient access methods
from dosemetrics.io import load_structure_set
# Load test subject data (NIfTI format)
subject_path = data_path / "test_subject"
structures = load_structure_set(subject_path)
print(f"✓ Loaded NIfTI data from: {subject_path}")
print(f"\nNumber of structures: {len(structures)}")
print(f"Structure names: {structures.structure_names}")
✓ Loaded NIfTI data from: /Users/amithkamath/.cache/huggingface/hub/datasets--contouraid--dosemetrics-data/snapshots/839ceab7ba71766265fd6a637fe799341bb0364f/test_subject Number of structures: 16 Structure names: ['OpticNerve_L', 'Cochlea_R', 'CTV', 'Lens_L', 'OpticNerve_R', 'Cochlea_L', 'Lens_R', 'PTV', 'LacrimalGland_L', 'Eye_R', 'LacrimalGland_R', 'Chiasm', 'GTV', 'Brain', 'Eye_L', 'Brainstem']
3. Working with the StructureSet¶
The StructureSet object provides convenient access to structure masks and their properties.
# List all available structures with details
print("Available structures:")
print("-" * 60)
for i, name in enumerate(structures.structure_names, 1):
structure = structures.get_structure(name)
voxel_count = (structure.mask > 0).sum()
print(f"{i:2d}. {name:20s} - {voxel_count:7,d} voxels")
# Get a specific structure mask
ptv = structures.get_structure("PTV")
print(f"\nPTV details:")
print(f" Shape: {ptv.mask.shape}")
print(f" Data type: {ptv.mask.dtype}")
print(f" Non-zero voxels: {(ptv.mask > 0).sum():,}")
print(f" Min value: {ptv.mask.min()}")
print(f" Max value: {ptv.mask.max()}")
# Check if structures are available
print(f"\nStructure availability:")
for check_name in ["Brainstem", "Chiasm", "OpticNerve_L"]:
available = check_name in structures
print(f" {check_name:20s}: {'✓' if available else '✗'}")
Available structures: ------------------------------------------------------------ 1. OpticNerve_L - 104 voxels 2. Cochlea_R - 10 voxels 3. CTV - 31,814 voxels 4. Lens_L - 28 voxels 5. OpticNerve_R - 134 voxels 6. Cochlea_L - 28 voxels 7. Lens_R - 33 voxels 8. PTV - 42,879 voxels 9. LacrimalGland_L - 58 voxels 10. Eye_R - 1,267 voxels 11. LacrimalGland_R - 56 voxels 12. Chiasm - 120 voxels 13. GTV - 9,312 voxels 14. Brain - 121,380 voxels 15. Eye_L - 1,179 voxels 16. Brainstem - 3,883 voxels PTV details: Shape: (128, 128, 128) Data type: bool Non-zero voxels: 42,879 Min value: False Max value: True Structure availability: Brainstem : ✓ Chiasm : ✓ OpticNerve_L : ✓
4. Loading Dose Distribution (Recommended)¶
Use the high-level Dose class to load dose files. This is the recommended approach for dose analysis.
from dosemetrics import Dose
# Load dose using the Dose class
dose_file = subject_path / "Dose.nii.gz"
dose = Dose.from_nifti(dose_file, name="Clinical")
print("Dose Distribution:")
print("-" * 60)
print(f" Name: {dose.name}")
print(f" Dimensions: {dose.shape}")
print(f" Max dose: {dose.max_dose:.2f} Gy")
print(f" Mean dose: {dose.mean_dose:.2f} Gy")
print(f" Min dose: {dose.min_dose:.2f} Gy")
print(f" Spacing: {dose.spacing} mm")
print(f" Origin: {dose.origin} mm")
Dose Distribution: ------------------------------------------------------------ Name: Clinical Dimensions: (128, 128, 128) Max dose: 64.45 Gy Mean dose: 7.95 Gy Min dose: -0.92 Gy Spacing: (2.0, 2.0, 2.0) mm Origin: (92.70909881591797, 80.26853942871094, 52.468624114990234) mm
5. Computing Dose Statistics¶
Combine the dose and structures to compute dose statistics.
from dosemetrics.metrics import dvh
# Compute dose statistics for a structure
ptv = structures.get_structure("PTV")
stats = dvh.compute_dose_statistics(dose, ptv)
print("PTV Dose Statistics:")
print("-" * 60)
print(f" Mean dose: {stats['mean_dose']:.2f} Gy")
print(f" Max dose: {stats['max_dose']:.2f} Gy")
print(f" Min dose: {stats['min_dose']:.2f} Gy")
print(f" D95: {stats['D95']:.2f} Gy")
print(f" D50: {stats['D50']:.2f} Gy")
print(f" D05: {stats['D05']:.2f} Gy")
# Compute DVH
dose_bins, volumes = dvh.compute_dvh(dose, ptv)
print(f"\nDVH computed with {len(dose_bins)} dose bins")
PTV Dose Statistics: ------------------------------------------------------------ Mean dose: 58.13 Gy Max dose: 63.90 Gy Min dose: 31.72 Gy D95: 48.17 Gy D50: 59.81 Gy D05: 61.21 Gy DVH computed with 640 dose bins
6. Low-Level NIfTI Operations (Advanced)¶
For advanced use cases, you can access raw NIfTI data using low-level functions.
from dosemetrics.io import nifti_io, load_volume
# Load individual volume with metadata (low-level)
volume, spacing, origin = load_volume(dose_file)
print("Low-Level Volume Access:")
print("-" * 60)
print(f" Shape: {volume.shape}")
print(f" Spacing: {spacing}")
print(f" Origin: {origin}")
print(f" Data type: {volume.dtype}")
# Load raw data as dictionary
nifti_data_dict = nifti_io.load_nifti_folder(subject_path, return_as_structureset=False)
print(f"\nDictionary keys: {list(nifti_data_dict.keys())}")
print("\nNote: For most use cases, use the high-level Dose and StructureSet classes instead.")
Low-Level Volume Access: ------------------------------------------------------------ Shape: (128, 128, 128) Spacing: (2.0, 2.0, 2.0) Origin: (92.70909881591797, 80.26853942871094, 52.468624114990234) Data type: float32 Dictionary keys: ['image_volumes', 'structure_masks', 'dose_volume', 'dose_spacing', 'dose_origin', 'spacing', 'origin'] Note: For most use cases, use the high-level Dose and StructureSet classes instead.
7. Loading Specific Structure Masks¶
You can load individual structure masks from NIfTI files.
# Load a specific structure mask
brainstem_file = subject_path / "Brainstem.nii.gz"
brainstem_mask, spacing, origin = load_volume(brainstem_file)
print("Brainstem Mask:")
print("-" * 60)
print(f" File: {brainstem_file.name}")
print(f" Shape: {brainstem_mask.shape}")
print(f" Data type: {brainstem_mask.dtype}")
print(f" Voxels in mask: {(brainstem_mask > 0).sum():,}")
print(f" Spacing: {spacing}")
print(f" Origin: {origin}")
Brainstem Mask: ------------------------------------------------------------ File: Brainstem.nii.gz Shape: (128, 128, 128) Data type: uint8 Voxels in mask: 3,883 Spacing: (2.0, 2.0, 2.0) Origin: (92.70909881591797, 80.26853942871094, 52.468624114990234)
8. Analyzing Spatial Properties¶
Extract spatial information from NIfTI files.
# Calculate physical dimensions
voxel_spacing = np.array(spacing)
grid_dimensions = np.array(volume.shape)
physical_dimensions = voxel_spacing * grid_dimensions
print("Spatial Properties:")
print("-" * 60)
print(f"Grid dimensions (voxels): {grid_dimensions}")
print(f"Voxel spacing (mm): {voxel_spacing}")
print(f"Physical dimensions (mm): {physical_dimensions}")
print(f"Physical dimensions (cm): {physical_dimensions / 10}")
print(f"\nVoxel volume: {np.prod(voxel_spacing):.3f} mm³")
print(f"Total volume: {np.prod(physical_dimensions) / 1000:.1f} cm³")
Spatial Properties: ------------------------------------------------------------ Grid dimensions (voxels): [128 128 128] Voxel spacing (mm): [2. 2. 2.] Physical dimensions (mm): [256. 256. 256.] Physical dimensions (cm): [25.6 25.6 25.6] Voxel volume: 8.000 mm³ Total volume: 16777.2 cm³
9. Format Detection¶
DoseMetrics can automatically detect the format of data in a folder.
from dosemetrics.io import detect_folder_format
# Check what format a folder contains
format_type = detect_folder_format(subject_path)
print(f"Detected format: {format_type}")
# Verify it's NIfTI
assert format_type == 'nifti', f"Expected 'nifti', got '{format_type}'"
print("✓ Format is NIfTI as expected")
Detected format: nifti ✓ Format is NIfTI as expected
10. Saving Data to NIfTI Format¶
Export structure masks and dose distributions to NIfTI files.
# Demonstration of saving capabilities
# Note: We're not actually saving here to avoid cluttering the workspace
print("NIfTI Export Capabilities:")
print("-" * 60)
print("\nTo save a volume (dose or mask) to NIfTI:")
print(" from dosemetrics.io import nifti_io")
print(" nifti_io.save_nifti(array, output_path, spacing, origin)")
print("\nTo save all structures from a StructureSet:")
print(" for name in structures.structure_names:")
print(" mask = structures.get_structure(name).mask")
print(" output_file = output_dir / f'{name}.nii.gz'")
print(" nifti_io.save_nifti(mask, output_file, spacing, origin)")
print("\n✓ For detailed export examples, see the exporting-results notebook")
NIfTI Export Capabilities:
------------------------------------------------------------
To save a volume (dose or mask) to NIfTI:
from dosemetrics.io import nifti_io
nifti_io.save_nifti(array, output_path, spacing, origin)
To save all structures from a StructureSet:
for name in structures.structure_names:
mask = structures.get_structure(name).mask
output_file = output_dir / f'{name}.nii.gz'
nifti_io.save_nifti(mask, output_file, spacing, origin)
✓ For detailed export examples, see the exporting-results notebook
Summary¶
In this notebook, you learned how to:
- ✓ Load NIfTI dose and structure data using
load_structure_set() - ✓ Work with the StructureSet API for convenient access
- ✓ Load individual NIfTI files with
load_volume() - ✓ Assign custom structure types (TARGET, OAR, etc.)
- ✓ Access spatial metadata (spacing, origin, dimensions)
- ✓ Use low-level NIfTI I/O functions for advanced control
- ✓ Analyze spatial properties and volumes
- ✓ Detect data format automatically
- ✓ Save data to NIfTI format
Key API Functions¶
High-level¶
load_structure_set(folder_path)- Auto-detect format and load all dataload_volume(file_path)- Load a single NIfTI file with metadata
Low-level¶
nifti_io.load_nifti_folder(folder_path)- Load all NIfTI files as dictionarynifti_io.save_nifti(array, path, spacing, origin)- Save array to NIfTI
Utilities¶
detect_folder_format(folder_path)- Detect data formatStructureType- Enum for structure classification
Next Steps¶
- DICOM I/O: See dicom-io.ipynb for DICOM operations
- Comparing Plans: Learn how to compare treatment plans in comparing-plans.ipynb
- API Documentation: Explore the full DoseMetrics API