Add image class - #515
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #515 +/- ##
==========================================
+ Coverage 85.42% 86.65% +1.23%
==========================================
Files 14 16 +2
Lines 1921 2316 +395
==========================================
+ Hits 1641 2007 +366
- Misses 280 309 +29 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
@jo-mueller Thanks for that - This looks like a nice approach to separate the metadata creation and manipulation from the writing to zarr. Comparing APIs:
Various comments, questions. I realise some of this is just not implemented yet... And I haven't tried the code (which might answer some of these)...
|
|
@will-moore thanks for the breakdown. I was aware of some of these points (not all) but decided to send it anyway to not go too far in the wrong direction in case there were strong objections to the approach. Actually, what you wrote is an excellent to-do list :) |
|
@will-moore I think this is taking a bit more shape towards how I'd expect it. But before this can continue, I think there is value in discussing first the current duplicity in functionality between these two functions:
There's a lot of overlap between these two functions which I think can be condensed so we'd have a single place where
Anyway, I just tried the |
|
This pull request has been mentioned on Image.sc Forum. There might be relevant details there: https://forum.image.sc/t/separate-tiles-to-ome-zarr/109071/55 |
|
@jo-mueller Could you update the description to reflect where this is heading now? Are we planning to keep all the existing write methods (any API changes)? |
2b38492 to
c67569d
Compare
|
It would be nice to support writing of "omero" metadata. I think that's covered by ome-zarr-models-py too. |
Agree.
THAT is a good question I'm not entirely sure of myself. I guess if we want to go this way further, we would ultimately deprecate def write_image(args, kwargs):
image = NGffImage(args, kwargs)
multiscales = NgffMultiscales(image, ....)
multiscales.to_ome_zarr(....)which would at least reduce the amount of code to maintain and make sure that everything we do on the class-based API side is covered well by the already existing tests. What's missing hereThe only thing that makes tests fail here currently is this one: ome-zarr-models/ome-zarr-models-py#398. Locally, all tests are passing. Also, note that this branch has been rebased on #544, so that'll have to go in first, too. |
d317e3b to
bcdba78
Compare
|
@will-moore to go forward with this one, my idea for a soft transition would be this: Step 1: Refactor - I am just now trying to see if I can get the existing entrypoints ( |
be59db0 to
8894c02
Compare
This writes an invalid image because the EDIT: Also the |
This image has so the version lookup needs to be a bit more specific |
Co-authored-by: Gage Bonner <58102495+70Gage70@users.noreply.github.com>
|
Hi @70Gage70 , thanks for the suggestion. I added a hard removal of any remaining 08.06.2026_10.07.34_REC.mp4 |
Co-authored-by: Wouter-Michiel Vierdag <w-mv@hotmail.com>
for more information, see https://pre-commit.ci
|
@pennycuda sorry for missing your comment here! We will see to have this merged this week, so to contribute it's probably easiest to check out this branch and open it as a PR to main and rebase later (won't do any force pushes here anymore) |
will-moore
left a comment
There was a problem hiding this comment.
LGTM: let's get this in and then followup with any remaining issues we need...
ome_zarr 0.18 refactored the functional write_image/write_multiscale entrypoints (ome/ome-zarr-py#515) to read omero from the top-level metadata dict; spatialdata passes it nested under metadata["metadata"], so 0.18 silently dropped it. Effects: write_channel_names() crashed (omero block absent) and plain write->read lost channel names entirely (["r","g","b"] came back as [0,1,2]). Instead of depending on ome-zarr-py to emit omero, write it ourselves: - _write_raster() now calls overwrite_channel_names() after every image write, so the omero block is always present (idempotent on 0.17). - overwrite_channel_names() defaults to an empty omero block when none exists yet. Pin ome_zarr>=0.18 so CI resolves the same version a fresh install gets (uv otherwise lands on 0.17, hiding 0.18 regressions). Verified: full tests/io suite (227) passes on 0.18. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Migrate supported Python to 3.12, 3.13, 3.14 Drop Python 3.11 (anndata>=0.12 already requires >=3.12, so 3.11 was effectively broken) and add 3.14. - pyproject.toml: requires-python ">=3.12", ruff target-version py312 - .mypy.ini: python_version 3.12 - test.yaml: matrix 3.12/3.13/3.14; repoint bleeding-edge deps job to 3.14 and drop the obsolete requires-python sed hack Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Adopt PEP 695 type aliases for py312 (ruff UP040) The target-version bump to py312 enables ruff UP040. Rewrite the four explicit TypeAlias declarations to the `type` keyword and drop the now unused TypeAlias imports. Annotation-only aliases (the repo uses `from __future__ import annotations`), so no runtime behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Fix infinite recursion in dataloader.__getattr__ The module __getattr__ fell through to `getattr(spatialdata.dataloader, attr_name)` for any unknown name, re-entering itself indefinitely (RecursionError) instead of raising AttributeError per PEP 562. This was latent until the docs build hit it: the PEP 695 `type` aliases live in private modules, so sphinx-autodoc-typehints probes every `spatialdata.*` submodule with getattr() looking for a public re-export, tripping the recursion and failing the RTD build. Raise AttributeError for unknown names; drop the now-unused `import spatialdata` and tighten the return type to type[ImageTilesDataset]. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Pin ome_zarr<0.18.0 ome_zarr 0.18 switched to the NGFF 0.5 layout: channel metadata moved out of the `omero` block, so overwrite_channel_names() in _io/_utils.py gets None and crashes (~48 IO test failures). This breaks main independently of the Python bump. Pin as a stopgap until NGFF 0.5 is supported. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Support ome_zarr 0.18 by writing omero channel metadata ourselves ome_zarr 0.18 refactored the functional write_image/write_multiscale entrypoints (ome/ome-zarr-py#515) to read omero from the top-level metadata dict; spatialdata passes it nested under metadata["metadata"], so 0.18 silently dropped it. Effects: write_channel_names() crashed (omero block absent) and plain write->read lost channel names entirely (["r","g","b"] came back as [0,1,2]). Instead of depending on ome-zarr-py to emit omero, write it ourselves: - _write_raster() now calls overwrite_channel_names() after every image write, so the omero block is always present (idempotent on 0.17). - overwrite_channel_names() defaults to an empty omero block when none exists yet. Pin ome_zarr>=0.18 so CI resolves the same version a fresh install gets (uv otherwise lands on 0.17, hiding 0.18 regressions). Verified: full tests/io suite (227) passes on 0.18. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Clean up: drop redundant omero metadata build, lean comments overwrite_channel_names() now writes the omero channel block on every image write, so building the same metadata to pass into the ome-zarr-py writer (which 0.18 ignores anyway) was dead duplication. Remove it along with the now-unused get_channel_names import, and tighten comments. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Require integer (or bool) dtype for labels ome_zarr 0.18's label writer auto-parses unique label values and validates each `label-value` as an integer (via ome-zarr-models), which rejected spatialdata's float-dtype labels. Float labels are meaningless for segmentation masks and inconsistent with the rest of the codebase (fixtures, rasterize, relabel_sequential all assume integers), so the correct fix is to enforce it: Labels{2,3}DModel.parse now rejects non-integer/bool data with a clear error. Tests that fed float data to label models (reusing image-style generators) now use integers; test_rasterize_bins_invalid casts a parsed integer label to float to still exercise rasterize_bins' own guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Relax ome_zarr pin to >=0.16 Our omero self-write and integer-label fixes work across 0.16/0.17/0.18 (verified), so keep the wider lower bound for ecosystem co-installability rather than forcing >=0.18. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * improve labels validation logic --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Luca Marconato <m.lucalmer@gmail.com>
….18) The units tests wrote per-axis units by embedding a "unit" key in the axes dicts handed to write_image/write_labels. ome-zarr 0.18.0 (released 2026-06-17, PR ome/ome-zarr-py#515 "Add image class") reworked the writer so it reduces the axes to bare names (_extract_dims_from_axes) and routes units exclusively through the separate `axes_units` kwarg (added in ~0.15). An embedded "unit" key is now silently dropped, so the fixtures wrote axes with no units, the reader had nothing to forward, and metadata["units"] raised KeyError on every CI job. CI installs ome-zarr unpinned (test-only dep since ome#123), so the July 1 run picked up the just-released 0.18.0 and went red; nothing in the PR itself changed. The reader is correct and forwards units unchanged when they are present on disk. Supply units via axes_units=SPATIAL_UNITS instead of embedding them, and drop the now-ignored "unit" keys from the axes dicts. Assertions are unchanged. Verified: 15/15 reader tests pass on napari 0.7.1 / ome-zarr 0.18.0 / zarr 3.2.1.
Hi @will-moore ,
this is an implementation of my idea for a class-based, user-facing API to writing and reading.
Key features
OMEZarrImage,OMEZarrMultiscaleandOMEZarrLabels(similar to the implementation over at ngff-zarr), that serve as primary entrypoints to the writing.OMEZarrImageaccepts the data to be written as an array and coerces it to dask internally. It requires theaxes(i.e.,"tczyx") to be passed at instantiation. It also accepts kwargs for pixel sizes (scale), axes units (axes_units), display settings, channel names and the name of the image (name) which are later serialized in the ome-zarr metadata.The
OMEZarrMultiscaleandOMEZarrLabelsthen construct a pyramid using the already existing methods (_build_pyramid) that were implemented in deprecate scaler class #516, using different defaults for downsampling (resizeandnearest).Metadataclass from there for simpke serialization and de-serialization in the write/read process. Primarily, the coordinate transformation classes and theMultiscalesmetadata classes are used.Importantly, all metadata is internally coerced to
ozmp.v05.Multiscales. Only on writing the metadata class is converted to whatever ome-zarr version is desired.OMEZarrMultiscale.to_ome_zarr()method. This method makes use of the already existing writing API from Streamline writing #531 (_write_pyramid_to_zarr). It then converts the metadata to the chosen version and uses pydantic'sobject.model_dump()to create the metadata dictionary. Importantly, the version conversion is only implemented in implement version converters ome-zarr-models/ome-zarr-models-py#398, so this is currently blocked by that.OMEZarrMultiscale/OMEZarrLabelsclass also has an attachedfrom_ome_zarr(...)classmethod. The argument is simply the path/group of the ome-zarr image. The function then reads the metadata and the multiscales as dask arrays and returns an instance ofOMEZarrMultiscale. The version is automatically detected and again coerced toozmp.v05.Multiscalesinternally.OMEZarrMultiscaleand passing them as a single image or as adict(str, OMEZarrLabels)to theto_ome_zarrwriter function. Thelabelsattribute is also automatically populated when an image is read using the.from_ome_zarrfunction: The reader then searches for all labels that exist under thelabelszarr group and adds them as adict[str, OMEZarrLabels]to the attribute. One can the add more label images to the attribute, callto_ome_zarr(..., overwrite=False), and only the added label images will be written and the corresponding metadata will be updatedwrite_imageandwrite_labelsnow use theOMEZarrImage,OMEZarrMultiscaleandOMEZarrLabelsclasses under the hood. Since the scale transformations are now entirely calculated under the hood, this means that thecoordinateTransformationsargument is now essentially deprecated, which I think is good since it was unvalidated anyway. The raised warning message reflects this.OMEZarrMultiscaleandNgffLabelsshare quite a bit of functionality regarding reading, writing and pyramid generation, so they both derive from a common ancestral classOMEZarrMultiscaleBase, which handles that and offers hook functions for the child classes' respective functionality:_parse_additional_metadata: Called on end of__init__, can be used to parse more stuff (i.e.,omerooriamge-labelsmetadata._write_additional_meta_data: Called on end ofto_ome_zarr()- derived classes can use it to validate and dump more metadata to a store that's only relevant to the respective implementation ofOMEZarrMultiscaleBase_read_additional_metadata: Called on end ofread_ome_zarr(): Can be used to populate additional (meta)data fields from store, i.e., the.labelsfield, etc.All in all, I think especially the
to_ome_zarrandfrom_ome_zarrmethods are super convenient. I have written a follow-up implementation of thescenemetadata from 0.6 and making use of the same API there makes a lot of sense. We could think of similar entrypoints to writing HCS layouts.TODOs:
omerometadata andimage-labelmetadatascaleargument to entrypoints #553