Skip to content

Commit f3b51ac

Browse files
committed
ENH: Add SimpleITK Image to ITK Image conversion support
- Implement image_from_simpleitk() function for converting SimpleITK images to ITK images - Support 2D, 3D, and multi-component (vector) images - Preserve spacing, origin, and direction metadata - Preserve MetaData dictionary entries - Integrate SimpleITK support into filter auto-conversion decorator - Add comprehensive tests for conversion functionality
1 parent 293cd47 commit f3b51ac

3 files changed

Lines changed: 188 additions & 5 deletions

File tree

Wrapping/Generators/Python/Tests/extras.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -655,3 +655,76 @@ def assert_images_equal(base, test):
655655
except ImportError:
656656
print("vtk not imported. Skipping vtk conversion tests")
657657
pass
658+
659+
# Test SimpleITK to ITK conversion
660+
try:
661+
import SimpleITK as sitk
662+
663+
print("Testing SimpleITK conversion")
664+
665+
# Test 2D scalar image
666+
sitk_image = sitk.Image([12, 8], sitk.sitkFloat32)
667+
sitk_image.SetSpacing([0.5, 1.5])
668+
sitk_image.SetOrigin([10.0, 20.0])
669+
theta = np.radians(30)
670+
cosine = np.cos(theta)
671+
sine = np.sin(theta)
672+
sitk_image.SetDirection([cosine, -sine, sine, cosine])
673+
# Fill with known data
674+
arr_2d = np.random.rand(8, 12).astype(np.float32)
675+
for y in range(8):
676+
for x in range(12):
677+
sitk_image[x, y] = float(arr_2d[y, x])
678+
679+
itk_image = itk.image_from_simpleitk(sitk_image)
680+
assert np.array_equal(
681+
np.array(sitk_image.GetSpacing()), np.array(itk.spacing(itk_image))
682+
)
683+
assert np.array_equal(
684+
np.array(sitk_image.GetOrigin()), np.array(itk.origin(itk_image))
685+
)
686+
sitk_dir = np.array(sitk_image.GetDirection()).reshape(2, 2)
687+
itk_dir = itk.array_from_matrix(itk_image.GetDirection())
688+
assert np.allclose(sitk_dir, itk_dir)
689+
assert np.array_equal(
690+
sitk.GetArrayFromImage(sitk_image), itk.array_from_image(itk_image)
691+
)
692+
693+
# Test 3D scalar image
694+
sitk_3d = sitk.Image([4, 6, 8], sitk.sitkFloat32)
695+
sitk_3d.SetSpacing([1.0, 2.0, 3.0])
696+
sitk_3d.SetOrigin([5.0, 10.0, 15.0])
697+
itk_3d = itk.image_from_simpleitk(sitk_3d)
698+
assert np.array_equal(np.array(sitk_3d.GetSpacing()), np.array(itk.spacing(itk_3d)))
699+
assert np.array_equal(np.array(sitk_3d.GetOrigin()), np.array(itk.origin(itk_3d)))
700+
assert itk_3d.GetImageDimension() == 3
701+
702+
# Test vector image (multi-component -> VectorImage)
703+
sitk_vector = sitk.Image([10, 10], sitk.sitkVectorFloat32, 3)
704+
sitk_vector.SetSpacing([0.8, 1.2])
705+
itk_vector = itk.image_from_simpleitk(sitk_vector)
706+
assert itk_vector.GetNumberOfComponentsPerPixel() == 3
707+
assert isinstance(itk_vector, itk.VectorImage[itk.F, 2])
708+
assert np.array_equal(
709+
np.array(sitk_vector.GetSpacing()), np.array(itk.spacing(itk_vector))
710+
)
711+
712+
# Test MetaDataDictionary preservation
713+
sitk_meta = sitk.Image([4, 4], sitk.sitkFloat32)
714+
sitk_meta.SetMetaData("test_key", "test_value")
715+
sitk_meta.SetMetaData("0010|0010", "patient_name")
716+
itk_meta = itk.image_from_simpleitk(sitk_meta)
717+
meta_dict = itk_meta.GetMetaDataDictionary()
718+
assert meta_dict["test_key"] == "test_value"
719+
assert meta_dict["0010|0010"] == "patient_name"
720+
721+
# Test auto-detection in filter function
722+
sitk_input = sitk.Image([32, 32], sitk.sitkFloat32)
723+
result = itk.median_image_filter(sitk_input, radius=1)
724+
assert isinstance(result, itk.Image[itk.F, 2])
725+
726+
print("SimpleITK conversion tests passed")
727+
728+
except ImportError:
729+
print("SimpleITK not imported. Skipping SimpleITK conversion tests")
730+
pass

Wrapping/Generators/Python/itk/support/extras.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
"image_from_xarray",
9494
"vtk_image_from_image",
9595
"image_from_vtk_image",
96+
"image_from_simpleitk",
9697
"dict_from_image",
9798
"image_from_dict",
9899
"image_intensity_min_max",
@@ -788,6 +789,88 @@ def image_from_vtk_image(vtk_image: "vtk.vtkImageData") -> "itkt.ImageBase":
788789
return l_image
789790

790791

792+
def image_from_simpleitk(sitk_image) -> "itkt.ImageBase":
793+
"""Convert a SimpleITK Image to an itk.Image.
794+
795+
The source image is accessed through generic interfaces: the NumPy
796+
``__array__`` protocol for pixel data, ``keys()`` for available
797+
metadata keys, and dictionary-style ``[]`` access for metadata
798+
values. The recognised spatial keys are ``'spacing'``,
799+
``'origin'``, and ``'direction'``; all other keys returned by
800+
``keys()`` are copied into the ITK MetaDataDictionary.
801+
802+
This makes the function forward-compatible with other image
803+
libraries that expose the same conventions (e.g. SimpleITK).
804+
805+
Pixel data is copied once (SimpleITK buffers are read-only).
806+
Multi-component images are converted to itk.VectorImage.
807+
808+
Parameters
809+
----------
810+
sitk_image :
811+
A SimpleITK Image object (or any object that supports
812+
``__array__``, ``keys()``, and dictionary ``[]`` access).
813+
814+
Returns
815+
-------
816+
image :
817+
The resulting itk.Image (or itk.VectorImage for multi-component pixels).
818+
"""
819+
import itk
820+
821+
array = np.array(sitk_image)
822+
dim = array.ndim
823+
824+
spatial_keys = {"spacing", "origin", "direction"}
825+
826+
if hasattr(sitk_image, "keys"):
827+
keys = list(sitk_image.keys())
828+
else:
829+
# Probe spatial keys directly (e.g. SimpleITK 3.x supports [] but not keys())
830+
keys = []
831+
for k in spatial_keys:
832+
try:
833+
sitk_image[k]
834+
keys.append(k)
835+
except (KeyError, TypeError, AttributeError):
836+
pass
837+
# Collect MetaData keys via dedicated accessor if available
838+
if hasattr(sitk_image, "GetMetaDataKeys"):
839+
keys.extend(sitk_image.GetMetaDataKeys())
840+
841+
if "spacing" in keys:
842+
spacing = sitk_image["spacing"]
843+
dim = len(spacing)
844+
845+
is_vector = array.ndim > dim
846+
847+
if is_vector:
848+
PixelType = _get_itk_pixelid(array)
849+
ImageType = itk.VectorImage[PixelType, dim]
850+
l_image = itk.image_view_from_array(array, ttype=ImageType)
851+
else:
852+
l_image = itk.image_view_from_array(array)
853+
854+
if "spacing" in keys:
855+
l_image.SetSpacing(spacing)
856+
857+
if "origin" in keys:
858+
l_image.SetOrigin(sitk_image["origin"])
859+
860+
if "direction" in keys:
861+
direction = np.array(sitk_image["direction"]).reshape(dim, dim)
862+
l_image.SetDirection(direction)
863+
864+
for key in keys:
865+
if key not in spatial_keys:
866+
l_image.GetMetaDataDictionary()[key] = sitk_image[key]
867+
868+
# Keep a reference to the numpy array to prevent garbage collection
869+
l_image._SetBase(array)
870+
871+
return l_image
872+
873+
791874
def dict_from_image(image: "itkt.Image") -> dict:
792875
"""Serialize a Python itk.Image object to a pickable Python dictionary."""
793876
import itk

Wrapping/Generators/Python/itk/support/helpers.py

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@
3838
_HAVE_TORCH = True
3939
except importlib.metadata.PackageNotFoundError:
4040
pass
41+
_HAVE_SIMPLEITK = False
42+
try:
43+
metadata("SimpleITK")
44+
45+
_HAVE_SIMPLEITK = True
46+
except (ImportError, importlib.metadata.PackageNotFoundError):
47+
pass
4148

4249

4350
def snake_to_camel_case(keyword: str):
@@ -92,11 +99,13 @@ def move_last_dimension_to_first(arr):
9299

93100
def accept_array_like_xarray_torch(image_filter):
94101
"""Decorator that allows itk.ProcessObject snake_case functions to accept
95-
NumPy array-like, PyTorch Tensor's or xarray DataArray inputs for itk.Image inputs.
102+
NumPy array-like, PyTorch Tensor's, xarray DataArray, or SimpleITK Image
103+
inputs for itk.Image inputs.
96104
97105
If a NumPy array-like is passed as an input, output itk.Image's are converted to numpy.ndarray's.
98106
If a torch.Tensor is passed as an input, output itk.Image's are converted to torch.Tensors.
99107
If a xarray DataArray is passed as an input, output itk.Image's are converted to xarray.DataArray's.
108+
If a SimpleITK Image is passed as an input, output itk.Image's are returned as-is.
100109
"""
101110
import numpy as np
102111
import itk
@@ -105,16 +114,23 @@ def accept_array_like_xarray_torch(image_filter):
105114
import xarray as xr
106115
if _HAVE_TORCH:
107116
import torch
117+
if _HAVE_SIMPLEITK:
118+
import SimpleITK as sitk
108119

109120
@functools.wraps(image_filter)
110121
def image_filter_wrapper(*args, **kwargs):
111122
have_array_input = False
112123
have_xarray_input = False
113124
have_torch_input = False
125+
have_simpleitk_input = False
114126

115127
args_list = list(args)
116128
for index, arg in enumerate(args):
117-
if _HAVE_XARRAY and isinstance(arg, xr.DataArray):
129+
if _HAVE_SIMPLEITK and isinstance(arg, sitk.Image):
130+
have_simpleitk_input = True
131+
image = itk.image_from_simpleitk(arg)
132+
args_list[index] = image
133+
elif _HAVE_XARRAY and isinstance(arg, xr.DataArray):
118134
have_xarray_input = True
119135
image = itk.image_from_xarray(arg)
120136
args_list[index] = image
@@ -135,7 +151,11 @@ def image_filter_wrapper(*args, **kwargs):
135151
potential_image_input_kwargs = ("input", "input1", "input2", "input3")
136152
for key, value in kwargs.items():
137153
if key.lower() in potential_image_input_kwargs or "image" in key.lower():
138-
if _HAVE_XARRAY and isinstance(value, xr.DataArray):
154+
if _HAVE_SIMPLEITK and isinstance(value, sitk.Image):
155+
have_simpleitk_input = True
156+
image = itk.image_from_simpleitk(value)
157+
kwargs[key] = image
158+
elif _HAVE_XARRAY and isinstance(value, xr.DataArray):
139159
have_xarray_input = True
140160
image = itk.image_from_xarray(value)
141161
kwargs[key] = image
@@ -155,9 +175,16 @@ def image_filter_wrapper(*args, **kwargs):
155175
image = itk.image_view_from_array(array)
156176
kwargs[key] = image
157177

158-
if have_xarray_input or have_torch_input or have_array_input:
159-
# Convert output itk.Image's to numpy.ndarray's
178+
if (
179+
have_simpleitk_input
180+
or have_xarray_input
181+
or have_torch_input
182+
or have_array_input
183+
):
184+
# Convert output itk.Image's based on input type
160185
output = image_filter(*tuple(args_list), **kwargs)
186+
if have_simpleitk_input:
187+
return output
161188
if isinstance(output, tuple):
162189
output_list = list(output)
163190
for index, value in enumerate(output_list):

0 commit comments

Comments
 (0)