spatialdata-io 0.1.4__tar.gz → 0.1.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/CHANGELOG.md +12 -1
  2. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/PKG-INFO +2 -2
  3. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/_version.py +2 -2
  4. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/pyproject.toml +1 -1
  5. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/src/spatialdata_io/_constants/_constants.py +3 -3
  6. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/src/spatialdata_io/readers/_utils/_utils.py +2 -2
  7. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/src/spatialdata_io/readers/codex.py +1 -0
  8. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/src/spatialdata_io/readers/cosmx.py +1 -0
  9. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/src/spatialdata_io/readers/mcmicro.py +6 -4
  10. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/src/spatialdata_io/readers/merscope.py +2 -1
  11. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/src/spatialdata_io/readers/steinbock.py +1 -1
  12. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/src/spatialdata_io/readers/visium.py +16 -9
  13. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/src/spatialdata_io/readers/visium_hd.py +53 -18
  14. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/src/spatialdata_io/readers/xenium.py +41 -19
  15. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/.bumpversion.cfg +0 -0
  16. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/.codecov.yaml +0 -0
  17. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/.editorconfig +0 -0
  18. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/.flake8 +0 -0
  19. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/.github/codecov.yml +0 -0
  20. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/.github/workflows/build.yaml +0 -0
  21. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/.github/workflows/prepare_test_data.yaml +0 -0
  22. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/.github/workflows/release.yaml +0 -0
  23. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/.github/workflows/test.yaml +0 -0
  24. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/.gitignore +0 -0
  25. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/.mypy.ini +0 -0
  26. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/.pre-commit-config.yaml +0 -0
  27. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/.readthedocs.yaml +0 -0
  28. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/LICENSE +0 -0
  29. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/README.md +0 -0
  30. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/docs/Makefile +0 -0
  31. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/docs/_static/.gitkeep +0 -0
  32. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/docs/_templates/.gitkeep +0 -0
  33. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/docs/_templates/autosummary/class.rst +0 -0
  34. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/docs/api.md +0 -0
  35. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/docs/changelog.md +0 -0
  36. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/docs/conf.py +0 -0
  37. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/docs/contributing.md +0 -0
  38. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/docs/extensions/.gitkeep +0 -0
  39. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/docs/extensions/typed_returns.py +0 -0
  40. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/docs/index.md +0 -0
  41. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/docs/make.bat +0 -0
  42. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/docs/references.bib +0 -0
  43. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/docs/references.md +0 -0
  44. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/docs/template_usage.md +0 -0
  45. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/src/spatialdata_io/__init__.py +0 -0
  46. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/src/spatialdata_io/_constants/__init__.py +0 -0
  47. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/src/spatialdata_io/_constants/_enum.py +0 -0
  48. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/src/spatialdata_io/_docs.py +0 -0
  49. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/src/spatialdata_io/_utils.py +0 -0
  50. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/src/spatialdata_io/converters/__init__.py +0 -0
  51. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/src/spatialdata_io/converters/legacy_anndata.py +0 -0
  52. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/src/spatialdata_io/experimental/__init__.py +0 -0
  53. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/src/spatialdata_io/readers/__init__.py +0 -0
  54. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/src/spatialdata_io/readers/_utils/__init__.py +0 -0
  55. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/src/spatialdata_io/readers/_utils/_read_10x_h5.py +0 -0
  56. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/src/spatialdata_io/readers/curio.py +0 -0
  57. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/src/spatialdata_io/readers/dbit.py +0 -0
  58. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/src/spatialdata_io/readers/iss.py +0 -0
  59. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/src/spatialdata_io/readers/seqfish.py +0 -0
  60. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/src/spatialdata_io/readers/stereoseq.py +0 -0
  61. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/tests/__init__.py +0 -0
  62. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/tests/converters/__init__.py +0 -0
  63. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/tests/converters/test_legacy_anndata.py +0 -0
  64. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/tests/test_basic.py +0 -0
  65. {spatialdata_io-0.1.4 → spatialdata_io-0.1.5}/tests/test_xenium.py +0 -0
