-
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
f57dc1d
b351aa0
1d4d7cb
2a8bc97
868c950
7565622
541335f
9d56ff2
675820d
7fa8b26
2bd6eee
9db6798
5538c95
fcbd639
1cd92c8
8963e04
5d197cc
ca0bac1
4b2479f
83c91df
79307e3
d4287bb
a17cb69
367b16e
c2d1e5a
84e3669
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 |
|---|---|---|
|
|
@@ -6,6 +6,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 | ||
|
|
@@ -17,6 +20,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, | ||
|
|
@@ -38,6 +42,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]]: | ||
|
|
@@ -160,6 +166,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: | ||
|
|
||
| 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.