InsightSoftwareConsortium / InsightSoftwareConsortium/ITK
SimpleITK conversion silently drops LargestPossibleRegion when the buffered region is smaller
- Dominant language
- C++
- Stars
- 1.7k
- Forks
- 748
- Avg merge
- 1d 1h
- Merged PRs (30d)
- 64
Description
`simpleitk_from_image` / `image_from_simpleitk` (added in #6811) preserve pixel data, spacing,
direction, origin and the buffered start index across a round trip, but not the
`LargestPossibleRegion`. When an image's buffered region is smaller than its largest possible
region, the round trip silently returns an image whose largest possible region has been shrunk to
the buffered one.
**This should land before 6.0.0 final.** Both functions are new in #6811 and unreleased, so adding
a guard now costs nothing. After 6.0.0 ships, making these calls raise turns working user code into
throwing code — a behavior break of a released API.
Raised by @blowekamp on #6811:
> Additionally if the buffered region is not the largest possible region, then in this current form
> the round trip image has different region information.
## Mechanism
SimpleITK images carry a single region: size, origin, spacing, direction, always starting at index
zero. ITK images carry three. The conversion can therefore represent at most one ITK region, and it
represents the buffered one.
In `simpleitk_from_image` (`Wrapping/Generators/Python/itk/support/extras.py`):
```python
array = itk.array_from_image(image) # buffered data only -> SimpleITK size == buffered size
...
start_index = tuple(image.GetBufferedRegion().GetIndex()) # recorded as ITK_original_index
```
The largest possible region is never recorded. In `image_from_simpleitk`:
```python
region = l_image.GetLargestPossibleRegion() # == the SimpleITK size, i.e. the buffered size
region.SetIndex(index)
l_image.SetRegions(region) # sets largest == buffered == requested
```
`SetRegions` assigns all three regions, so the restored image reports the buffered extent as its
largest possible region.
## Demonstration
Running the same operations the two functions perform, on ITK 5.4.6:
```
SOURCE largest size : [10, 10, 10] index [0, 0, 0]
SOURCE buffered size: [4, 4, 4] index [2, 2, 2]
array_from_image shape (zyx): (4, 4, 4)
ROUNDTRIP largest size : [4, 4, 4] index [2, 2, 2]
ROUNDTRIP buffered size: [4, 4, 4] index [2, 2, 2]
```
Pixel values and physical locations survive; the largest possible region does not.
## Why the frequency is low
`array_from_image` defaults to `update=True`, so it calls `Update()` on the image. For any image
attached to a pipeline that re-buffers to the requested region — normally the largest possible
region — and the mismatch is erased before it can be observed. The ordering in
`simpleitk_from_image` is also correct: `start_index` is read after the array extraction, so the
recorded index always matches the data actually copied.
The mismatch therefore survives only for a standalone image whose regions were set explicitly (no
source, so `Update()` is a no-op), a deliberately streamed pipeline with a smaller requested
region, or a reader configured with `SetIORegion`. Pixel values and physical locations are correct
in every case; only extent metadata is lost.
## Proposed remedy
Detect the mismatch and raise rather than silently discard, per @hjmjohnson on #6811:
> 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).
In `simpleitk_from_image`, compare `GetBufferedRegion()` with `GetLargestPossibleRegion()` and
raise a `ValueError` naming both extents when they differ. Failing loudly is preferable to
returning an image whose region information disagrees with its source.
An opt-out argument (accepting the buffered extent as the whole image) would cover the streaming
case for callers who know that is what they want. Recording the largest possible region as a second
metadata key and restoring it is a further option, but it makes the SimpleITK image's own size
disagree with the region it advertises, so it needs its own design discussion — as does
@blowekamp's separate question about `_zyx` keys on the SimpleITK side.
## Scope
`Wrapping/Generators/Python/itk/support/extras.py` — `simpleitk_from_image` and
`image_from_simpleitk`. Follow-up to #6811; not a regression, as neither function existed before it.
Contributor guide
Research direction
Read Wrapping/Generators/Python/itk/support/extras.py, focusing on simpleitk_from_image and image_from_simpleitk and their region handling. Reproduce the demonstrated case with buffered and largest regions of different sizes, then verify that conversion raises a ValueError naming both extents instead of silently shrinking the largest possible region.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- api
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- Half a day
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 74/100