Skip to content
Closed
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
2 changes: 1 addition & 1 deletion .github/workflows/run_unit_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
# and the likelihood of 3.12-specific bugs is considered low at this
# stage
python-version: ["3.10", "3.11", "3.13", "3.14"]
dependencies: [".", "'.[libjpeg]'"]
dependencies: [".", "'.[libjpeg,itk,sitk]'"]

env:
# Set this otherwise coverage on python 3.12 is absurdly slow
Expand Down
1 change: 1 addition & 0 deletions docs/general.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ parts of the library.
volume
coding
remote
other_libraries
49 changes: 49 additions & 0 deletions docs/itk_lib.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
.. _itk_lib:

ITK
===

`ITK`_ is a widely-used library for volumetric image processing. In order to
use ITK with highdicom, the ``itk`` python package must be installed separately.
Version 5.4.0 or later is required.

.. _itk_vol:

Volume Conversions
------------------

Highdicom supports conversions with the ``itk.Image`` class through the
:meth:`highdicom.Volume.to_itk` and :meth:`highdicom.Volume.from_itk` methods.
Like highdicom, ITK uses the "LPS" convention. However, when converting to and
from NumPy arrays, ITK reverses the order of dimensions. This permutation is
handled automatically by highdicom and requires no intervention by the user.

Creating an ITK Image from a Volume:

.. code-block:: python

import highdicom as hd


vol = hd.Volume(...)

itk_im = vol.to_itk()

Creating a volume from an ITK Image:

.. code-block:: python

import itk
import highdicom as hd


itk_im = itk.image(...)

vol = hd.Volume.from_itk(
itk_im=itk_im,
coordinate_system='PATIENT',
frame_of_reference_uid=None
)


.. _`ITK`: https://itk.org/
24 changes: 24 additions & 0 deletions docs/other_libraries.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
.. _other_libraries:

Interactions with Other Libraries
=================================

Highdicom is able to interact with other imaging and scientific python
libraries. These integrations are intended to provide streamlined access to
complementary functionality available in external ecosystems that would
otherwise require custom user code.

Because these libraries are not required for all workflows, they are
treated as optional dependencies and are not installed as part of the
default ``highdicom`` installation. Functionality requiring an external
library becomes available when an appropriate version of the corresponding
package is installed alongside ``highdicom``. The following sections describe
which libraries are currently supported and the additional features each
library enables.

.. toctree::
:maxdepth: 3
:caption: Contents:

itk_lib
sitk_lib
51 changes: 51 additions & 0 deletions docs/sitk_lib.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
.. _sitk_lib:

SimpleITK
=========

`SimpleITK`_ provides a simplified interface for the algorithms and data structures
of `ITK`_. In order to use ITK with highdicom, the ``SimpleITK`` python package must
be installed separately. Version 2.2.1 or later is required.

.. _sitk_vol:

Volume Conversions
------------------

Highdicom supports conversions with the ``SimpleITK.Image`` class through the
:meth:`highdicom.Volume.to_sitk` and :meth:`highdicom.Volume.from_sitk` methods.
Like highdicom, SimpleITK uses the "LPS" convention. However, when converting to
and from NumPy arrays, SimpleITK reverses the order of dimensions. This permutation
is handled automatically by highdicom and requires no intervention by the user.


Creating a SimpleITK Image from a Volume:

.. code-block:: python

import highdicom as hd


vol = hd.Volume(...)

sitk_im = vol.to_sitk()

Creating a volume from a SimpleITK Image:

.. code-block:: python

import SimpleITK as sitk
import highdicom as hd


sitk_im = sitk.image(...)

vol = hd.Volume.from_sitk(
sitk_im=sitk_im,
coordinate_system='PATIENT',
frame_of_reference_uid=None
)


.. _`ITK`: https://itk.org/
.. _`SimpleITK`: https://simpleitk.org/
60 changes: 10 additions & 50 deletions docs/volume.rst
Original file line number Diff line number Diff line change
Expand Up @@ -731,60 +731,20 @@ Writing a volume to a NIfTI file:
nifti_path = '/path/to/nifti.nii' # or .nii.gz
nib.save(nifti, nifti_path)

Volumes To/From ITK Images
--------------------------

