-
Notifications
You must be signed in to change notification settings - Fork 51
Feature/itk volume conversions #394
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v0.28.0dev
Are you sure you want to change the base?
Changes from all commits
5023720
c5f7398
feef10b
bc41ecf
ce4998a
5c5e952
1ce1bc7
bdd3639
b4cd240
36a8fb5
7ff4a13
efb70aa
1232204
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| """Representations of multidimensional arrays with spatial metadata.""" | ||
| from __future__ import annotations | ||
| from abc import ABC, abstractmethod | ||
| from enum import Enum | ||
| import itertools | ||
|
|
@@ -44,6 +45,7 @@ | |
| keyword_for_tag, | ||
| ) | ||
|
|
||
| from highdicom._dependency_utils import import_optional_dependency | ||
|
|
||
| _DCM_PYTHON_TYPE_MAP = { | ||
| 'CS': str, | ||
|
|
@@ -3583,6 +3585,137 @@ def pad_array(array: np.ndarray, cval: float) -> float: | |
| channels=self._channels, | ||
| ) | ||
|
|
||
| def to_sitk(self) -> SimpleITK.Image: | ||
| """Convert the Volume to `SimpleITK.Image` format. | ||
|
|
||
| Returns | ||
| ------- | ||
| SimpleITK.Image: | ||
| Image constructed from the Volume. | ||
|
|
||
| """ | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be nice to add some more information to the docstring to help users:
|
||
| func = self.to_sitk | ||
| sitk = import_optional_dependency( | ||
| module_name="SimpleITK", | ||
| feature=f"{func.__module__}.{func.__qualname__}" | ||
| ) | ||
|
|
||
| array = self.array.transpose(2, 1, 0) | ||
|
|
||
| if self.dtype == np.bool_: | ||
| array = array.astype(int) | ||
|
|
||
| sitk_im = sitk.GetImageFromArray(array) | ||
| sitk_im.SetSpacing(self.spacing) | ||
| sitk_im.SetDirection(self.direction.flatten()) | ||
| sitk_im.SetOrigin(self.position) | ||
|
|
||
| return sitk_im | ||
|
|
||
|
|
||
| @classmethod | ||
| def from_sitk( | ||
| cls, | ||
| sitk_im: SimpleITK.Image | ||
| ): | ||
|
Comment on lines
+3617
to
+3620
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should expose
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also missing type hint on the return |
||
| """Construct a Volume from a `SimpleITK.Image`. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| sitk_im: SimpleITK.Image | ||
| A `SimpleITK.Image` to convert to a volume. | ||
|
|
||
| Returns | ||
| ------- | ||
| highdicom.Volume: | ||
| Volume constructed from the `SimpleITK.Image`. | ||
|
|
||
| """ | ||
| func = cls.from_sitk | ||
| sitk = import_optional_dependency( | ||
| module_name="SimpleITK", | ||
| feature=f"{func.__module__}.{func.__qualname__}" | ||
| ) | ||
|
|
||
| array = sitk.GetArrayFromImage(sitk_im).transpose(2, 1, 0) | ||
|
|
||
| if array.dtype == np.bool_: | ||
| array = array.astype(int) | ||
|
|
||
| return cls.from_components( | ||
| array=array, | ||
| spacing=sitk_im.GetSpacing(), | ||
| coordinate_system="PATIENT", | ||
| direction=np.reshape(sitk_im.GetDirection(), (3, 3)), | ||
| position=sitk_im.GetOrigin() | ||
| ) | ||
|
|
||
|
|
||
| def to_itk(self) -> itk.Image: | ||
| """Convert the volume to `itk.Image` format. | ||
|
|
||
| Returns | ||
| ------- | ||
| itk.Image: | ||
| Image constructed from the volume. | ||
|
|
||
| """ | ||
| func = self.to_itk | ||
| itk = import_optional_dependency( | ||
| module_name="itk", | ||
| feature=f"{func.__module__}.{func.__qualname__}" | ||
| ) | ||
|
|
||
| array = self.array | ||
|
|
||
| if self.dtype == np.bool_: | ||
| array = array.astype(int) | ||
|
|
||
| itk_im = itk.GetImageFromArray(array) | ||
| itk_im.SetSpacing(self.spacing) | ||
| itk_im.SetDirection(self.direction) | ||
| itk_im.SetOrigin(self.position) | ||
|
|
||
| return itk_im | ||
|
|
||
|
|
||
| @classmethod | ||
| def from_itk( | ||
| cls, | ||
| itk_im: itk.Image | ||
| ): | ||
| """Construct a Volume from an `itk.Image`. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| itk_im: itk.Image | ||
| A `itk.Image` to convert to a volume. | ||
|
|
||
| Returns | ||
| ------- | ||
| highdicom.Volume: | ||
| Volume constructed from the `itk.Image`. | ||
|
|
||
| """ | ||
| func = cls.from_itk | ||
| itk = import_optional_dependency( | ||
| module_name="itk", | ||
| feature=f"{func.__module__}.{func.__qualname__}" | ||
| ) | ||
|
|
||
| array = itk.GetArrayFromImage(itk_im) | ||
|
|
||
| if array.dtype == np.bool_: | ||
| array = array.astype(int) | ||
|
|
||
| return cls.from_components( | ||
| array=array, | ||
| spacing=np.array(itk_im.GetSpacing()), | ||
| coordinate_system="PATIENT", | ||
| direction=np.reshape(itk_im.GetDirection(), (3, 3)), | ||
| position=np.array(itk_im.GetOrigin()) | ||
| ) | ||
|
|
||
|
|
||
| class VolumeToVolumeTransformer: | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this type hint will fail at runtime if SimpleITK is not installed. I think this would work though: