ENH: Add SimpleITK Image to ITK Image conversion support - #6021
Conversation
|
@blowekamp I made a note this morning that this was going to be needed :)! You must have heard my thoughts! |
|
@dzenanz Raised this issue here: SimpleITK/SimpleITK#2531 |
hjmjohnson
left a comment
There was a problem hiding this comment.
Can you comment on the missing "Image buffer start index" that is missing?
- 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
f3b51ac to
98916f9
Compare
|
This feature ran into a issue/block with the ITK dictionary keys unexpectedly return attributes in numpy order and not ITK order. |
|
I replicated the ordering blocker with released wheels (itk 5.4.6 + SimpleITK 2.5.5): ITK's Standalone replication script (needs only Replication results3-D image, spacing (1, 2, 3), origin (5, 10, 15) in x,y,z:
So ITK is also internally inconsistent: The Feeding this PR's generic-reader pattern (read Separate finding: against released SimpleITK 2.5.5, Possible paths forward
|
|
| Filename | Overview |
|---|---|
| Wrapping/Generators/Python/Tests/extras.py | Adds SimpleITK conversion coverage for scalar, vector, metadata, and filter auto-conversion paths. |
| Wrapping/Generators/Python/itk/support/extras.py | Adds image_from_simpleitk, but real SimpleITK spatial metadata is not read through the implemented generic key fallback. |
| Wrapping/Generators/Python/itk/support/helpers.py | Extends the array-like conversion decorator to accept SimpleITK images and return ITK outputs without reconversion. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Caller
participant Decorator as accept_array_like_xarray_torch
participant Converter as itk.image_from_simpleitk
participant Filter as ITK filter
Caller->>Decorator: filter(SimpleITK.Image)
Decorator->>Converter: convert input image
Converter-->>Decorator: itk.Image / itk.VectorImage
Decorator->>Filter: invoke with ITK image
Filter-->>Decorator: ITK output
Decorator-->>Caller: ITK output
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Caller
participant Decorator as accept_array_like_xarray_torch
participant Converter as itk.image_from_simpleitk
participant Filter as ITK filter
Caller->>Decorator: filter(SimpleITK.Image)
Decorator->>Converter: convert input image
Converter-->>Decorator: itk.Image / itk.VectorImage
Decorator->>Filter: invoke with ITK image
Filter-->>Decorator: ITK output
Decorator-->>Caller: ITK output
Comments Outside Diff (1)
-
General comment
image_from_simpleitk does not obtain pixel arrays from real SimpleITK images
- Bug
- The generated PR test-plan harness used a real
SimpleITK.Image([12, 8], sitk.sitkFloat32)and executed the changed repo implementation ofimage_from_simpleitk(). It failed immediately duringitk.image_view_from_array(array), before metadata or filter auto-conversion could be validated. The stack trace shows ITK attempteditk.Image[itk.D, 1], which is consistent withnp.array(sitk_image)not returning the expected 2D float pixel buffer for SimpleITK images.
- The generated PR test-plan harness used a real
- Cause
image_from_simpleitk()reads pixels witharray = np.array(sitk_image). Real SimpleITK images do not expose their pixel buffer through NumPy's generic array protocol in the required shape/dtype; SimpleITK's supported API isSimpleITK.GetArrayFromImage()/GetArrayViewFromImage(). As a result, scalar 2D images are misinterpreted as a 1D double/object-like array and conversion fails.
- Fix
- Use SimpleITK's image array API when the input is a SimpleITK image, e.g.
sitk.GetArrayFromImage(sitk_image), while preserving the intended vector-image channel layout. Keep the generic path only for objects that actually provide a valid ndarray via__array__, and add a runtime test that checks the array shape/dtype before callingitk.image_view_from_array().
- Use SimpleITK's image array API when the input is a SimpleITK image, e.g.
- Bug
Reviews (1): Last reviewed commit: "ENH: Add SimpleITK Image to ITK Image co..." | Re-trigger Greptile
| else: | ||
| # Probe spatial keys directly (e.g. SimpleITK 3.x supports [] but not keys()) | ||
| keys = [] | ||
| for k in spatial_keys: | ||
| try: | ||
| sitk_image[k] | ||
| keys.append(k) | ||
| except (KeyError, TypeError, AttributeError): | ||
| pass | ||
| # Collect MetaData keys via dedicated accessor if available | ||
| if hasattr(sitk_image, "GetMetaDataKeys"): | ||
| keys.extend(sitk_image.GetMetaDataKeys()) | ||
|
|
||
| if "spacing" in keys: | ||
| spacing = sitk_image["spacing"] | ||
| dim = len(spacing) |
There was a problem hiding this comment.
Use SimpleITK getters
The fallback path never reads GetSpacing(), GetOrigin(), or GetDirection() from real SimpleITK.Image objects, because SimpleITK exposes those as methods rather than image['spacing'] / keys(). With only GetMetaDataKeys() added to keys, spacing is absent, so dim stays as array.ndim, vector images are treated as scalar 3D images, and spatial metadata is left at ITK defaults; the new spacing/origin/direction tests fail against current SimpleITK.
Context Used: AGENTS.md (source)
Artifacts
- Contains supporting evidence from the run (text/x-python; charset=utf-8).
- Keeps the command output available without making the summary code-heavy.
I think this is still important. |
|
It would be good if this was finished before the next tag. |
If feels like we might need a short meeting to discuss the pros/cons of the various possible solutions, and once the solution is picked, it will be very easy to implement. My reading is we are stuck at a point of two conflicting design decisions (support numpy native, support ITK/simpleITK native conventions). Perhaps we need to break both and have a new naming scheme. "numpy_spacing" = reverse ("itk_spacing") |
|
@blowekamp — thank you for this, and apologies it sat so long. The conversion logic here is the basis of what finally landed as a follow-on, and you are credited as co-author on the commit. The blocker was never this PR's approach; it was that the underlying dict protocol was ambiguous. #6811 carries your implementation forward with the review concerns on this PR addressed:
Also worth flagging for your question on SimpleITK/SimpleITK#2531 ("is there a generic way we can support conversion between ITK Python and SimpleITK — can both implementations accept an array buffer and these keys?"): ITK's half is answered, but SimpleITK exposes no order-explicit keys as of 3.0.0b1, so the accessor fallback carries every conversion today. If SimpleITK adds them, the key path starts working with no code change. Verified end to end against SimpleITK 3.0.0b1 built with Suggest closing this in favor of #6811, but leaving that to you. |
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>
This PR introduces a "generic" interface for an "itk-like" python image object. The itk-like object can be converted to a numpy array and contains "spacing", "direction", and "origin" keys. This should be an interface both ITK and SimpleITK images provide.
Any thoughts on this type of interface?
PR Checklist
Refer to the ITK Software Guide for
further development details if necessary.