`ITK`_ is a widely-used library for volumetric image processing. Its ``Image``
class shares many similarities with our :class:`highdicom.Volume` class. Like
highdicom, ITK uses the "LPS" convention. However, when converting to and from
NumPy arrays, ITK reverses the order of dimensions. It is important to account
for this when performing conversions.
Volumes To/From Other Libraries
-------------------------------

We plan to add tools to handle this conversion in the near future, but for now
these snippets should correctly handle simple situations converting to and from
ITK Images.

Creating a volume from an ITK Image:

.. code-block:: python

import itk
import numpy as np
import highdicom as hd


im = itk.image(...)

# Reverse array dimension order
array = itk.array_from_image(im).transpose([2, 1, 0])

vol2 = hd.Volume.from_components(
array=array,
direction=np.asarray(im.GetDirection()),
spacing=np.asarray(im.GetSpacing()),
position=np.asarray(im.GetOrigin()),
coordinate_system="PATIENT"
)

Creating an ITK Image from a Volume:

.. code-block:: python

import itk
import highdicom as hd


vol = hd.Volume(...)

# Reverse array dimension order
array = vol.array.transpose([2, 1, 0])
Many other libraries have classes that share similarities with our
:class:`highdicom.Volume` class and allow for simple conversions between
formats. For commonly used libraries, highdicom Volumes provide
``to_<library>()`` and ``from_<library>()`` methods to simplify moving to and
from other libraries as much as possible. For more information about each library,
visit the following:

im = itk.image_from_array(array)
im.SetOrigin(vol.position)
im.SetDirection(vol.direction)
im.SetSpacing(vol.spacing)
- :ref:`ITK <itk_vol>`
- :ref:`SimpleITK <sitk_vol>`


.. _`NIfTI`: https://nifti.nimh.nih.gov/
.. _`ITK`: https://itk.org/
.. _`nibabel`: https://nipy.org/nibabel/
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "highdicom"
version = "0.27.0"
version = "0.28.0"
description = "High-level DICOM abstractions."
readme = "README.md"
requires-python = ">=3.10"
Expand Down Expand Up @@ -39,6 +39,7 @@ dependencies = [
"pydicom>=3.0.1",
"pyjpegls>=1.0.0",
"typing-extensions>=4.0.0",
"packaging>=17.1"
]

[project.optional-dependencies]
Expand All @@ -47,6 +48,8 @@ libjpeg = [
"pylibjpeg-openjpeg>=2.0.0",
"pylibjpeg>=2.0",
]
itk = ["itk>=5.4.0"]
sitk = ["SimpleITK>=2.2.1"]

[dependency-groups]
test = [
Expand Down
64 changes: 64 additions & 0 deletions src/highdicom/_dependency_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from importlib import import_module, metadata
from packaging.requirements import Requirement
from types import ModuleType


def import_optional_dependency(
module_name: str,
feature: str
) -> ModuleType:
"""Import an optional dependency.

This function is designed to support interaction with other common
libraries that are not required for `highdicom` by default.

Parameters
----------
module_name: str
Name of the module to be imported.
feature: str
Name or description of the feature that requires this dependency.
This is used for improving the clarity of error messages.

Returns
-------
ModuleType:
Imported module.

Raises
------
ImportError:
When the specified module cannot be imported.

"""
for req_str in metadata.requires('highdicom'):
req = Requirement(req_str)
if req.name == module_name:
break

else:
raise ValueError(
f'`{module_name}` is not a requirement of highdicom '
f'but is required for {feature}.'
)

try:
module = import_module(name=module_name)

except ImportError as error:
raise ImportError(
f'Optional dependency `{module_name}` could not be imported'
f' but is required for {feature}.'
f' highdicom requires {module_name}{req.specifier}.'
) from error

installed_version = metadata.version(module_name)

if installed_version not in req.specifier:
raise ImportError(
f'Optional dependency `{module_name}` has an unsuitable '
f'version. Found {module_name}=={installed_version}, but '
f'highdicom requires {module_name}{req.specifier}.'
)

return module
2 changes: 1 addition & 1 deletion src/highdicom/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '0.27.0'
__version__ = '0.28.0'
Loading
Loading