ENH: Add ngff-zarr multi-resolution registration example#375
Conversation
|
Check out this pull request on See visual diffs & provide feedback on Jupyter Notebooks. Powered by ReviewNB |
|
@thewtex Please do By the way, what does "9566-example-demonstr" mean? |
09b02cd to
56a87dd
Compare
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.
56a87dd to
95de05e
Compare
- 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
N-Dekker
left a comment
There was a problem hiding this comment.
Thanks for looking at my initial comments, Matt! Did you push the changes you made to address them?
|
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). In MeVisLab ("CT_2D_head_fixed.mha" at the left side, "CT_2D_head_moving.mha" at the right):
Do you still remember where "CT_2D_head_fixed.mha" and "CT_2D_head_moving.mha" came from? |
| "def resample_block(block, block_info=None, transform=None, moving_image=None,\n", | ||
| " spacing_itk=None, origin_itk=None):\n", |
There was a problem hiding this comment.
Why all those =None defaults?
block_info=None, transform=None, moving_image=None, spacing_itk=None, origin_itk=NoneIt appears that resample_block crashes when doing transform=None, for example.
I would suggest to simply remove all those =None defaults.
| "ngff_fixed = itk_image_to_ngff_image(fixed_image)\n", | ||
| "ngff_moving = itk_image_to_ngff_image(moving_image)\n", |
There was a problem hiding this comment.
For readability, can you please rename the following variables, by appending "_image"?
- ngff_fixed to ngff_fixed_image
- ngff_moving to ngff_moving_image
- fixed_coarse to fixed_coarse_image
- moving_coarse to moving_coarse_image
- fixed_coarse_ngff to fixed_coarse_ngff_image
- moving_coarse_ngff to moving_coarse_ngff_image
| "ImageType = itk.Image[itk.F, 2]\n", | ||
| "if not isinstance(fixed_coarse, ImageType):\n", | ||
| " fixed_coarse = itk.cast_image_filter(fixed_coarse, ttype=(type(fixed_coarse), ImageType))\n", | ||
| "if not isinstance(moving_coarse, ImageType):\n", | ||
| " moving_coarse = itk.cast_image_filter(moving_coarse, ttype=(type(moving_coarse), ImageType))" |
There was a problem hiding this comment.
Why would you do this? (Conditionally casting the images to float) I'm sorry, but I think this part should be removed. First of all, the images returned by ngff_image_to_itk_image are already float images, so itk.cast_image_filter is not called. Secondly, ITKElastix/elastix will already take care of converting the input images to the internal pixel types, automatically. So I think users do not need to "learn" how to do that manually, beforehand.
| "\n", | ||
| "# Materialize the full-resolution moving image as an itk.Image for resampling.\n", | ||
| "# Each block function will sample from this image.\n", | ||
| "moving_itk_full = ngff_image_to_itk_image(moving_full, wasm=False)\n", |
There was a problem hiding this comment.
moving_itk_full is basically identical to the original moving_image, right? Same size, same pixel values, right? So it's a loss-less round-trip: moving_image ==> itk_image_to_ngff_image ==> to_multiscales ==> images[0] ==> moving_itk_full
Maybe it's worth noting 🤷
| "# Compute the result (triggers parallel execution across chunks)\n", | ||
| "resampled_array = resampled_dask.compute()\n", |
There was a problem hiding this comment.
Do I understand correctly that this compute() may internally call resample_block multiple times in parallel, for multiple chunks? And does that actually happen, when running this example?
There was a problem hiding this comment.
Is this a relevant use case for dask map_blocks, or is it more like a toy example? The aim is to speed up the resampling by using multi-threading, right? But then, isn't itk.resample_image_filter itself already multi-threaded?
95de05e to
96c4f88
Compare
@N-Dekker oops, I think I pushed them to the wrong remote -- re-pushed! |
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:
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 |



Add Example 23 demonstrating a workflow that combines ngff-zarr multi-resolution image pyramids with ITKElastix registration:
Uses itk_image_to_ngff_image and ngff_image_to_itk_image to bridge between ITK and ngff-zarr data representations.