Skip to content

ENH: Add ngff-zarr multi-resolution registration example - #375

Open
thewtex wants to merge 5 commits into
mainfrom
9566-example-demonstr
Open

ENH: Add ngff-zarr multi-resolution registration example#375
thewtex wants to merge 5 commits into
mainfrom
9566-example-demonstr

Conversation

@thewtex

@thewtex thewtex commented Mar 9, 2026

Copy link
Copy Markdown
Member

Add Example 23 demonstrating a workflow that combines ngff-zarr multi-resolution image pyramids with ITKElastix registration:

  • Convert ITK images to ngff-zarr Multiscales via to_multiscales
  • Register at a coarse resolution (rigid + affine + bspline)
  • Convert Elastix results to itk.CompositeTransform
  • Apply the transform at full resolution in parallel using dask.array.map_blocks with itk.resample_image_filter

Uses itk_image_to_ngff_image and ngff_image_to_itk_image to bridge between ITK and ngff-zarr data representations.

@review-notebook-app

Copy link
Copy Markdown

Check out this pull request on  ReviewNB

See visual diffs & provide feedback on Jupyter Notebooks.


Powered by ReviewNB

@thewtex
thewtex requested a review from N-Dekker March 9, 2026 12:18
@N-Dekker

N-Dekker commented Mar 9, 2026

Copy link
Copy Markdown
Collaborator

@thewtex Please do git rebase main on this PR! It appears 14 commits behind!

By the way, what does "9566-example-demonstr" mean?

Comment thread examples/ITK_Example23_NgffZarrMultiscaleRegistration.ipynb
Comment thread examples/ITK_Example23_NgffZarrMultiscaleRegistration.ipynb Outdated
@thewtex
thewtex force-pushed the 9566-example-demonstr branch from 09b02cd to 56a87dd Compare March 24, 2026 19:07
Add Example 23 demonstrating a workflow that combines ngff-zarr
multi-resolution image pyramids with ITKElastix registration:

- Convert ITK images to ngff-zarr Multiscales via to_multiscales
- Register at a coarse resolution (rigid + affine + bspline)
- Convert Elastix results to itk.CompositeTransform
- Apply the transform at full resolution in parallel using
  dask.array.map_blocks with itk.resample_image_filter

Uses itk_image_to_ngff_image and ngff_image_to_itk_image to bridge
between ITK and ngff-zarr data representations.
@thewtex

thewtex commented Mar 24, 2026

Copy link
Copy Markdown
Member Author

@thewtex Please do git rebase main on this PR! It appears 14 commits behind!

@N-Dekker 👍 done!

By the way, what does "9566-example-demonstr" mean?

This is a unique identifier for the git worktree I am using.

@thewtex
thewtex force-pushed the 9566-example-demonstr branch from 56a87dd to 95de05e Compare March 24, 2026 19:19
@thewtex
thewtex marked this pull request as draft March 24, 2026 19:19
Comment thread examples/ITK_Example23_NgffZarrMultiscaleRegistration.ipynb Outdated
Comment thread examples/ITK_Example23_NgffZarrMultiscaleRegistration.ipynb Outdated
Comment thread examples/ITK_Example23_NgffZarrMultiscaleRegistration.ipynb Outdated
- Reword 'required by Elastix' to 'recommended for Elastix, by default'
  in markdown and code comment for the float pixel type discussion
- Clarify that 'native itk.Image' is in contrast to 'itkwasm.image.Image'
  when calling ngff_image_to_itk_image with wasm=False
- Rename 'resampled_dask' to 'resampled_dask_array' for clarity
@thewtex
thewtex marked this pull request as ready for review May 18, 2026 18:59

@N-Dekker N-Dekker left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for looking at my initial comments, Matt! Did you push the changes you made to address them?

@N-Dekker

Copy link
Copy Markdown
Collaborator

Maybe beyond the scope of this PR, but "CT_2D_head_fixed.mha" looks a bit warped. Especially when looking at the border of the skull. I see, you introduced the image by 56caa00 (back in 2020).
image

In MeVisLab ("CT_2D_head_fixed.mha" at the left side, "CT_2D_head_moving.mha" at the right):

image

Do you still remember where "CT_2D_head_fixed.mha" and "CT_2D_head_moving.mha" came from?

Comment thread examples/ITK_Example23_NgffZarrMultiscaleRegistration.ipynb Outdated
Comment thread examples/ITK_Example23_NgffZarrMultiscaleRegistration.ipynb Outdated
Comment thread examples/ITK_Example23_NgffZarrMultiscaleRegistration.ipynb Outdated
Comment thread examples/ITK_Example23_NgffZarrMultiscaleRegistration.ipynb Outdated
Comment thread examples/ITK_Example23_NgffZarrMultiscaleRegistration.ipynb Outdated
@thewtex
thewtex force-pushed the 9566-example-demonstr branch from 95de05e to 96c4f88 Compare May 20, 2026 20:51
@thewtex