@@ -8,7 +8,18 @@ and this project adheres to [Semantic Versioning][].
8
8
  [keep a changelog]: https://keepachangelog.com/en/1.0.0/
9
9
  [semantic versioning]: https://semver.org/spec/v2.0.0.html
10
10
 
11
- ## [0.1.4] - xxxx-xx-xx
11
+ ## [0.1.5] - 2024-09-25
12
+
13
+ ### Added
14
+
15
+ - (Xenium) added `dims` parameter for more control in `xenium_aligned_image()`
16
+
17
+ ### Fixed
18
+
19
+ - Passing `rgb=None` to image model parser for both visium and visiumhd, leading to 3-4 channel images being
20
+ interpreted as RGB(A)
21
+ - Fix header bug Visium data #200
22
+ - (Visium HD) Fix path parsing when images are missing #204 #206
12
23
 
13
24
  ## [0.1.4] - 2024-08-07
14
25
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: spatialdata-io
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: SpatialData IO for common techs
5
5
  Project-URL: Documentation, https://spatialdata-io.readthedocs.io/
6
6
  Project-URL: Source, https://github.com/scverse/spatialdata-io
@@ -48,7 +48,7 @@ Requires-Dist: pyarrow
48
48
  Requires-Dist: readfcs
49
49
  Requires-Dist: scanpy
50
50
  Requires-Dist: scikit-image
51
- Requires-Dist: spatialdata>=0.2.1
51
+ Requires-Dist: spatialdata>=0.2.2
52
52
  Requires-Dist: tifffile>=2023.8.12
53
53
  Provides-Extra: dev
