-
Notifications
You must be signed in to change notification settings - Fork 26
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
[python] Add export for MultiscaleImage
to SpatialData
#3355
Changes from 3 commits
efd6cce
6ccf214
522282a
44226aa
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 |
---|---|---|
@@ -1,5 +1,6 @@ | ||
geopandas | ||
tifffile | ||
pillow | ||
spatialdata | ||
xarray>=2024.05.0 | ||
spatialdata>=0.2.5 | ||
xarray | ||
dask |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,6 +16,26 @@ | |
return int(value) | ||
|
||
|
||
def _version_less_than(version: str, upper_bound: Tuple[int, int, int]) -> bool: | ||
split_version = version.split(".") | ||
try: | ||
major = _str_to_int(split_version[0]) | ||
minor = _str_to_int(split_version[1]) | ||
patch = _str_to_int(split_version[2]) | ||
except ValueError as err: | ||
raise ValueError(f"Unable to parse version {version}.") from err | ||
print(f"Actual: {(major, minor, patch)} and Compare: {upper_bound}") | ||
return ( | ||
major < upper_bound[0] | ||
or (major == upper_bound[0] and minor < upper_bound[1]) | ||
or ( | ||
major == upper_bound[0] | ||
and minor == upper_bound[1] | ||
and patch < upper_bound[2] | ||
) | ||
) | ||
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. Just as a heads up, It's mostly what we use for this kind of thing in scverse packages. |
||
|
||
|
||
def _read_visium_software_version( | ||
gene_expression_path: Union[str, Path] | ||
) -> Tuple[int, int, int]: | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -3,11 +3,25 @@ | |||||
# | ||||||
# Licensed under the MIT License. | ||||||
import json | ||||||
from typing import Any, Mapping, Optional, Tuple, Union | ||||||
import warnings | ||||||
from typing import Any, Mapping, Optional, Sequence, Tuple, Union | ||||||
|
||||||
import dask.array as da | ||||||
import numpy as np | ||||||
from xarray import DataArray | ||||||
|
||||||
from ._util import _version_less_than | ||||||
|
||||||
try: | ||||||
import spatialdata as sd | ||||||
from spatialdata.models.models import DataTree | ||||||
except ImportError as err: | ||||||
warnings.warn("Experimental spatial exporter requires the spatialdatda package.") | ||||||
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.
Suggested change
|
||||||
raise err | ||||||
try: | ||||||
import xarray as xr | ||||||
except ImportError as err: | ||||||
warnings.warn("Experimental spatial exporter requires the spatialdatda package.") | ||||||
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.
Suggested change
|
||||||
raise err | ||||||
|
||||||
from .. import DenseNDArray | ||||||
from ..options._soma_tiledb_context import SOMATileDBContext | ||||||
|
@@ -112,7 +126,7 @@ | |||||
chunks: Optional[Tuple[int, ...]] = None, | ||||||
attrs: Optional[Mapping[str, Any]] = None, | ||||||
context: Optional[SOMATileDBContext] = None, | ||||||
) -> DataArray: | ||||||
) -> xr.DataArray: | ||||||
"""Create a :class:`xarray.DataArray` that accesses a SOMA :class:`DenseNDarray` | ||||||
through dask. | ||||||
|
||||||
|
@@ -139,4 +153,19 @@ | |||||
fancy=False, | ||||||
) | ||||||
|
||||||
return DataArray(data, dims=dim_names, attrs=attrs) | ||||||
return xr.DataArray(data, dims=dim_names, attrs=attrs) | ||||||
|
||||||
|
||||||
def images_to_datatree(image_data_arrays: Sequence[xr.DataArray]) -> DataTree: | ||||||
# If SpatialData version < 0.2.6 use the legacy xarray_datatree implementation | ||||||
# of the DataTree. | ||||||
if _version_less_than(sd.__version__, (0, 2, 5)): | ||||||
return DataTree.from_dict( | ||||||
{f"scale{index}": image for index, image in enumerate(image_data_arrays)} | ||||||
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. Why add the 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. It's what SpatialData uses: see https://github.com/scverse/spatialdata/blob/main/src/spatialdata/models/models.py#L256 |
||||||
) | ||||||
return DataTree.from_dict( | ||||||
{ | ||||||
f"scale{index}": xr.Dataset({"image": image}) | ||||||
for index, image in enumerate(image_data_arrays) | ||||||
} | ||||||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,17 +2,29 @@ | |
# Copyright (c) 2024 TileDB, Inc | ||
# | ||
# Licensed under the MIT License. | ||
from typing import Dict, Optional, Tuple, Union | ||
import warnings | ||
from typing import TYPE_CHECKING, Dict, Optional, Tuple, Union | ||
|
||
import geopandas as gpd | ||
try: | ||
import geopandas as gpd | ||
except ImportError as err: | ||
warnings.warn("Experimental spatial outgestor requires the geopandas package.") | ||
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. I think it might make sense to import spatial data first, since geopandas is a hard dep there. Then there isn't the UX of |
||
raise err | ||
import pandas as pd | ||
import somacore | ||
import spatialdata as sd | ||
import xarray as xr | ||
|
||
try: | ||
import spatialdata as sd | ||
except ImportError as err: | ||
warnings.warn("Experimental spatial outgestor requires the spatialdata package.") | ||
raise err | ||
|
||
from .. import MultiscaleImage, PointCloudDataFrame | ||
from .._constants import SOMA_JOINID | ||
from ._xarray_backend import dense_nd_array_to_data_array | ||
from ._xarray_backend import dense_nd_array_to_data_array, images_to_datatree | ||
|
||
if TYPE_CHECKING: | ||
from spatialdata.models.models import DataArray, DataTree | ||
|
||
|
||
def _convert_axis_names( | ||
|
@@ -195,7 +207,7 @@ | |
scene_id: str, | ||
scene_dim_map: Dict[str, str], | ||
transform: somacore.CoordinateTransform, | ||
) -> xr.DataArray: | ||
) -> "DataArray": | ||
"""Export a level of a :class:`MultiscaleImage` to a | ||
:class:`spatialdata.Image2DModel` or :class:`spatialdata.Image3DModel`. | ||
""" | ||
|
@@ -256,3 +268,78 @@ | |
attrs={"transform": transformations}, | ||
context=image.context, | ||
) | ||
|
||
|
||
def to_spatial_data_multiscale_image( | ||
image: MultiscaleImage, | ||
*, | ||
scene_id: str, | ||
scene_dim_map: Dict[str, str], | ||
transform: somacore.CoordinateTransform, | ||
) -> "DataTree": | ||
"""Export a MultiscaleImage to a DataTree.""" | ||
|
||
# Check for channel axis. | ||
if not image.has_channel_axis: | ||
raise NotImplementedError( | ||
"Support for exporting a MultiscaleImage to without a channel axis to " | ||
"SpatialData is not yet implemented." | ||
) | ||
|
||
# Convert from SOMA axis names to SpatialData axis names. | ||
orig_axis_names = image.coordinate_space.axis_names | ||
if len(orig_axis_names) not in {2, 3}: | ||
raise NotImplementedError( | ||
f"Support for converting a '{len(orig_axis_names)}'D is not yet implemented." | ||
) | ||
new_axis_names, image_dim_map = _convert_axis_names( | ||
orig_axis_names, image.data_axis_order | ||
) | ||
|
||
# Get the transformtion from the image level to the scene: | ||
# If the result is a single scale transform (or identity transform), output a | ||
# single transformation. Otherwise, convert to a SpatialData sequence of | ||
# transformations. | ||
inv_transform = transform.inverse_transform() | ||
if isinstance(transform, somacore.ScaleTransform): | ||
# inv_transform @ scale_transform -> applies scale_transform first | ||
spatial_data_transformations = tuple( | ||
_transform_to_spatial_data( | ||
inv_transform @ image.get_transform_from_level(level), | ||
image_dim_map, | ||
scene_dim_map, | ||
) | ||
for level in range(image.level_count) | ||
) | ||
|
||
else: | ||
sd_scale_transforms = tuple( | ||
_transform_to_spatial_data( | ||
image.get_transform_from_level(level), image_dim_map, image_dim_map | ||
) | ||
for level in range(1, image.level_count) | ||
) | ||
sd_inv_transform = _transform_to_spatial_data( | ||
inv_transform, image_dim_map, scene_dim_map | ||
) | ||
|
||
# First level transform is always the identity, so just directly use | ||
# inv_transform. For remaining transformations, | ||
# Sequence([sd_transform1, sd_transform2]) -> applies sd_transform1 first | ||
spatial_data_transformations = (sd_inv_transform,) + tuple( | ||
sd.transformations.Sequence([scale_transform, sd_inv_transform]) | ||
for scale_transform in sd_scale_transforms | ||
) | ||
|
||
# Create a sequence of resolution level. | ||
image_data_arrays = tuple( | ||
dense_nd_array_to_data_array( | ||
uri=image.level_uri(index), | ||
dim_names=new_axis_names, | ||
attrs={"transform": {scene_id: spatial_data_transformations[index]}}, | ||
context=image.context, | ||
) | ||
for index, (soma_name, val) in enumerate(image.levels().items()) | ||
) | ||
|
||
return images_to_datatree(image_data_arrays) |
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.