thewtex commented May 20, 2026

Copy link
Copy Markdown
Member Author

Thanks for looking at my initial comments, Matt! Did you push the changes you made to address them?

@N-Dekker oops, I think I pushed them to the wrong remote -- re-pushed!

@thewtex

thewtex commented May 20, 2026

Copy link
Copy Markdown
Member Author

Maybe beyond the scope of this PR, but "CT_2D_head_fixed.mha" looks a bit warped. Especially when looking at the border of the skull. I see, you introduced the image by 56caa00 (back in 2020).

This is intentional, an artificially induced warp. I believe these images come from the ITK Software Guide.

@N-Dekker

N-Dekker commented May 21, 2026

Copy link
Copy Markdown
Collaborator

This is intentional, an artificially induced warp. I believe these images come from the ITK Software Guide.

Ah, thanks, I see similar images now, in InsightSoftwareGuide-Book2-5.4.6.pdf, although not warped:

image

Still, I would expect the warping to take place on the moving image, during registration (bspline), and no warping of the fixed image 🤷


Update, I see now, the T1 image (before warping) is at https://github.com/InsightSoftwareConsortium/ITKSoftwareGuide/blob/7510e24a4dd8e98b6ee582c2468961f69ca1ec4b/SoftwareGuide/Art/BrainT1Slice.jpg

@N-Dekker

Copy link
Copy Markdown
Collaborator

@thewtex Thanks for addressing my initial comments, Matt. Feel free to ignore my comment about the artificially warped image. It appears that Marius (@mstaring) actually likes the warped image 😃 I think the other remaining comments that I had are still relevant. Please consider them, when you have the time.

@vboussot

Copy link
Copy Markdown
Collaborator

Hi @N-Dekker, @thewtex,

I updated the example so the full-resolution resampling is out-of-core: the new ngff_zarr.itk_transform_resample_bounding_box (fideus-labs/ngff-zarr#632) computes from geometry alone the moving-image region the resample will read, and only that region is materialized. The output grid is also re-chunked so map_blocks really resamples 16 blocks in parallel.

@thewtex

thewtex commented Aug 12, 2026

Copy link
Copy Markdown
Member Author

@vboussot looking good 🔢

To make the functionality more illustrative, can we draw colored bounding boxes in images with matplotlib for:

  1. A single region in the fixed image, the corresponding resampling region in the moving image
  2. A region with different padding parameters in the moving, different shades of the same color
  3. All the regions from the fixed image and their correspondence in the moving image, different colors per region, that are used in a full block-wise resample

?

@N-Dekker

Copy link
Copy Markdown
Collaborator

@thewtex @vboussot I see that this pull request is very much renewed now, wow! Are my original comments (from before July) still going to be addressed?

@vboussot
vboussot force-pushed the 9566-example-demonstr branch from 1729162 to fc32ade Compare August 13, 2026 16:30
@vboussot

vboussot commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Hi @niels !

All your comments are addressed, squashed into a single commit: no more =None defaults, variables renamed to *_image, and the float casts removed.

The example now also demonstrates out-of-core resampling: with ngff_zarr.itk_transform_resample_bounding_box, each block reads only the moving-image region its resample needs that's what map_blocks adds over ITK's own multi-threading and the new figures visualize those regions.

Thanks for the careful review!

@vboussot
vboussot requested a review from N-Dekker August 13, 2026 16:40
@thewtex

thewtex commented Aug 13, 2026

Copy link
Copy Markdown
Member Author

@vboussot thanks for the updates 💯

Can we please run the notebook, save the cell outputs? That way people can see the figures in the rendered notebooks on GitHub with a web browser.

Comment thread examples/ITK_Example23_NgffZarrMultiscaleRegistration.ipynb
Comment thread examples/ITK_Example23_NgffZarrMultiscaleRegistration.ipynb
@vboussot
vboussot force-pushed the 9566-example-demonstr branch from fc32ade to 6608bad Compare August 14, 2026 08:39
Each map_blocks call computes its block's moving-image region with
ngff_zarr.itk_transform_resample_bounding_box, materializes only that
region, and resamples it into the block's reference grid. The full
moving image is never loaded in memory. Figures visualize the per-block
regions.
@vboussot
vboussot force-pushed the 9566-example-demonstr branch from 6608bad to 2caf80b Compare August 14, 2026 09:13
@N-Dekker

Copy link
Copy Markdown
Collaborator

Hi (@)niels !

Thanks for the careful review!

Hi @vboussot,

You're welcome, but honestly I still need to review most of your part! Please wait! Today I'll have a look!

Minor nitpick: "Niels" is my first name, but it's not my GitHub name! On GitHub, just use @N-Dekker to address me!

@review-notebook-app

review-notebook-app Bot commented Aug 14, 2026

Copy link
Copy Markdown

View / edit / reply to this conversation on ReviewNB

N-Dekker commented on 2026-08-14T14:12:59Z
----------------------------------------------------------------

Line #2.    itk_composite_transform = itk.CompositeTransform[itk.D, 2].cast(

Please do not do cast here, if possible. The ITKElastix function registration_method.ConvertToItkTransform already returns an itk.CompositeTransform[itk.D, 2] object. (We should try to avoid unnecessary casts.)



Update: I see now, removing the itk.CompositeTransform[itk.D, 2].cast triggers an error, locally on my PC. I don't know why 🤷 Here is the output that I locally got from cell 15:

AttributeError                            Traceback (most recent call last)
Cell In[15], line 5
      1 # 1. One fixed block and the moving-image region its resample reads.
      2 locations = block_locations(fixed_grid)
      3 location = locations[5]  # an interior block
      4 
----> 5 region = block_bounding_box(location)
      6 
      7 fig, axs = plt.subplots(1, 2, figsize=[12, 6])
      8 axs[0].imshow(fixed_array, cmap="gray")

Cell In[14], line 28, in block_bounding_box(array_location, padding)
     24         dims=fixed_full.dims,
     25         scale=fixed_full.scale,
     26         translation=block_translation,
     27     )
