Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ libjpeg = [
]
itk = ["itk>=5.4.0"]
sitk = ["SimpleITK>=2.2.1"]
nib = ["nibabel>=4.0.0"]

[dependency-groups]
test = [
Expand Down
130 changes: 130 additions & 0 deletions src/highdicom/volume.py
Original file line number Diff line number Diff line change
Expand Up @@ -3892,6 +3892,136 @@ def from_itk(
frame_of_reference_uid=frame_of_reference_uid
)

def to_nib(self) -> 'nibabel.Nifti1Image': # noqa: F821
"""Convert the volume to `nibabel.Nifti1Image` format.

The Volume is converted to a 3D ``nibabel.Nifti1Image``. If its
array's current datatype is not supported by NiBabel, it is safely
cast to a compatible type where possible. If impossible to cast safely,
a ``ValueError`` is raised. Casting is performed on the following
data types:

- ``bool`` -> ``uint8``
- ``float16`` -> ``float32`` (with warning)
- ``float128`` -> ``float64`` (with warning if possible, else
raises error)

Spatial metadata is preserved through the affine array. However,
highdicom uses "LPS" convention and NiBabel uses "RAS". This change
in convention is performed directly by this method.

Returns
-------
nibabel.Nifti1Image:
Image constructed from the volume.

Raises
------
ValueError
When the volume is not 3D (multiple channels are unsupported).
ValueError
When the array's current datatype is not supported
and it is not possible to safely cast to a new datatype.

"""
func = self.to_nib
nib = import_optional_dependency(
module_name='nibabel',
feature=f'{func.__module__}.{func.__qualname__}'
)

if self.array.ndim != 3:
raise ValueError(
'NiBabel conversion does not currently support'
' volumes with multiple channels.'
)

array = self.array

if array.dtype == np.bool_:
array = array.astype(np.uint8)

elif array.dtype == np.float16:
warnings.warn(
'NiBabel does not support float16 data.'
' Safely casting to float32.'
)
array = array.astype(np.float32)

elif array.dtype == np.float128:
f64 = np.finfo(np.float64)
if array.min() >= f64.min and array.max() <= f64.max:
warnings.warn(
'NiBabel does not support float128 data.'
' Casting to float64, precision may be lost.'
)
array = array.astype(np.float64)

else:
raise ValueError(
'NiBabel does not support float128 data.'
' Casting to float64 is not possible.'
)

nifti = nib.Nifti1Image(
Copy link
Copy Markdown
Collaborator Author

@mccle mccle May 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nibabel has several other image classes that I believe could represent a volume, listed here. Not all of them are relevant, but the MINC and MGH formats seem worth consideration. I think at the very least, we should support both Nifti1Image and Nifti2Image, meaning the to_nib method should already be changed to allow the user to specify the class to which they wish to convert. Additionally, several of these classes can accept an array, affine, and dtype exactly as the Nifti1Image class or with extremely minor changes.

However, I do not think all of the formats support the same data types. Are you interested in supporting other formats @CPBridge? If so, would it be best to group different classes into separate to_* methods or have any branching logic be included in a single to_nib method?

array,
self.get_affine('RAS'),
dtype=array.dtype
)

return nifti

@classmethod
def from_nib(
cls,
nifti: 'nibabel.Nifti1Image', # noqa: F821
coordinate_system: CoordinateSystemNames | str = 'PATIENT',
frame_of_reference_uid: str | None = None
) -> Self:
"""Construct a Volume from an `nibabel.Nifti1Image`.

The ``nibabel.Nifti1Image`` is converted to a 3D Volume.
Spatial metadata is preserved through the affine array. However,
highdicom uses "LPS" convention and NiBabel uses "RAS". This change
in convention is performed directly by this method.

Parameters
----------
nifti: nibabel.Nifti1Image
An ``nibabel.Nifti1Image`` to convert to a volume.
coordinate_system: highdicom.CoordinateSystemNames | str
Coordinate system (``"PATIENT"`` or ``"SLIDE"``) in which the volume
is defined.
frame_of_reference_uid: Union[str, None], optional
Frame of reference UID for the frame of reference, if known.

Returns
-------
highdicom.Volume:
Volume constructed from the `itk.Image`.

Raises
------
ValueError
When the volume is not 3D (multiple channels are unsupported).

"""
array = np.asarray(nifti.dataobj)

if array.ndim != 3:
raise ValueError(
'NiBabel conversion does not currently support'
' volumes with multiple channels.'
)

return cls(
array=array,
affine=nifti.affine,
coordinate_system=coordinate_system,
frame_of_reference_uid=frame_of_reference_uid,
from_reference_convention='RAS'
)


class VolumeToVolumeTransformer:

Expand Down
Loading
Loading