import copy
import os
import numpy as np
import SimpleITK as sitk
from .io import dicom, nifti
[docs]
class Image:
"""Image volume with voxel data and physical geometry metadata.
``Image`` stores arrays in NumPy order while preserving the origin, spacing,
direction, and size used by SimpleITK. The class is used throughout
preprocessing, filtering, and radiomics to keep image data aligned with ROI
masks.
Parameters
----------
array : numpy.ndarray or None, optional
Voxel array. Image data are typically stored in ``(z, y, x)`` order.
origin : sequence of float or None, optional
Physical origin of the image in SimpleITK ``(x, y, z)`` order.
spacing : sequence of float or None, optional
Physical voxel spacing in SimpleITK ``(x, y, z)`` order.
direction : sequence of float or None, optional
Flattened 3D direction cosine matrix.
shape : sequence of int or None, optional
Image size in SimpleITK ``(x, y, z)`` order.
"""
def __init__(self, array=None, origin=None, spacing=None, direction=None, shape=None):
self.sitk_image = None
self.array = array
self.origin = origin
self.spacing = spacing
self.direction = direction
self.shape = shape
[docs]
@classmethod
def from_nifti(cls, image_path):
"""Create an image from a NIfTI file.
Parameters
----------
image_path : str or path-like
Path to the NIfTI image file.
Returns
-------
image : Image
Image populated with voxel data and geometry read from the file.
"""
return cls._from_sitk_image(nifti.read_nifti_image(image_path))
[docs]
@classmethod
def from_nifti_mask(cls, mask_path, reference):
"""Create a NIfTI mask aligned to a reference image.
Parameters
----------
mask_path : str or path-like
Path to the NIfTI mask file.
reference : Image
Reference image that defines the target grid and geometry.
Returns
-------
mask : Image
Binary mask image resampled onto the reference geometry.
"""
mask = nifti.read_nifti_mask(mask_path, reference.sitk_image)
image = cls._from_sitk_image(mask)
image.origin = reference.origin
image.spacing = reference.spacing
image.direction = reference.direction
image.shape = reference.shape
return image
[docs]
@classmethod
def from_dicom(cls, dicom_dir, modality):
"""Create an image from a DICOM series.
Parameters
----------
dicom_dir : str or path-like
Directory containing the DICOM series.
modality : str
Imaging modality used by the DICOM reader.
Returns
-------
image : Image
Image populated with voxel data and geometry read from the series.
"""
return cls._from_sitk_image(dicom.read_dicom_image(dicom_dir, modality))
[docs]
@classmethod
def from_dicom_mask(cls, rtstruct_path, structure_name, reference, dicom_dir=None):
"""Create a DICOM RTSTRUCT or SEG mask aligned to a reference image.
Parameters
----------
rtstruct_path : str or path-like
Path to an RTSTRUCT or BINARY DICOM SEG file.
structure_name : str
RTSTRUCT ROI name or DICOM SEG ``SegmentLabel`` to extract.
reference : Image
Reference image that defines the target grid and geometry.
dicom_dir : str or path-like, optional
Directory containing the referenced source DICOM image series.
Supply it for SEG input so source-frame references can be resolved
and checked, including frames without independent spatial geometry.
RTSTRUCT loading does not use this argument.
Returns
-------
mask : Image
Binary mask image aligned to the reference geometry.
"""
return dicom.read_dicom_mask(rtstruct_path, structure_name, reference.sitk_image, dicom_dir=dicom_dir)
@classmethod
def _from_sitk_image(cls, image):
array = sitk.GetArrayFromImage(image)
result = cls(
array=array.astype(np.float64),
origin=image.GetOrigin(),
spacing=np.array(image.GetSpacing()),
direction=image.GetDirection(),
shape=image.GetSize(),
)
result.sitk_image = image
return result
[docs]
def copy(self):
"""Return a deep copy of the image data and geometry.
Returns
-------
image : Image
New image with copied array, origin, spacing, direction, and shape.
"""
return Image(
array=copy.deepcopy(self.array),
origin=copy.deepcopy(self.origin),
spacing=copy.deepcopy(self.spacing),
direction=copy.deepcopy(self.direction),
shape=copy.deepcopy(self.shape),
)
[docs]
def resample_to_target(self, target, interpolator=sitk.sitkLinear):
"""Resample this image onto the physical grid of another image.
Values outside this image's physical extent are filled with its minimum
intensity. The returned image has the target's origin, spacing,
direction, and shape; neither input image is modified.
Parameters
----------
target : Image
Image whose physical grid defines the resampling output.
interpolator : int, optional
SimpleITK interpolator enum, such as ``sitk.sitkLinear`` or
``sitk.sitkNearestNeighbor``. The default is linear interpolation.
Returns
-------
image : Image
A new image containing this image resampled onto ``target``.
"""
if not isinstance(target, Image):
raise TypeError(f"Expected target to be Image, got {type(target)}.")
moving_image = sitk.GetImageFromArray(self.array)
moving_image.SetOrigin(self.origin)
moving_image.SetSpacing(self.spacing)
moving_image.SetDirection(self.direction)
target_image = sitk.GetImageFromArray(target.array)
target_image.SetOrigin(target.origin)
target_image.SetSpacing(target.spacing)
target_image.SetDirection(target.direction)
resampler = sitk.ResampleImageFilter()
resampler.SetReferenceImage(target_image)
resampler.SetInterpolator(interpolator)
output_pixel_type = moving_image.GetPixelID()
if interpolator != sitk.sitkNearestNeighbor:
output_pixel_type = sitk.sitkFloat64
resampler.SetOutputPixelType(output_pixel_type)
resampler.SetDefaultPixelValue(float(np.nanmin(self.array)))
return self._from_sitk_image(resampler.Execute(moving_image))
[docs]
def save_as_nifti(self, output_path):
"""Write the image to a NIfTI file.
Parameters
----------
output_path : str or path-like
Destination file path for the written NIfTI image.
"""
if not os.path.exists(os.path.dirname(output_path)):
os.makedirs(os.path.dirname(output_path))
img = sitk.GetImageFromArray(self.array)
img.SetOrigin(self.origin)
img.SetSpacing(self.spacing)
img.SetDirection(self.direction)
sitk.WriteImage(img, output_path)