Skip to content

Conversation

@will-moore
Copy link
Member

@will-moore will-moore commented Sep 8, 2025

Work in progress...

NB: This is on top of #476

To test:

Basic conversion

The generated image has datasets.coordinateTransformations of scale and translate nested into a sequence transform with "output": "physical" and the axes are stored within a coordinateSystem named "physical".

import zarr
from ome_zarr.writer import write_image, get_metadata

url = "https://uk1s3.embassy.ebi.ac.uk/idr/zarr/v0.5/idr0033A/BR00109990_C2.zarr/0/"

read_group = zarr.open_group(url, mode='r')
# copy the data locally
data = read_group['0'][:,:,:]

multiscales = get_metadata(read_group)["multiscales"][0]
axes = multiscales["axes"]
scale = multiscales["datasets"][0]["coordinateTransformations"][0]["scale"]

print("axes:", axes)
print("scale:", scale)
print("data shape:", data.shape)

write_image(data, "idr0033_v0.6dev2.zarr", axes=axes, scale=scale)

Then, to open with validator with RFC5 support from ome/ome-ngff-validator#48

$ ome_zarr view idr0033_v0.6dev2.zarr

Rotation

The script includes commented-out code for creating the rotation matrix using the Affine class from napari, but if napari isn't required for the code below as the rotation_matrix is hard-coded.

import zarr
# from napari.utils.transforms import Affine

from ome_zarr.writer import write_image, get_metadata

url = "https://uk1s3.embassy.ebi.ac.uk/idr/zarr/v0.5/idr0033A/BR00109990_C2.zarr/0/"

read_group = zarr.open_group(url, mode='r')
# copy the data locally
data = read_group['0'][:,:,:]

multiscales = get_metadata(read_group)["multiscales"][0]
axes = multiscales["axes"]
scale = multiscales["datasets"][0]["coordinateTransformations"][0]["scale"]

# create a rotation matrix using napari Affine
# rotation_aff = Affine(rotate=30)    # 3 x 3 matrix for 2D image
# extra_dims = data.ndim - 2
# if extra_dims > 0:
#     rotation_aff = rotation_aff.expand_dims(list(range(extra_dims)))
# # RFC-5 rotation matrix is the upper-left N x N part of the affine matrix
# rotation_matrix = rotation_aff.affine_matrix[:-1, :-1].tolist()

rotation_matrix = [[1.0, 0.0, 0.0], [0.0, 0.8660254037844387, -0.49999999999999994], [0.0, 0.49999999999999994, 0.8660254037844387]]

# NB: "input" is automatically set to "physical" to correspond to the default coordinatSystem
coordinateTransformations = [
    {
        "type": "rotation",
        "rotation": rotation_matrix,
        "output": "rotated"
    }
]

write_image(data, "idr0033_rot_v0.6dev2.zarr", axes=axes, scale=scale,
            coordinateTransformations=coordinateTransformations)

Then view this with will-moore/napari-ome-zarr#2 installed:

$ napari --plugin napari-ome-zarr idr0033_rot_v0.6dev2.zarr
Screenshot 2025-11-04 at 09 50 38

@codecov
Copy link

codecov bot commented Sep 9, 2025

Codecov Report

❌ Patch coverage is 90.59829% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.12%. Comparing base (bb1c3fe) to head (e25627b).
⚠️ Report is 7 commits behind head on master.

Files with missing lines Patch % Lines
ome_zarr/csv.py 16.66% 5 Missing ⚠️
ome_zarr/format.py 93.84% 4 Missing ⚠️
ome_zarr/scale.py 33.33% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #492      +/-   ##
==========================================
- Coverage   87.15%   87.12%   -0.04%     
==========================================
  Files          13       13              
  Lines        1775     1817      +42     
==========================================
+ Hits         1547     1583      +36     
- Misses        228      234       +6     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@jo-mueller
Copy link

Hi @will-moore ,

just played a bit with this. The additional coordinate transformations (i.e., those under multiscales > coordinateTransformations) are not implemented yet, right? So far, there's the coordinateTransformations under multiscales > datasets > coordinateTransformations which are autogenerated by the downsampling process but no additional API.

Do you envision a particular way how this could be organized? I could envision the following:

  • coordinateTransformation argument in write_image gets superseded by scale and translate arguments, which are converted to the corresponding coordinateTransformation object in each of the datasets.
  • scale + translate are only given for the root resolution - the scaler does the rest.
  • The coordinateTransformations argument could then be used to pass the "additional transformations" directly to the writer. I'm not sure whether I'd see an alternative to writing out the dictionary yourself here or referring to ome-zarr-models-py.

On the minus side, that would be a pretty massive breaking change to the API and would need some careful consideration. On the plus-side, it would introduce a relatively clean separation of transforms that go into multiscales > coordinateTransformations and multiscales > datasets > coordinateTransformations on a front-end API level.

@jo-mueller
Copy link

jo-mueller commented Oct 29, 2025

Hi @will-moore , I think 6566d3c is a good addition to the API. I noticed that when I use the code for writing, I get this for the scale coordinate Transformations inside the datasets:

{
  "path": "0",
  "coordinateTransformations": [
    {
      "type": "sequence",
      "transformations": [
        {
          "type": "scale",
          "scale": [
            0.5,
            0.391,
            0.391
          ]
        },
        {
          "type": "translation",
          "translation": [
            0.0,
            0.0,
            0.0
          ]
        }
      ],
      "input": "",
      "output": "physical"
    }
  ]
},

which is an invalid value for input (must be the same as path). I think a reasonable change to the code would be in the write_multiscale to not calculate the scale transformations after generating the pyramid levels. Instead, the correct scale/translation could be calculated on-the-fly inside the loop over the pyramid levels?

This way, the ("default") coordinate transformations for the multiscales would always be calculated, if only for a default scale. using the scale as input for the transformations inside the datasets the latter would always be reserved for (autocalculated) scale/translate and the coordinateTransformations keyword would be freed for the additional transforms. I think this would be a nice way to separate the two components in the API.

Could send a PR with this to your work?

edit: Here goes will-moore#2

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.

2 participants