---> 28     return itk_transform_resample_bounding_box(
     29         itk_composite_transform, block_grid, moving_full, padding=padding
     30     )

File ~\.conda\envs\ITKElx-0254-pip-py313\Lib\site-packages\ngff_zarr\itk_transform_resample_bounding_box.py:425, in itk_transform_resample_bounding_box(transform, fixed, moving, padding)
    413     zeros = dict.fromkeys(fixed_spatial, 0)
    414     return ResampleBoundingBox(
    415         dims=tuple(fixed_spatial),
    416         start_index=dict(zeros),
   (...)    422         moving_shape=moving_shape,
    423     )
--> 425 transform_list = _as_itk_transform_list(transform)
    426 fixed_direction = _itk_direction(fixed, itk_dims)
    427 moving_direction = _itk_direction(moving, itk_dims)

File ~\.conda\envs\ITKElx-0254-pip-py313\Lib\site-packages\ngff_zarr\itk_transform_resample_bounding_box.py:294, in _as_itk_transform_list(transform)
    291 if hasattr(transform, "GetTransformTypeAsString"):
    292     import itk
--> 294     as_dict = itk.dict_from_transform(transform)
    295     if isinstance(as_dict, dict):
    296         as_dict = [as_dict]

File ~\.conda\envs\ITKElx-0254-pip-py313\Lib\site-packages\itk\support\extras.py:1100, in dict_from_transform(transform)
   1098         add_transform_dict(t)
   1099 else:
-> 1100     multi = add_transform_dict(transform)
   1102 if multi:
   1103     return dict_array

File ~\.conda\envs\ITKElx-0254-pip-py313\Lib\site-packages\itk\support\extras.py:1087, in dict_from_transform.<locals>.add_transform_dict(transform)
   1083 transform_type = transform.GetTransformTypeAsString()
   1084 if "CompositeTransform" in transform_type:
   1085     # Add the transforms inside the composite transform
   1086     # range is over-ridden so using this hack to create a list
-> 1087     for i, _ in enumerate([0] * transform.GetNumberOfTransforms()):
   1088         current_transform = transform.GetNthTransform(i)
   1089         dict_array.append(update_transform_dict(current_transform))

AttributeError: 'itkTransformD22' object has no attribute 'GetNumberOfTransforms'

@review-notebook-app

review-notebook-app Bot commented Aug 14, 2026

Copy link
Copy Markdown

View / edit / reply to this conversation on ReviewNB

thewtex commented on 2026-08-14T14:38:32Z
----------------------------------------------------------------

Since they are different modality, I think we should remove the difference here.


@review-notebook-app

review-notebook-app Bot commented Aug 14, 2026

Copy link
Copy Markdown

View / edit / reply to this conversation on ReviewNB

N-Dekker commented on 2026-08-14T16:28:20Z
----------------------------------------------------------------

Line #3.        block_info=None,

Sorry for nitpicking, but this =None may also be removed, right?