54
54
  Requires-Dist: bump2version; extra == 'dev'
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.1.4'
16
- __version_tuple__ = version_tuple = (0, 1, 4)
15
+ __version__ = version = '0.1.5'
16
+ __version_tuple__ = version_tuple = (0, 1, 5)
@@ -25,7 +25,7 @@ dependencies = [
25
25
  "anndata",
26
26
  "numpy",
27
27
  "scanpy",
28
- "spatialdata>=0.2.1",
28
+ "spatialdata>=0.2.2",
29
29
  "scikit-image",
30
30
  "h5py",
31
31
  "joblib",
@@ -353,9 +353,9 @@ class VisiumHDKeys(ModeEnum):
353
353
  TISSUE_POSITIONS_FILE = "tissue_positions.parquet"
354
354
 
355
355
  # images
356
- IMAGE_HIRES_FILE = "spatial/tissue_hires_image.png"
357
- IMAGE_LOWRES_FILE = "spatial/tissue_lowres_image.png"
358
- IMAGE_CYTASSIST = "spatial/cytassist_image.tiff"
356
+ IMAGE_HIRES_FILE = "tissue_hires_image.png"
357
+ IMAGE_LOWRES_FILE = "tissue_lowres_image.png"
358
+ IMAGE_CYTASSIST = "cytassist_image.tiff"
359
359
 
360
360
  # scalefactors
361
361
  SCALEFACTORS_FILE = "scalefactors_json.json"
@@ -18,8 +18,8 @@ try:
18
18
 
19
19
  NDArrayA = NDArray[Any]
20
20
  except (ImportError, TypeError):
21
- NDArray = np.ndarray # type: ignore[misc]
22
- NDArrayA = np.ndarray # type: ignore[misc]
21
+ NDArray = np.ndarray
22
+ NDArrayA = np.ndarray
23
23
 
24
24
 
25
25
  def _read_counts(
@@ -80,6 +80,7 @@ def codex(
80
80
  "images": Image2DModel.parse(
81
81
  image,
82
82
  scale_factors=[2, 2],
83
+ rgb=None,
83
84
  )
84
85
  }
85
86
  sdata = SpatialData(images=images, shapes={str(region): shapes}, table=table)
@@ -200,6 +200,7 @@ def cosmx(
200
200
  "global_only_image": aff,
201
201
  },
202
202
  dims=("y", "x", "c"),
203
+ rgb=None,
203
204
  **image_models_kwargs,
204
205
  )
205
206
  images[f"{fov}_image"] = parsed_im
@@ -100,7 +100,7 @@ def mcmicro(
100
100
  assert len(samples) == 1
101
101
  data = imread(samples[0], **imread_kwargs)
102
102
  # , scale_factors=[2, 2]
103
- data = Image2DModel.parse(data, transformations=_get_transformation(), **image_models_kwargs)
103
+ data = Image2DModel.parse(data, transformations=_get_transformation(), rgb=None, **image_models_kwargs)
104
104
  images["tma_map"] = data
105
105
 
106
106
  image_dir = path / McmicroKeys.IMAGES_DIR_TMA
@@ -116,7 +116,7 @@ def mcmicro(
116
116
  image_id = core_id
117
117
 
118
118
  data = imread(sample, **imread_kwargs)
119
- data = Image2DModel.parse(data, c_coords=marker_names, **image_models_kwargs)
119
+ data = Image2DModel.parse(data, c_coords=marker_names, rgb=None, **image_models_kwargs)
120
120
  transformations = _get_transformation(
121
121
  tma=int(core_id) if tma else None, tma_centroids=tma_centroids, raster_data=data
122
122
  )
@@ -131,7 +131,9 @@ def mcmicro(
131
131
  raw_name = raw_image.with_name(raw_image.stem).with_suffix("").stem
132
132
 
133
133
  data = imread(raw_image, **imread_kwargs)
134
- images[raw_name] = Image2DModel.parse(data, transformations={raw_name: Identity()}, **image_models_kwargs)
134
+ images[raw_name] = Image2DModel.parse(
135
+ data, transformations={raw_name: Identity()}, rgb=None, **image_models_kwargs
136
+ )
135
137
 
136
138
  illumination_dir = path / McmicroKeys.ILLUMINATION_DIR
137
139
  if illumination_dir.exists():
@@ -143,7 +145,7 @@ def mcmicro(
143
145
 
144
146
  data = imread(illumination_image, **imread_kwargs)
145
147
  images[illumination_name] = Image2DModel.parse(
146
- data, transformations={raw_name: Identity()}, **image_models_kwargs
148
+ data, transformations={raw_name: Identity()}, rgb=None, **image_models_kwargs
147
149
  )
148
150
 
149
151
  samples_labels = list((path / McmicroKeys.LABELS_DIR).glob("*/*" + McmicroKeys.IMAGE_SUFFIX))
@@ -273,7 +273,7 @@ def _rioxarray_load_merscope(
273
273
  dim="c",
274
274
  )
275
275
 
276
- return Image2DModel.parse(im, c_coords=stainings, **image_models_kwargs)
276
+ return Image2DModel.parse(im, c_coords=stainings, rgb=None, **image_models_kwargs)
277
277
 
278
278
 
279
279
  def _dask_image_load_merscope(
@@ -292,6 +292,7 @@ def _dask_image_load_merscope(
292
292
  im,
293
293
  dims=("c", "y", "x"),
294
294
  c_coords=stainings,
295
+ rgb=None,
295
296
  **image_models_kwargs,
296
297
  )
297
298
 
@@ -97,7 +97,7 @@ def _get_images(
97
97
  image_models_kwargs: Mapping[str, Any] = MappingProxyType({}),
98
98
  ) -> Union[SpatialImage, MultiscaleSpatialImage]:
99
99
  image = imread(path / SteinbockKeys.IMAGES_DIR / f"{sample}{SteinbockKeys.IMAGE_SUFFIX}", **imread_kwargs)
100
- return Image2DModel.parse(data=image, transformations={sample: Identity()}, **image_models_kwargs)
100
+ return Image2DModel.parse(data=image, transformations={sample: Identity()}, rgb=None, **image_models_kwargs)
101
101
 
102
102
 
103
103
  def _get_labels(
@@ -4,7 +4,6 @@ import json
4
4
  import os
5
5
  import re
6
6
  from collections.abc import Mapping
7
- from functools import partial
8
7
  from pathlib import Path
9
8
  from types import MappingProxyType
10
9
  from typing import Any
@@ -138,7 +137,6 @@ def visium(
138
137
  if (path / "spatial" / VisiumKeys.SPOTS_FILE_1).exists() or (
139
138
  tissue_positions_file is not None and str(VisiumKeys.SPOTS_FILE_1) in str(tissue_positions_file)
140
139
  ):
141
- read_coords = partial(pd.read_csv, header=None, index_col=0)
142
140
  tissue_positions_file = (
143
141
  path / "spatial" / VisiumKeys.SPOTS_FILE_1
144
142
  if tissue_positions_file is None
@@ -147,7 +145,6 @@ def visium(
147
145
  elif (path / "spatial" / VisiumKeys.SPOTS_FILE_2).exists() or (
148
146
  tissue_positions_file is not None and str(VisiumKeys.SPOTS_FILE_2) in str(tissue_positions_file)
149
147
  ):
150
- read_coords = partial(pd.read_csv, header=0, index_col=0)
151
148
  tissue_positions_file = (
152
149
  path / "spatial" / VisiumKeys.SPOTS_FILE_2
153
150
  if tissue_positions_file is None
@@ -155,11 +152,18 @@ def visium(
155
152
  )
156
153
  else:
157
154
  raise ValueError(f"Cannot find `tissue_positions` file in `{path}`.")
158
- coords = read_coords(tissue_positions_file)
155
+ coords = pd.read_csv(tissue_positions_file, header=None, index_col=0)
159
156
 
160
- # to handle spaceranger_version < 2.0.0, where no column names are provided
161
- if "in_tissue" not in coords.columns:
162
- coords.columns = ["in_tissue", "array_row", "array_col", "pxl_row_in_fullres", "pxl_col_in_fullres"]
157
+ if tissue_positions_file.name == VisiumKeys.SPOTS_FILE_1:
158
+ # spaceranger_version < 2.0.0 but header detected: https://github.com/scverse/spatialdata-io/issues/146
159
+ if "in_tissue" in coords.iloc[0].tolist():
160
+ coords = pd.read_csv(tissue_positions_file, header=0, index_col=0)
161
+ else:
162
+ assert "in_tissue" not in coords.columns
163
+ coords.columns = ["in_tissue", "array_row", "array_col", "pxl_row_in_fullres", "pxl_col_in_fullres"]
164
+ else:
165
+ assert tissue_positions_file.name == VisiumKeys.SPOTS_FILE_2
166
+ coords = pd.read_csv(tissue_positions_file, header=0, index_col=0)
163
167
 
164
168
  adata.obs = pd.merge(adata.obs, coords, how="left", left_index=True, right_index=True)
165
169
  coords = adata.obs[[VisiumKeys.SPOTS_X, VisiumKeys.SPOTS_Y]].values
@@ -217,6 +221,7 @@ def visium(
217
221
  full_image,
218
222
  scale_factors=[2, 2, 2, 2],
219
223
  transformations={"global": transform_original},
224
+ rgb=None,
220
225
  **image_models_kwargs,
221
226
  )
222
227
  else:
@@ -226,13 +231,15 @@ def visium(
226
231
  image_hires = imread(path / VisiumKeys.IMAGE_HIRES_FILE, **imread_kwargs).squeeze().transpose(2, 0, 1)
227
232
  image_hires = DataArray(image_hires, dims=("c", "y", "x"))
228
233
  images[dataset_id + "_hires_image"] = Image2DModel.parse(
229
- image_hires, transformations={"downscaled_hires": Identity()}
234
+ image_hires, transformations={"downscaled_hires": Identity()}, rgb=None
230
235
  )
231
236
  if (path / VisiumKeys.IMAGE_LOWRES_FILE).exists():
232
237
  image_lowres = imread(path / VisiumKeys.IMAGE_LOWRES_FILE, **imread_kwargs).squeeze().transpose(2, 0, 1)
233
238
  image_lowres = DataArray(image_lowres, dims=("c", "y", "x"))
234
239
  images[dataset_id + "_lowres_image"] = Image2DModel.parse(
235
- image_lowres, transformations={"downscaled_lowres": Identity()}
240
+ image_lowres,
241
+ transformations={"downscaled_lowres": Identity()},
242
+ rgb=None,
236
243
  )
237
244
 
238
245
  return SpatialData(images=images, shapes=shapes, table=table)
@@ -86,15 +86,14 @@ def visium_hd(
86
86
  SpatialData object for the Visium HD data.
87
87
  """
88
88
  path = Path(path)
89
+ all_files = [file for file in path.rglob("*") if file.is_file()]
89
90
  tables = {}
90
91
  shapes = {}
91
92
  images: dict[str, Any] = {}
92
93
 
93
94
  if dataset_id is None:
94
95
  dataset_id = _infer_dataset_id(path)
95
- filename_prefix = f"{dataset_id}_"
96
- else:
97
- filename_prefix = ""
96
+ filename_prefix = f"{dataset_id}_"
98
97
 
99
98
  def load_image(path: Path, suffix: str, scale_factors: list[int] | None = None) -> None:
100
99
  _load_image(
@@ -127,8 +126,12 @@ def visium_hd(
127
126
  ]
128
127
  )
129
128
 
130
- if VisiumHDKeys.BINNED_OUTPUTS in os.listdir(path):
131
- path_bins = path / VisiumHDKeys.BINNED_OUTPUTS
129
+ all_path_bins = [path_bin for path_bin in all_files if VisiumHDKeys.BINNED_OUTPUTS in str(path_bin)]
130
+ if len(all_path_bins) != 0:
131
+ path_bins_parts = all_path_bins[
132
+ -1
133
+ ].parts # just choosing last one here as users might have tar file which would be first
134
+ path_bins = Path(*path_bins_parts[: path_bins_parts.index(VisiumHDKeys.BINNED_OUTPUTS) + 1])
132
135
  else:
133
136
  path_bins = path
134
137
  all_bin_sizes = _get_bins(path_bins)
@@ -266,15 +269,26 @@ def visium_hd(
266
269
  fullres_image_filenames = [
267
270
  f for f in os.listdir(path_fullres) if os.path.isfile(os.path.join(path_fullres, f))
268
271
  ]
269
- if len(fullres_image_filenames) > 1:
270
- warnings.warn(
271
- f"Multiple files found in {path_fullres}, using the first one: {fullres_image_filenames[0]}. Please"
272
- " specify the path to the full resolution image manually using the `fullres_image_file` argument.",
273
- UserWarning,
274
- stacklevel=2,
275
- )
276
- fullres_image_filename = fullres_image_filenames[0]
277
- fullres_image_file = path_fullres / fullres_image_filename
272
+ fullres_image_paths = [path_fullres / image_filename for image_filename in fullres_image_filenames]
273
+ elif list((path_fullres := (path / f"{filename_prefix}tissue_image")).parent.glob(f"{path_fullres.name}.*")):
274
+ fullres_image_paths = list(path_fullres.parent.glob(f"{path_fullres.name}.*"))
275
+ else:
276
+ fullres_image_paths = []
277
+ if len(fullres_image_paths) > 1:
278
+ warnings.warn(
279
+ f"Multiple files found in {path_fullres}, using the first one: {fullres_image_paths[0].stem}. Please"
280
+ " specify the path to the full resolution image manually using the `fullres_image_file` argument.",
281
+ UserWarning,
282
+ stacklevel=2,
283
+ )
284
+ if len(fullres_image_paths) == 0:
285
+ warnings.warn(
286
+ "No full resolution image found. If incorrect, please specify the path in the "
287
+ "`fullres_image_file` parameter when calling the `visium_hd` reader function.",
288
+ UserWarning,
289
+ stacklevel=2,
290
+ )
291
+ fullres_image_file = fullres_image_paths[0] if len(fullres_image_paths) > 0 else None
278
292
 
279
293
  if fullres_image_file is not None:
280
294
  load_image(
@@ -284,8 +298,15 @@ def visium_hd(
284
298
  )
285
299
 
286
300
  # hires image
301
+ hires_image_path = [path for path in all_files if VisiumHDKeys.IMAGE_HIRES_FILE in str(path)]
302
+ if len(hires_image_path) == 0:
303
+ warnings.warn(
304
+ f"No image path found containing the hires image: {VisiumHDKeys.IMAGE_HIRES_FILE}",
305
+ UserWarning,
306
+ stacklevel=2,
307
+ )
287
308
  load_image(
288
- path=path / VisiumHDKeys.IMAGE_HIRES_FILE,
309
+ path=hires_image_path[0],
289
310
  suffix="_hires_image",
290
311
  )
291
312
  set_transformation(
@@ -295,8 +316,15 @@ def visium_hd(
295
316
  )
296
317
 
297
318
  # lowres image
319
+ lowres_image_path = [path for path in all_files if VisiumHDKeys.IMAGE_LOWRES_FILE in str(path)]
320
+ if len(lowres_image_path) == 0:
321
+ warnings.warn(
322
+ f"No image path found containing the lowres image: {VisiumHDKeys.IMAGE_LOWRES_FILE}",
323
+ UserWarning,
324
+ stacklevel=2,
325
+ )
298
326
  load_image(
299
- path=path / VisiumHDKeys.IMAGE_LOWRES_FILE,
327
+ path=lowres_image_path[0],
300
328
  suffix="_lowres_image",
301
329
  )
302
330
  set_transformation(
@@ -306,9 +334,16 @@ def visium_hd(
306
334
  )
307
335
 
308
336
  # cytassist image
337
+ cytassist_path = [path for path in all_files if VisiumHDKeys.IMAGE_CYTASSIST in str(path)]
338
+ if len(cytassist_path) == 0:
339
+ warnings.warn(
340
+ f"No image path found containing the cytassist image: {VisiumHDKeys.IMAGE_CYTASSIST}",
341
+ UserWarning,
342
+ stacklevel=2,
343
+ )
309
344
  if load_all_images:
310
345
  load_image(
311
- path=path / VisiumHDKeys.IMAGE_CYTASSIST,
346
+ path=cytassist_path[0],
312
347
  suffix="_cytassist_image",
313
348
  )
314
349
  image = images[dataset_id + "_cytassist_image"]
@@ -352,7 +387,7 @@ def _load_image(
352
387
  # dask_image doesn't recognize .btf automatically and imageio v3 throws error due to pixel limit -> use imageio v2
353
388
  data = imread2(path, **imread_kwargs).squeeze().transpose(2, 0, 1)
354
389
  image = DataArray(data, dims=("c", "y", "x"))
355
- parsed = Image2DModel.parse(image, scale_factors=scale_factors, **image_models_kwargs)
390
+ parsed = Image2DModel.parse(image, scale_factors=scale_factors, rgb=None, **image_models_kwargs)
356
391
  images[dataset_id + suffix] = parsed
357
392
  else:
358
393
  warnings.warn(f"File {path} does not exist, skipping it.", UserWarning, stacklevel=2)
@@ -22,12 +22,11 @@ import zarr
22
22
  from anndata import AnnData
23
23
  from dask.dataframe import read_parquet
24
24
  from dask_image.imread import imread
25
+ from datatree.datatree import DataTree
25
26
  from geopandas import GeoDataFrame
26
27
  from joblib import Parallel, delayed
27
- from multiscale_spatial_image.multiscale_spatial_image import MultiscaleSpatialImage
28
28
  from pyarrow import Table
29
29
  from shapely import Polygon
30
- from spatial_image import SpatialImage
31
30
  from spatialdata import SpatialData
32
31
  from spatialdata._core.query.relational_query import get_element_instances
33
32
  from spatialdata._types import ArrayLike
@@ -39,6 +38,7 @@ from spatialdata.models import (
39
38
  TableModel,
40
39
  )
41
40
  from spatialdata.transformations.transformations import Affine, Identity, Scale
41
+ from xarray import DataArray
42
42
 
43
43
  from spatialdata_io._constants._constants import XeniumKeys
44
44
  from spatialdata_io._docs import inject_docs
@@ -111,7 +111,9 @@ def xenium(
111
111
  morphology_focus
112
112
  Whether to read the morphology focus image.
113
113
  aligned_images
114
- Whether to also parse, when available, additional H&E or IF aligned images.
114
+ Whether to also parse, when available, additional H&E or IF aligned images. For more control over the aligned
115
+ images being read, in particular, to specify the axes of the aligned images, please set this parameter to
116
+ `False` and use the `xenium_aligned_image` function directly.
115
117
  cells_table
116
118
  Whether to read the cell annotations in the `AnnData` table.
117
119
  n_jobs
@@ -513,6 +515,7 @@ def _get_points(path: Path, specs: dict[str, Any]) -> Table:
513
515
  feature_key=XeniumKeys.FEATURE_NAME,
514
516
  instance_key=XeniumKeys.CELL_ID,
515
517
  transformations={"global": transform},
518
+ sort=True,
516
519
  )
517
520
  return points
518
521
 
@@ -550,7 +553,7 @@ def _get_images(
550
553
  file: str,
551
554
  imread_kwargs: Mapping[str, Any] = MappingProxyType({}),
552
555
  image_models_kwargs: Mapping[str, Any] = MappingProxyType({}),
553
- ) -> SpatialImage | MultiscaleSpatialImage:
556
+ ) -> DataArray | DataTree:
554
557
  image = imread(path / file, **imread_kwargs)
555
558
  if "c_coords" in image_models_kwargs and "dummy" in image_models_kwargs["c_coords"]:
556
559
  # Napari currently interprets 4 channel images as RGB; a series of PRs to fix this is almost ready but they will
@@ -559,7 +562,7 @@ def _get_images(
559
562
  # let's add a dummy channel as a temporary workaround.
560
563
  image = da.concatenate([image, da.zeros_like(image[0:1])], axis=0)
561
564
  return Image2DModel.parse(
562
- image, transformations={"global": Identity()}, dims=("c", "y", "x"), **image_models_kwargs
565
+ image, transformations={"global": Identity()}, dims=("c", "y", "x"), rgb=None, **image_models_kwargs
563
566
  )
564
567
 
565
568
 
@@ -567,7 +570,7 @@ def _add_aligned_images(
567
570
  path: Path,
568
571
  imread_kwargs: Mapping[str, Any] = MappingProxyType({}),
569
572
  image_models_kwargs: Mapping[str, Any] = MappingProxyType({}),
570
- ) -> dict[str, MultiscaleSpatialImage]:
573
+ ) -> dict[str, DataTree]:
571
574
  """Discover and parse aligned images."""
572
575
  images = {}
573
576
  ome_tif_files = list(path.glob("*.ome.tif"))
@@ -598,7 +601,8 @@ def xenium_aligned_image(
598
601
  alignment_file: str | Path | None,
599
602
  imread_kwargs: Mapping[str, Any] = MappingProxyType({}),
600
603
  image_models_kwargs: Mapping[str, Any] = MappingProxyType({}),
601
- ) -> MultiscaleSpatialImage:
604
+ dims: tuple[str, ...] | None = None,
605
+ ) -> DataTree:
602
606
  """
603
607
  Read an image aligned to a Xenium dataset, with an optional alignment file.
604
608
 
@@ -610,6 +614,12 @@ def xenium_aligned_image(
610
614
  Path to the alignment file, if not passed it is assumed that the image is aligned.
611
615
  image_models_kwargs
612
616
  Keyword arguments to pass to the image models.
617
+ dims
618
+ Dimensions of the image (tuple of axes names); valid strings are "c", "x" and "y". If not passed, the function
619
+ will try to infer the dimensions from the image shape. Please use this argument when the default behavior fails.
620
+ Example: for an image with shape (1, y, 1, x, 3), use dims=("anystring", "y", "dummy", "x", "c"). Values that
621
+ are not "c", "x" or "y" are considered dummy dimensions and will be squeezed (the data must have len 1 for
622
+ those axes).
613
623
 
614
624
  Returns
615
625
  -------
@@ -627,19 +637,28 @@ def xenium_aligned_image(
627
637
  # In fact, it could be that the len(image.shape) == 4 has actually dimes (1, x, y, c) and not (1, y, x, c). This is
628
638
  # not a problem because the transformation is constructed to be consistent, but if is the case, the data orientation
629
639
  # would be transposed compared to the original image, not ideal.
630
- if len(image.shape) == 4:
631
- assert image.shape[0] == 1
632
- assert image.shape[-1] == 3
633
- image = image.squeeze(0)
634
- dims = ("y", "x", "c")
640
+ if dims is None:
641
+ if len(image.shape) == 4:
642
+ assert image.shape[0] == 1
643
+ assert image.shape[-1] == 3
644
+ image = image.squeeze(0)
645
+ dims = ("y", "x", "c")
646
+ else:
647
+ assert len(image.shape) == 3
648
+ assert image.shape[0] in [3, 4]
649
+ if image.shape[0] == 4:
650
+ # as explained before in _get_images(), we need to add a dummy channel until we support 4-channel images as
651
+ # non-RGBA images in napari
652
+ image = da.concatenate([image, da.zeros_like(image[0:1])], axis=0)
653
+ dims = ("c", "y", "x")
635
654
  else:
636
- assert len(image.shape) == 3
637
- assert image.shape[0] in [3, 4]
638
- if image.shape[0] == 4:
639
- # as explained before in _get_images(), we need to add a dummy channel until we support 4-channel images as
640
- # non-RGBA images in napari
641
- image = da.concatenate([image, da.zeros_like(image[0:1])], axis=0)
642
- dims = ("c", "y", "x")
655
+ logging.info(f"Image has shape {image.shape}, parsing with dims={dims}.")
656
+ image = DataArray(image, dims=dims)
657
+ # squeeze spurious dimensions away
658
+ to_squeeze = [dim for dim in dims if dim not in ["c", "x", "y"]]
659
+ dims = tuple(dim for dim in dims if dim in ["c", "x", "y"])
660
+ for dim in to_squeeze:
661
+ image = image.squeeze(dim)
643
662
 
644
663
  if alignment_file is None:
645
664
  transformation = Identity()
@@ -762,3 +781,6 @@ def prefix_suffix_uint32_from_cell_id_str(cell_id_str: ArrayLike) -> tuple[Array
762
781
  cell_id_prefix = [int(x, 16) for x in cell_id_prefix_hex]
763
782
 
764
783
  return np.array(cell_id_prefix, dtype=np.uint32), np.array(dataset_suffix_int)
784
+
785
+
786
+ ##
File without changes
File without changes
File without changes
File without changes