New extension: OMEZarr - open OME-Zarr images directly in Slicer

Hi all,

We’re happy to announce OMEZarr, a new extension from Fideus Labs that opens and saves OME-Zarr images. It is available in the Extensions Manager for Slicer 5.12.4 and the Preview release, on Linux, macOS and Windows.

OME-Zarr is a format increasingly used for large microscopy datasets: a folder of compressed chunks at several resolutions, stored on disk, on a web server or in the cloud. Until now, looking at one in Slicer meant converting it to NRRD or NIfTI first. Now you can:

  • Drag an .ome.zarr folder onto Slicer, or open an https:// or s3:// address. No conversion.
  • Open datasets larger than RAM and progressively refine the views at full resolution, or load a region of interest at any level.
  • Work with regular Slicer nodes. Channels become volumes with their names and colours, labels become label maps or segmentations, time series become sequences. Segment Editor, registration and volume rendering work as usual.
  • Get the geometry right. Spacing and units (µm, nm) are converted to mm, and the RFC-4 anatomical orientation becomes the IJK-to-RAS matrix.
  • Save back to OME-Zarr. Volumes, label maps and segmentations are written as multiscale OME-Zarr, so the results open in napari, Fiji or Python.

Reading and writing go through ngff-zarr, which we maintain.

The video is recorded on a public two-photon image of GFP-labelled neurons in a marmoset cortex, streamed from S3. To try it right away, paste this in the Python console:

slicer.util.loadNodeFromFile("s3://ome-zarr-scivis/v0.5/96x2/marmoset_neurons.ome.zarr", "OMEZarr")

Code and step-by-step tutorial: GitHub - fideus-labs/SlicerOMEZarr: 3D Slicer extension to open OME-Zarr (OME-NGFF) images, built on ngff-zarr · GitHub

Please give it a go and tell us what breaks, especially with your own datasets. Thanks to @lassoan for reviewing the extension and for the helpful feedback.

I am quite excited about this, thanks for implementing the ome-zarr support. When I tried the extension with you example on MacOS (5.12.4)

>>> slicer.util.loadNodeFromFile("s3://ome-zarr-scivis/v0.5/96x2/marmoset_neurons.ome.zarr", "OMEZarr")

Traceback (most recent call last):

  File "/Applications/Slicer.app/Contents/lib/Python/lib/python3.12/site-packages/ngff_zarr/_remote_reader.py", line 498, in open_remote_node

    arr = _run(lambda: AsyncArray.open(store._store, path))

          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

  File "/Applications/Slicer.app/Contents/lib/Python/lib/python3.12/site-packages/ngff_zarr/_remote_reader.py", line 100, in _run

    return asyncio.run_coroutine_threadsafe(_invoke(), _io_loop()).result()

           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

  File "/Applications/Slicer.app/Contents/lib/Python/lib/python3.12/concurrent/futures/_base.py", line 456, in result

    return self.__get_result()

           ^^^^^^^^^^^^^^^^^^^

  File "/Applications/Slicer.app/Contents/lib/Python/lib/python3.12/concurrent/futures/_base.py", line 401, in __get_result

    raise self._exception

  File "/Applications/Slicer.app/Contents/lib/Python/lib/python3.12/site-packages/ngff_zarr/_remote_reader.py", line 98, in _invoke

    return await factory()

           ^^^^^^^^^^^^^^^

zarrista.exceptions.ArrayCreateError: Generic S3 error: Error performing PUT http://169.254.169.254/latest/api/token in 2.635813709s, after 10 retries, max_retries: 10, retry_timeout: 180s  - HTTP error: error sending request



The above exception was the direct cause of the following exception:



Traceback (most recent call last):

  File "/Applications/Slicer.app/Contents/Extensions-34645/OMEZarr/lib/Slicer-5.12/qt-scripted-modules/OMEZarr.py", line 1575, in load

    nodes = OMEZarrLogic.loadImage(

            ^^^^^^^^^^^^^^^^^^^^^^^

  File "/Applications/Slicer.app/Contents/Extensions-34645/OMEZarr/lib/Slicer-5.12/qt-scripted-modules/OMEZarr.py", line 763, in loadImage

    multiscales = multiscales or cls.openMultiscales(path)

                                 ^^^^^^^^^^^^^^^^^^^^^^^^^

  File "/Applications/Slicer.app/Contents/Extensions-34645/OMEZarr/lib/Slicer-5.12/qt-scripted-modules/OMEZarr.py", line 378, in openMultiscales

    ngff_zarr.from_ome_zarr(source, storage_options=options) if options else ngff_zarr.from_ome_zarr(source)

                                                                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

  File "/Applications/Slicer.app/Contents/lib/Python/lib/python3.12/site-packages/ngff_zarr/from_ngff_zarr.py", line 264, in from_ome_zarr

    root = _open_root_node(store, version)

           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

  File "/Applications/Slicer.app/Contents/lib/Python/lib/python3.12/site-packages/ngff_zarr/from_ngff_zarr.py", line 158, in _open_root_node

    return _open_remote_root(store, version)

           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

  File "/Applications/Slicer.app/Contents/lib/Python/lib/python3.12/site-packages/ngff_zarr/from_ngff_zarr.py", line 141, in _open_remote_root

    node = open_remote_node(store)

           ^^^^^^^^^^^^^^^^^^^^^^^

  File "/Applications/Slicer.app/Contents/lib/Python/lib/python3.12/site-packages/ngff_zarr/_remote_reader.py", line 504, in open_remote_node

    raise group_error from array_error

  File "/Applications/Slicer.app/Contents/lib/Python/lib/python3.12/site-packages/ngff_zarr/_remote_reader.py", line 493, in open_remote_node

    group = _run(lambda: AsyncGroup.open(store._store, path))

            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

  File "/Applications/Slicer.app/Contents/lib/Python/lib/python3.12/site-packages/ngff_zarr/_remote_reader.py", line 100, in _run

    return asyncio.run_coroutine_threadsafe(_invoke(), _io_loop()).result()

           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

  File "/Applications/Slicer.app/Contents/lib/Python/lib/python3.12/concurrent/futures/_base.py", line 456, in result

    return self.__get_result()

           ^^^^^^^^^^^^^^^^^^^

  File "/Applications/Slicer.app/Contents/lib/Python/lib/python3.12/concurrent/futures/_base.py", line 401, in __get_result

    raise self._exception

  File "/Applications/Slicer.app/Contents/lib/Python/lib/python3.12/site-packages/ngff_zarr/_remote_reader.py", line 98, in _invoke

    return await factory()

           ^^^^^^^^^^^^^^^

zarrista.exceptions.GroupCreateError: Generic S3 error: Error performing PUT http://169.254.169.254/latest/api/token in 6.812037334s, after 10 retries, max_retries: 10, retry_timeout: 180s  - HTTP error: error sending request

Traceback (most recent call last):

  File "<console>", line 1, in <module>

  File "/Applications/Slicer.app/Contents/bin/Python/slicer/util.py", line 913, in loadNodeFromFile

    raise RuntimeError(errorMessage)

RuntimeError: Failed to load node from file: s3://ome-zarr-scivis/v0.5/96x2/marmoset_neurons.ome.zarr

Error: Loading s3://ome-zarr-scivis/v0.5/96x2/marmoset_neurons.ome.zarr - Failed to read OME-Zarr: Generic S3 error: Error performing PUT http://169.254.169.254/latest/api/token in 6.812037334s, after 10 retries, max_retries: 10, retry_timeout: 180s  - HTTP error: error sending request

Thanks for the report, the traceback pointed straight at it. The extension treated a ~/.aws/credentials file as AWS credentials, but the S3 client it uses only reads environment variables, so it ended up asking the EC2 metadata service and failing.

Fixed in fix: read public S3 stores anonymously even when an AWS credentials f… · fideus-labs/SlicerOMEZarr@eb3cbbf · GitHub , in tomorrow’s extension build. Until then, in the OME-Zarr module go to Settings → Remote storage options, enter {"anon": true}, and the example loads.

I was just about to post it, you beat me to it :slight_smile:

A second suggestion: please consider writing OME-Zarr 0.6 when a volume has a rotation. Right now the writer uses the 0.5 default, which can only store scale and translation. A volume with a rotated direction matrix, which is common in Slicer after reorienting, is quietly saved axis-aligned, so it no longer lines up with segmentations made on the original. ngff-zarr already supports 0.6 (RFC-5) affines. I tried a small patch locally:

  • Saving: keep the per-level scale and translation as they are, and add the leftover rotation as an image-level affine, written as 0.6 only when needed.
  • Loading: apply that affine when building IJK-to-RAS.

With that, an oblique uint16 volume round-trips exactly, and a .seg.nrrd made on the original lines up voxel for voxel. I’m happy to send it as a PR if that’s useful.

This extension is important for us. We are evaluating OME-Zarr as the source-volume format for MorphoDepot, so thanks again for putting it together.

You are right, thanks for checking it. A PR is very welcome. Great to hear about MorphoDepot.

PR is open Keep oblique directions by writing an OME-Zarr 0.6 (RFC-5) affine by muratmaga · Pull Request #13 · fideus-labs/SlicerOMEZarr · GitHub