@review-notebook-app

review-notebook-app Bot commented Aug 14, 2026

Copy link
Copy Markdown

View / edit / reply to this conversation on ReviewNB

N-Dekker commented on 2026-08-14T16:28:20Z
----------------------------------------------------------------

Line #4.        *,

The use of an asterisk (*) in a function parameter list is new to me, I have to admit. Just for my understanding, is this an important part of the function declaration, or just a nicety? I read that it will make the following parameters "keyword-arguments". So then, would it be even nicer to move the asterisk one line up? As in:

def resample_block(

  block,

  *,

  block_info,

  transform,

  fixed_image,

  moving_image,

)

So that only the first parameter would be an ordinary "positional argument"...?


Make block_info keyword-only. dask then always supplies it, so the
metadata probe call and the "if block_info is None" guard it required
both go away: measured one probe call before the change, none after.

Drop the difference panel from the final figure. The fixed and moving
images are different modalities, so their subtraction is not meaningful.

Record why the composite transform is cast: ConvertToItkTransform
returns the itk.Transform[itk.D, 2, 2] base, which does not expose the
composite interface.

Cell outputs regenerated by running the notebook.
@review-notebook-app

review-notebook-app Bot commented Aug 21, 2026

Copy link
Copy Markdown

View / edit / reply to this conversation on ReviewNB

N-Dekker commented on 2026-08-21T14:13:19Z
----------------------------------------------------------------

Line #2.    # ConvertToItkTransform hands back the itk.Transform[itk.D, 2, 2] base, so the

Thank you for clarifying why the cast is necessary! In the future, I hope that such a cast is no longer necessary, as I just made a pull request to add a new member function, ElastixRegistrationMethod::ConvertCompositionToItkTransform, which returns a CompositeTransform. Please check: SuperElastix/elastix#1464 Anyway, for now I agree that the cast should still be there!


@review-notebook-app

review-notebook-app Bot commented Aug 24, 2026

Copy link
Copy Markdown

View / edit / reply to this conversation on ReviewNB

N-Dekker commented on 2026-08-24T12:03:16Z
----------------------------------------------------------------

Line #4.    scale_factors = [2, 4]

For the described registration use case, only the coarsest resolution level is used, so I guess a single scale factor (scale_factors=[4]) would be sufficient, right? No problem for me, just asking, for my understanding. So in practice, users would just use a single scale factor, right?


@review-notebook-app

review-notebook-app Bot commented Aug 24, 2026

Copy link
Copy Markdown

View / edit / reply to this conversation on ReviewNB

N-Dekker commented on 2026-08-24T13:28:22Z
----------------------------------------------------------------

Line #30.        array_location = block_info[0]["array-location"]

I find this array-location stuff a bit hard to understand. Does each block_info[i] have a different "array-location"? A previous revision had some explanation, saying:

  # block_info[0]['array-location'] gives [(y_start, y_stop), (x_start, x_stop)]

  # for a 2D array with dims (y, x)

Would this comment still be applicable?


…t in Example 23

Comment what block_info[0]["array-location"] holds in resample_block,
note that elastix's ConvertCompositionToItkTransform (SuperElastix/elastix#1464)
removes the need for the cast once ITKElastix ships it, and state that a single
scale factor is enough for registration alone. Re-executed with saved outputs.
@vboussot

Copy link
Copy Markdown
Collaborator

@N-Dekker @thewtex 962ce96 addresses the remaining threads; the notebook is re-executed with saved outputs.

  • array-location: block_info is keyed by the position of each array passed to map_blocks. fixed_grid is the only one, so block_info[0] describes the current block of it, and its "array-location" is [(y_start, y_stop), (x_start, x_stop)], the block's index range along each dim of the fixed image. This is now commented in resample_block.
  • CompositeTransform cast: ENH: Add ConvertCompositionToItkTransform to ElastixRegistrationMethod SuperElastix/elastix#1464 is merged, thanks. ITKElastix pins elastix at ef34ca99, 54 commits before that merge, and itk-elastix 0.25.4 does not expose ConvertCompositionToItkTransform yet, so the cast stays. The comment now points to the new method so it can be dropped once ITKElastix updates its elastix.
  • scale_factors=[2, 4]: yes, scale_factors=[4] is enough for the registration alone. The intermediate level is what a stored OME-Zarr pyramid typically carries for visualization, and the example builds the same kind of pyramid; the markdown says this now.
  • block_info=None and the * placement: already in 648a616 (block is the only positional parameter, everything after * is keyword-only with no defaults).
  • Difference panel (different modalities): removed in 648a616; the final figure shows fixed, moving, and resampled moving.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants