Skip to content

ENH: Add SimpleITK <-> ITK image conversion to the Python interface (supersedes #6021) - #6811

Merged
hjmjohnson merged 1 commit into
InsightSoftwareConsortium:mainfrom
hjmjohnson:enh-image-from-simpleitk
Sep 2, 2026
Merged

ENH: Add SimpleITK <-> ITK image conversion to the Python interface (supersedes #6021)#6811
hjmjohnson merged 1 commit into
InsightSoftwareConsortium:mainfrom
hjmjohnson:enh-image-from-simpleitk

Conversation

@hjmjohnson

Copy link
Copy Markdown
Member

Adds itk.image_from_simpleitk() and itk.simpleitk_from_image(), and teaches the filter decorator to accept a SimpleITK image wherever it accepts a NumPy array. Supersedes #6021 (@blowekamp), which stalled on the axis-order conflict that #6710 has since resolved.

No new dependency. SimpleITK is imported lazily inside the functions, so ITK builds and imports without it. The in-tree test uses a duck-typed stub and never imports SimpleITK — SimpleITK's superbuild builds ITK, so a test requiring it would close a cycle in the build graph, and SimpleITK is not present in ITK CI.

Related Status
Original PR this supersedes #6021
Axis-order conflict #6706
Order-explicit keys this builds on #6710
SimpleITK-side dual (open question) SimpleITK#2531
Why the bare spacing key cannot be used

image['spacing'] means (z, y, x) on an itk.Image and (x, y, z) on a SimpleITK Image. A converter reading it from one and writing it to the other silently reverses the spacing — the conflict reported in #6706, and what blocked #6021.

Geometry is therefore read through the order-explicit spacing_xyz / origin_xyz / direction_xyz keys from #6710, falling back to GetSpacing() / GetOrigin() / GetDirection(). Both are unambiguous.

Note that SimpleITK exposes no order-explicit keys today, so the accessor fallback carries every conversion; the key path is forward-looking, and starts working by itself if SimpleITK#2531 lands. A test asserts the two bare-key conventions still disagree, so this fails loudly rather than drifting if either toolkit changes.

Concerns from #6021, and how each is resolved
Concern Raised by Resolution
P1 — fallback never read GetSpacing()/GetOrigin()/GetDirection(); SimpleITK exposes methods, not dict keys. Vector images treated as scalar, geometry left at ITK defaults greptile _spatial_from_order_explicit() tries the _xyz key, then the accessor. Components come from GetNumberOfComponentsPerPixel(); dim from GetDimension(), not array.ndim
Missing image buffer start index @hjmjohnson (CHANGES_REQUESTED) A non-zero buffered-region index is carried as ITK_original_index and restored by the inverse. SimpleITK images always start at 0, so it cannot be represented directly
Store ImageRegion.m_Index as metadata @hjmjohnson / @blowekamp Implemented as above. The earlier "not needed" applied to sitk→itk, where SimpleITK enforces a zero index; this PR adds the itk→sitk direction, where it is needed

Metadata: every key is copied in both directions, and ITK_-prefixed entries are guaranteed to survive — a conversion failure on one of those raises rather than being silently dropped.

Testing

macOS 15 arm64, Release.

  • PythonSimpleITKProtocolTestPassed via ctest against this commit. Runs with SimpleITK present and with it blocked, confirming no hard dependency.
  • Full ITK Python suite — 167/176. The 9 failures are pre-existing: the identical 9 test numbers fail with pristine upstream extras.py/helpers.py. Causes are a broken VTK @rpath and a wrapping predating 002eebce7c4, neither related to this change.
  • Cross-toolkit round trip — 16/16 with ITK 6.0.0 and SimpleITK 3.0.0b1 built against that same ITK, in one interpreter. Covers geometry, vector, 2-D, pixels, metadata, index round trip, and physical-point agreement between the toolkits (which catches a transposed direction matrix that element-wise comparison misses). That suite lives outside both projects so neither gains a dependency.
  • pre-commit run --all-files passes on the branch tip.

Not validated on Linux or Windows.

@github-actions github-actions Bot added type:Infrastructure Infrastructure/ecosystem related changes, such as CMake or buildbots type:Enhancement Improvement of existing methods or implementation area:Python wrapping Python bindings for a class type:Testing Ensure that the purpose of a class is met/the results on a wide set of test cases are correct labels Aug 27, 2026
@hjmjohnson

Copy link
Copy Markdown
Member Author

@greptileai review

@greptile-apps

This comment was marked as resolved.

Comment thread Wrapping/Generators/Python/itk/support/extras.py Outdated
Comment thread Wrapping/Generators/Python/itk/support/extras.py
@hjmjohnson
hjmjohnson force-pushed the enh-image-from-simpleitk branch from 769fa94 to 46afb95 Compare August 28, 2026 00:52
@hjmjohnson
hjmjohnson marked this pull request as ready for review August 28, 2026 00:57

@thewtex thewtex left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM!

@thewtex
thewtex requested a review from blowekamp August 28, 2026 17:52
@blowekamp

blowekamp commented Aug 31, 2026

Copy link
Copy Markdown
Member

Thank for working on this and moving it forward!

Do you have an active need to support the round trip origin that adds those special cases to this conversion code? Maybe this is not a requirement of these methods? [ EDIT: Additionally if the buffered region is not the largest possible region, then in this current form the round trip image has different region information.]

I am still mulling over adding the _zyx _zyx keys to the SimpleITK class. The extra specification and verbosity is nice and clarifies things. It is unfortunate that the keys between ITK and SimpleITK are not natively compatible. But for SimpleITK maybe it would be simpler to just have the one consistent representation. I am still mulling over the best corse of action here.

Thanks again!

@hjmjohnson

hjmjohnson commented Aug 31, 2026

Copy link
Copy Markdown
Member Author

Thank for working on this and moving it forward!

Do you have an active need to support the round-trip origin that adds those special cases to this conversion code? Maybe this is not a requirement of these methods? [ EDIT: Additionally, if the buffered region is not the largest possible region, then in this current form the round trip image has different region information.]

@blowekamp ,

I do not have an active case because this was unusable previously :). I was trying to be complete and hoped for parity with C++ in the new features being added. I had missed considering the Buffer != Largest case, and I think we should throw a Python exception in that slightly unusual case for now (at least by default).

Hans

@hjmjohnson

Copy link
Copy Markdown
Member Author

@blowekamp Filed your buffered-vs-largest observation as #6826, milestoned for ITK 6.0 RC1 so the guard lands before the API is frozen.

Why RC1 rather than the backlog

Both functions are new here and unreleased, so adding a ValueError when BufferedRegion != LargestPossibleRegion costs nothing today. After 6.0.0 final the same change turns working user code into throwing code, which is the expensive kind of break — so the window is open but closing.

The frequency is genuinely low: array_from_image defaults to update=True, so any pipeline-attached image re-buffers to the requested region and the mismatch is erased before it can be observed. It survives for standalone images with explicitly set regions, deliberate streaming, or a reader with SetIORegion. Pixel values and physical locations are correct in all cases; only the extent metadata is lost. The issue carries a verified before/after transcript.

Your _zyx key question is noted there as a separate design discussion rather than folded into the guard.

@dzenanz

dzenanz commented Sep 1, 2026

Copy link
Copy Markdown
Member

Can this be merged, or are some further changes needed/desired (e.g. throw when regions are not all equal)?

@blowekamp

Copy link
Copy Markdown
Member

I don't think this round trip preservation of ITK meta-data and regions are useful and should be removed from the design goals of this feature.

No filters copy meta-data from input to output. This preservation feature will not be frequently used because the meta-data will only be preserved in the trivial copy in and out or the limited "in-place" operations available.

Keep things simple and remove unnecessary features that complicate behavior and could cause problems.

@hjmjohnson

Copy link
Copy Markdown
Member Author

I don't think this round trip preservation of ITK meta-data and regions are useful and should be removed from the design goals of this feature.

I'm going to remove for now to get the minimal needed work in place. I diagree that the ability to preserve round trip meta-data and regions is useful. It was the "criteria for complete success" that I had in mind when first thinking about this. We are not there yet, and we can take that up later when more funding and resources are available.

itk.image_from_simpleitk() and itk.simpleitk_from_image() convert between the
two toolkits, and the filter decorator accepts a SimpleITK image wherever it
accepts a NumPy array.

Geometry uses the order-explicit spatial keys from InsightSoftwareConsortium#6710, falling back to the
Get*() accessors. The bare 'spacing' key means (z,y,x) on an itk.Image and
(x,y,z) on a SimpleITK Image, so reading it would reverse the spacing (InsightSoftwareConsortium#6706).

SimpleITK images start at index 0, so the origin moves to the first stored
voxel and the index is carried as ITK_original_index; the inverse restores
both, keeping the pixels in the same physical location.

Supersedes InsightSoftwareConsortium#6021.

Co-Authored-By: Bradley Lowekamp <321061+blowekamp@users.noreply.github.com>
Co-Authored-By: Matt McCormick <25432+thewtex@users.noreply.github.com>
Co-Authored-By: Dzenan Zukic <1792121+dzenanz@users.noreply.github.com>
@hjmjohnson
hjmjohnson force-pushed the enh-image-from-simpleitk branch from 46afb95 to f831d4d Compare September 2, 2026 00:47
@hjmjohnson

Copy link
Copy Markdown
Member Author

Pushed f831d4d: round-trip preservation removed, and simpleitk_from_image now raises when the buffered region differs from the largest possible region rather than silently exporting the smaller extent. That covers both @blowekamp's request and @dzenanz's question.

What was removed, and what deliberately stayed

Removed (−27 lines in extras.py):

  • the ITK_original_index key and the block in image_from_simpleitk that restored the start index, un-shifted the origin, and erased the key
  • the block in simpleitk_from_image that recorded and cleared that key
  • the ITK_-prefix raise rule in the metadata copy, which existed only to guarantee those keys survived a round trip

Kept:

  • the origin shift — this is geometric correctness, not round-trip machinery. The origin is set to the physical location of the first buffered voxel, so a non-zero-index image's pixels keep their position instead of moving. Dropping it would reintroduce the coordinate shift fixed earlier in this PR.
  • a plain metadata-dictionary copy, now best-effort: entries SimpleITK cannot store as strings are skipped rather than raising
  • the order-explicit spacing_xyz / origin_xyz / direction_xyz reading, which is unrelated
The guard

Placed after array_from_image, which calls Update() — so the regions compared are the ones whose data was just read, not a pre-update snapshot.

ValueError: cannot convert an image whose buffered region differs from its largest
possible region: buffered index [2, 2, 2] size [4, 4, 4], largest index [0, 0, 0]
size [10, 10, 10]. A SimpleITK image holds one extent, so the difference cannot be
represented. Update the image over its largest possible region before converting.

Verified that it fires on a mismatched image and does not fire on a normally-allocated one. This is what #6826 proposed, so that issue is addressed here.

@blowekamp blowekamp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thank you for considering my comments.

@hjmjohnson
hjmjohnson merged commit 9a82e23 into InsightSoftwareConsortium:main Sep 2, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:Python wrapping Python bindings for a class type:Enhancement Improvement of existing methods or implementation type:Infrastructure Infrastructure/ecosystem related changes, such as CMake or buildbots type:Testing Ensure that the purpose of a class is met/the results on a wide set of test cases are correct

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants