-
Notifications
You must be signed in to change notification settings - Fork 93
NGFF v06 transformations #1182
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: transformation_manager
Are you sure you want to change the base?
NGFF v06 transformations #1182
Changes from all commits
d94abfd
5aaf4ec
2379e27
0d4140b
a0582e8
9d9315b
96066f3
b15281e
4cba32e
c733658
8ee025e
ad1d809
99ecb22
7361863
0718fb7
df63609
76f7c6b
4f725b1
9591fa2
17eae96
3a42bef
2849a29
4c6c8d9
0c6d7b1
113cf7d
876c176
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,9 @@ | |
|
|
||
| import dask.array as da | ||
| import numpy as np | ||
| import ome_zarr as oz | ||
| import ome_zarr_models.v06.coordinate_transforms as ozm06trans | ||
| import xarray as xr | ||
| import zarr | ||
| from ome_zarr.format import Format | ||
| from ome_zarr.io import ZarrLocation | ||
|
|
@@ -18,6 +21,7 @@ | |
| from ome_zarr.writer import write_multiscale as write_multiscale_ngff | ||
| from ome_zarr.writer import write_multiscale_labels as write_multiscale_labels_ngff | ||
| from xarray import DataArray, DataTree | ||
| from xarray.indexes import RangeIndex | ||
|
|
||
| from spatialdata._io._utils import ( | ||
| _get_transformations_from_ngff_dict, | ||
|
|
@@ -30,7 +34,7 @@ | |
| RasterFormatType, | ||
| get_ome_zarr_format, | ||
| ) | ||
| from spatialdata._types import ELEMENT_TYPE, ELEMENT_TYPE_RASTER, GROUP_NAME | ||
| from spatialdata._types import ELEMENT_TYPE, ELEMENT_TYPE_RASTER | ||
| from spatialdata._utils import get_pyramid_levels | ||
| from spatialdata.models.models import ATTRS_KEY | ||
| from spatialdata.models.pyramids_utils import dask_arrays_to_datatree | ||
|
|
@@ -40,6 +44,8 @@ | |
| _set_transformations, | ||
| compute_coordinates, | ||
| ) | ||
| from spatialdata.transformations.graph.edge import BaseTransformationEdge, parse_ngff_transf | ||
| from spatialdata.transformations.graph.vert import Axis, CoordSystem | ||
|
|
||
|
|
||
| def _is_flat_int_sequence(value: object) -> TypeGuard[Sequence[int]]: | ||
|
|
@@ -162,6 +168,97 @@ def _prepare_storage_options( | |
| return prepared_options | ||
|
|
||
|
|
||
| def try_read_ngff06_multiscale(store: Path) -> tuple[DataTree, Sequence[BaseTransformationEdge]]: | ||
| multiscale = oz.OMEZarrMultiscale.from_ome_zarr(str(store)) | ||
| assert isinstance(multiscale, oz.OMEZarrMultiscale) # disambiguate from OMEZarrLabel | ||
| return try_parse_ngff06_multiscale(multiscale) | ||
|
|
||
|
|
||
| def try_parse_ngff06_multiscale(multiscale: oz.OMEZarrMultiscale) -> tuple[DataTree, Sequence[BaseTransformationEdge]]: | ||
| """Parse an OMEZarMultiscale into a DataTree and collects Multiscale-level transforms.""" | ||
| name_to_cs: dict[str, CoordSystem] = {} | ||
| for cs in multiscale.metadata.coordinateSystems or (): | ||
| parsed_cs = CoordSystem.try_from_model(cs) | ||
| name_to_cs[cs.name] = parsed_cs | ||
|
|
||
| parsed_transfs: list[BaseTransformationEdge] = [] | ||
| for transf in multiscale.metadata.coordinateTransformations or (): | ||
| in_cs_id = transf.input | ||
| out_cs_ref = transf.output | ||
| # these should not be None as per the spec | ||
| assert in_cs_id is not None | ||
| assert out_cs_ref is not None | ||
|
|
||
| # FIXME: not handling references into labels yet, which use name and path | ||
| in_cs_name = in_cs_id.name | ||
| out_cs_name = out_cs_ref.name | ||
| assert in_cs_name is not None | ||
| assert out_cs_name is not None | ||
|
|
||
| # assume CS references are valid via ome-zarr(-models)-py | ||
| input = name_to_cs[in_cs_name] | ||
| output = name_to_cs[out_cs_name] | ||
| parsed = parse_ngff_transf(input=input, output=output, model=transf) | ||
| parsed_transfs.append(parsed) | ||
|
|
||
| omero = multiscale.omero | ||
| channel_names = None if omero is None else [d.color for d in omero.channels] | ||
|
|
||
| data_tree = xr.DataTree() | ||
| for scale_idx, (ds_md, ds) in enumerate(zip(multiscale.metadata.datasets, multiscale.images, strict=True)): | ||
| transf = ds_md.coordinateTransformations[0] | ||
|
|
||
| intrinsic_cs = name_to_cs[multiscale.metadata.intrinsic_coordinate_system.name] | ||
| assert transf.input is not None | ||
| assert transf.input.path is not None | ||
|
|
||
| # This coord system doesn't exist explicitly in the NGFF file, nor will | ||
| # it exist in our graph of transformations; It is only created here | ||
| # for the sake of creating the transformations that will be expressed | ||
| # in levels of a xr.DataTree | ||
| pixel_cs = CoordSystem( | ||
| name=transf.input.path, | ||
| axes=tuple(Axis(name=ax.name, type=ax.type) for ax in intrinsic_cs.axes), | ||
| virtual=True, | ||
| ) | ||
|
|
||
| pixel_cs_to_intrinsic_ngff = ozm06trans.Sequence(transformations=ds_md.coordinateTransformations) | ||
| seq = parse_ngff_transf(input=pixel_cs, output=intrinsic_cs, model=pixel_cs_to_intrinsic_ngff) | ||
| ds_shape = np.asarray(ds.data.shape) | ||
| transformed_start = seq.transform_points(np.zeros_like(ds_shape)[np.newaxis, :])[0] | ||
| transformed_stop = seq.transform_points((ds_shape - 1)[np.newaxis, :])[0] | ||
|
|
||
| coords: xr.Coordinates = xr.Coordinates() | ||
| for low, high, ax, extent in zip(transformed_start, transformed_stop, intrinsic_cs.axes, ds_shape, strict=True): | ||
| if ax.type == "channel" and channel_names is not None: | ||
| coords = coords.merge({ax.name: channel_names}).coords | ||
| else: | ||
| axis_index = xr.Coordinates.from_xindex( | ||
| RangeIndex.linspace( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looking good |
||
| start=low, | ||
| stop=high, | ||
| num=extent, | ||
| endpoint=True, | ||
| dim=ax.name, | ||
| ) | ||
| ) | ||
| coords = coords.merge(axis_index).coords | ||
|
|
||
| # Note: the magic "image" and "scale<N> " strings mimic the current | ||
| # behavior from `dask_arrays_to_datatree` | ||
| data_tree[f"scale{scale_idx}"] = xr.Dataset( | ||
| { | ||
| "image": xr.DataArray( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not for this PR (we can "resolve the conversation"), but a reminder. This string and the E.g. will the users expect to have always CC @jan-glx
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yup, I agree. I am replicating the current behavior, but I'd really rather have it not use magic strings at all, even if well documented |
||
| ds.data, | ||
| name="image", | ||
| dims=intrinsic_cs.axes_names, | ||
| coords=coords, | ||
| ) | ||
| }, | ||
| ) | ||
| return data_tree, parsed_transfs | ||
|
|
||
|
|
||
| def _read_multiscale( | ||
| store: str | Path, raster_type: ELEMENT_TYPE_RASTER, reader_format: Format | ||
| ) -> DataArray | DataTree: | ||
|
|
@@ -267,9 +364,7 @@ def _get_multiscale_nodes(image_nodes: list[Node], nodes: list[Node]) -> list[No | |
| return nodes | ||
|
|
||
|
|
||
| def _get_raster_element_group( | ||
| raster_type: ELEMENT_TYPE_RASTER, group: zarr.Group, element_name: str | ||
| ) -> zarr.Group: | ||
| def _get_raster_element_group(raster_type: ELEMENT_TYPE_RASTER, group: zarr.Group, element_name: str) -> zarr.Group: | ||
| """Get the Zarr group holding a raster element that has just been written. | ||
|
|
||
| Labels are nested one level deeper than images: ome-zarr writes them inside a "labels" group, so for them the | ||
|
|
@@ -288,7 +383,7 @@ def _get_raster_element_group( | |
| ------- | ||
| The Zarr group of the raster element. | ||
| """ | ||
| if raster_type != "labels": | ||
| if raster_type != ELEMENT_TYPE.LABELS: | ||
| return group | ||
| labels_group = group["labels"] | ||
| if not isinstance(labels_group, zarr.Group): | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reminder: later when we work with
Scenes, the coordinate system name is not enough for uniquely identifying a CS. It will be the combniation of path where the cs is defined, and the name.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I saw later in that in
vert.pythere is already some logic needed for this (theCoordinateSystemIndentifierusage.