spatialdata-io 0.1.3.post0__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.3.post0 → spatialdata_io-0.1.5}/.pre-commit-config.yaml +2 -2
  2. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/CHANGELOG.md +20 -1
  3. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/PKG-INFO +21 -13
  4. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/README.md +18 -11
  5. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/_version.py +2 -2
  6. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/pyproject.toml +2 -1
  7. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/src/spatialdata_io/_constants/_constants.py +3 -3
  8. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/src/spatialdata_io/converters/legacy_anndata.py +2 -0
  9. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/src/spatialdata_io/readers/_utils/_utils.py +2 -2
  10. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/src/spatialdata_io/readers/codex.py +1 -0
  11. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/src/spatialdata_io/readers/cosmx.py +13 -11
  12. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/src/spatialdata_io/readers/mcmicro.py +6 -4
  13. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/src/spatialdata_io/readers/merscope.py +2 -1
  14. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/src/spatialdata_io/readers/steinbock.py +1 -1
  15. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/src/spatialdata_io/readers/visium.py +16 -9
  16. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/src/spatialdata_io/readers/visium_hd.py +74 -26
  17. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/src/spatialdata_io/readers/xenium.py +78 -29
  18. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/tests/test_xenium.py +2 -2
  19. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/.bumpversion.cfg +0 -0
  20. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/.codecov.yaml +0 -0
  21. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/.editorconfig +0 -0
  22. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/.flake8 +0 -0
  23. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/.github/codecov.yml +0 -0
  24. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/.github/workflows/build.yaml +0 -0
  25. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/.github/workflows/prepare_test_data.yaml +0 -0
  26. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/.github/workflows/release.yaml +0 -0
  27. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/.github/workflows/test.yaml +0 -0
  28. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/.gitignore +0 -0
  29. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/.mypy.ini +0 -0
  30. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/.readthedocs.yaml +0 -0
  31. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/LICENSE +0 -0
  32. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/docs/Makefile +0 -0
  33. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/docs/_static/.gitkeep +0 -0
  34. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/docs/_templates/.gitkeep +0 -0
  35. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/docs/_templates/autosummary/class.rst +0 -0
  36. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/docs/api.md +0 -0
  37. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/docs/changelog.md +0 -0
  38. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/docs/conf.py +0 -0
  39. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/docs/contributing.md +0 -0
  40. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/docs/extensions/.gitkeep +0 -0
  41. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/docs/extensions/typed_returns.py +0 -0
  42. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/docs/index.md +0 -0
  43. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/docs/make.bat +0 -0
  44. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/docs/references.bib +0 -0
  45. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/docs/references.md +0 -0
  46. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/docs/template_usage.md +0 -0
  47. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/src/spatialdata_io/__init__.py +0 -0
  48. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/src/spatialdata_io/_constants/__init__.py +0 -0
  49. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/src/spatialdata_io/_constants/_enum.py +0 -0
  50. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/src/spatialdata_io/_docs.py +0 -0
  51. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/src/spatialdata_io/_utils.py +0 -0
  52. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/src/spatialdata_io/converters/__init__.py +0 -0
  53. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/src/spatialdata_io/experimental/__init__.py +0 -0
  54. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/src/spatialdata_io/readers/__init__.py +0 -0
  55. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/src/spatialdata_io/readers/_utils/__init__.py +0 -0
  56. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/src/spatialdata_io/readers/_utils/_read_10x_h5.py +0 -0
  57. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/src/spatialdata_io/readers/curio.py +0 -0
  58. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/src/spatialdata_io/readers/dbit.py +0 -0
  59. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/src/spatialdata_io/readers/iss.py +0 -0
  60. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/src/spatialdata_io/readers/seqfish.py +0 -0
  61. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/src/spatialdata_io/readers/stereoseq.py +0 -0
  62. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/tests/__init__.py +0 -0
  63. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/tests/converters/__init__.py +0 -0
  64. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/tests/converters/test_legacy_anndata.py +0 -0
  65. {spatialdata_io-0.1.3.post0 → spatialdata_io-0.1.5}/tests/test_basic.py +0 -0
@@ -23,7 +23,7 @@ repos:
23
23
  hooks:
24
24
  - id: isort
25
25
  - repo: https://github.com/pre-commit/mirrors-mypy
26
- rev: v1.10.1
26
+ rev: v1.11.0
27
27
  hooks:
28
28
  - id: mypy
29
29
  additional_dependencies: [numpy, types-PyYAML]
@@ -70,7 +70,7 @@ repos:
70
70
  - flake8-bugbear
71
71
  - flake8-blind-except
72
72
  - repo: https://github.com/asottile/pyupgrade
73
- rev: v3.16.0
73
+ rev: v3.17.0
74
74
  hooks:
75
75
  - id: pyupgrade
76
76
  args: [--py3-plus, --py39-plus, --keep-runtime-typing]
@@ -8,7 +8,26 @@ 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
23
+
24
+ ## [0.1.4] - 2024-08-07
25
+
26
+ ### Changed
27
+
28
+ - (Xenium) changed default target of table to labels; radii of circles computed from cells, not nuclei #179
29
+ - (Visium HD) changed default geometry to squares from circles for the bins; added parameter to choose #183
30
+ - (CosMx) dropping points element with zero-length from the cosmx reader #191
12
31
 
13
32
  ## [0.1.3] - 2024-07-03
14
33
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: spatialdata-io
3
- Version: 0.1.3.post0
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,8 @@ 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
+ Requires-Dist: tifffile>=2023.8.12
52
53
  Provides-Extra: dev
53
54
  Requires-Dist: bump2version; extra == 'dev'
54
55
  Requires-Dist: pre-commit; extra == 'dev'
@@ -82,17 +83,20 @@ Description-Content-Type: text/markdown
82
83
 
83
84
  This package contains reader functions to load common spatial omics formats into SpatialData. Currently, we provide support for:
84
85
 
85
- - 10x Genomics Visium
86
- - 10x Genomics Visium HD
87
- - 10x Genomics Xenium
88
- - Akoya PhenoCycler (formerly CODEX)
89
- - Curio Seeker
86
+ - 10x Genomics Visium®
87
+ - 10x Genomics Visium HD®
88
+ - 10x Genomics Xenium®
89
+ - Akoya PhenoCycler® (formerly CODEX®)
90
+ - Curio Seeker®
90
91
  - DBiT-seq
91
92
  - MCMICRO (output data)
92
- - NanoString CosMx
93
+ - NanoString CosMx®
94
+ - Spatial Genomics GenePS® (seqFISH)
93
95
  - Steinbock (output data)
94
- - STOmics Stereo-seq
95
- - Vizgen MERSCOPE (MERFISH)
96
+ - STOmics Stereo-seq®
97
+ - Vizgen MERSCOPE® (MERFISH)
98
+
99
+ Note: all mentioned technologies are registered trademarks of their respective companies.
96
100
 
97
101
  ## Getting started
98
102
 
@@ -129,9 +133,13 @@ If you found a bug, please use the [issue tracker][issue-tracker].
129
133
  Technologies that can be read into `SpatialData` objects using third-party libraries:
130
134
 
131
135
  - METASPACE (MALDI, ...): [metaspace-converter](https://github.com/metaspace2020/metaspace-converter)
132
- - PhenoCycler: [SOPA](https://github.com/gustaveroussy/sopa)
133
- - MACSima: [SOPA](https://github.com/gustaveroussy/sopa)
134
- - Hyperion (Imaging Mass Cytometry): [SOPA](https://github.com/gustaveroussy/sopa)
136
+ - PhenoCycler®: [SOPA](https://github.com/gustaveroussy/sopa)
137
+ - MACSima®: [SOPA](https://github.com/gustaveroussy/sopa)
138
+ - Hyperion® (Imaging Mass Cytometry): [SOPA](https://github.com/gustaveroussy/sopa)
139
+
140
+ ## Disclaimer
141
+
142
+ This library is community maintained and is not officially endorsed by the aforementioned spatial technology companies. As such, we cannot offer any warranty of the correctness of the representation. Furthermore, we cannot ensure the correctness of the readers for every data version as the technologies evolve and update their formats. If you find a bug or notice a misrepresentation of the data please report it via our [Bug Tracking System](https://github.com/scverse/spatialdata-io/issues?q=sort%3Aupdated-desc+is%3Aissue+is%3Aopen) so that it can be addressed either by the maintainers of this library or by the community.
135
143
 
136
144
  ## Citation
137
145
 
@@ -12,17 +12,20 @@
12
12
 
13
13
  This package contains reader functions to load common spatial omics formats into SpatialData. Currently, we provide support for:
14
14
 
15
- - 10x Genomics Visium
16
- - 10x Genomics Visium HD
17
- - 10x Genomics Xenium
18
- - Akoya PhenoCycler (formerly CODEX)
19
- - Curio Seeker
15
+ - 10x Genomics Visium®
16
+ - 10x Genomics Visium HD®
17
+ - 10x Genomics Xenium®
18
+ - Akoya PhenoCycler® (formerly CODEX®)
19
+ - Curio Seeker®
20
20
  - DBiT-seq
21
21
  - MCMICRO (output data)
22
- - NanoString CosMx
22
+ - NanoString CosMx®
23
+ - Spatial Genomics GenePS® (seqFISH)
23
24
  - Steinbock (output data)
24
- - STOmics Stereo-seq
25
- - Vizgen MERSCOPE (MERFISH)
25
+ - STOmics Stereo-seq®
26
+ - Vizgen MERSCOPE® (MERFISH)
27
+
28
+ Note: all mentioned technologies are registered trademarks of their respective companies.
26
29
 
27
30
  ## Getting started
28
31
 
@@ -59,9 +62,13 @@ If you found a bug, please use the [issue tracker][issue-tracker].
59
62
  Technologies that can be read into `SpatialData` objects using third-party libraries:
60
63
 
61
64
  - METASPACE (MALDI, ...): [metaspace-converter](https://github.com/metaspace2020/metaspace-converter)
62
- - PhenoCycler: [SOPA](https://github.com/gustaveroussy/sopa)
63
- - MACSima: [SOPA](https://github.com/gustaveroussy/sopa)
64
- - Hyperion (Imaging Mass Cytometry): [SOPA](https://github.com/gustaveroussy/sopa)
65
+ - PhenoCycler®: [SOPA](https://github.com/gustaveroussy/sopa)
66
+ - MACSima®: [SOPA](https://github.com/gustaveroussy/sopa)
67
+ - Hyperion® (Imaging Mass Cytometry): [SOPA](https://github.com/gustaveroussy/sopa)
68
+
69
+ ## Disclaimer
70
+
71
+ This library is community maintained and is not officially endorsed by the aforementioned spatial technology companies. As such, we cannot offer any warranty of the correctness of the representation. Furthermore, we cannot ensure the correctness of the readers for every data version as the technologies evolve and update their formats. If you find a bug or notice a misrepresentation of the data please report it via our [Bug Tracking System](https://github.com/scverse/spatialdata-io/issues?q=sort%3Aupdated-desc+is%3Aissue+is%3Aopen) so that it can be addressed either by the maintainers of this library or by the community.
65
72
 
66
73
  ## Citation
67
74
 
@@ -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.3.post0'
16
- __version_tuple__ = version_tuple = (0, 1, 3)
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",
@@ -33,6 +33,7 @@ dependencies = [
33
33
  "dask-image",
34
34
  "pyarrow",
35
35
  "readfcs",
36
+ "tifffile>=2023.8.12",
36
37
  ]
37
38
 
38
39
  [project.optional-dependencies]
@@ -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"
@@ -268,6 +268,8 @@ def from_legacy_anndata(adata: AnnData) -> SpatialData:
268
268
  keys = set(adata.uns[SPATIAL][dataset_id].keys())
269
269
  tissue_hires_scalef = None
270
270
  tissue_lowres_scalef = None
271
+ hires = None
272
+ lowres = None
271
273
  if SCALEFACTORS in keys:
272
274
  scalefactors = adata.uns[SPATIAL][dataset_id][SCALEFACTORS]
273
275
  if TISSUE_HIRES_SCALEF in scalefactors:
@@ -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
@@ -271,17 +272,18 @@ def cosmx(
271
272
  sub_table[CosmxKeys.INSTANCE_KEY] = sub_table[CosmxKeys.INSTANCE_KEY].astype("category")
272
273
  # we rename z because we want to treat the data as 2d
273
274
  sub_table.rename(columns={"z": "z_raw"}, inplace=True)
274
- points[f"{fov}_points"] = PointsModel.parse(
275
- sub_table,
276
- coordinates={"x": CosmxKeys.X_LOCAL_TRANSCRIPT, "y": CosmxKeys.Y_LOCAL_TRANSCRIPT},
277
- feature_key=CosmxKeys.TARGET_OF_TRANSCRIPT,
278
- instance_key=CosmxKeys.INSTANCE_KEY,
279
- transformations={
280
- fov: Identity(),
281
- "global": aff,
282
- "global_only_labels": aff,
283
- },
284
- )
275
+ if len(sub_table) > 0:
276
+ points[f"{fov}_points"] = PointsModel.parse(
277
+ sub_table,
278
+ coordinates={"x": CosmxKeys.X_LOCAL_TRANSCRIPT, "y": CosmxKeys.Y_LOCAL_TRANSCRIPT},
279
+ feature_key=CosmxKeys.TARGET_OF_TRANSCRIPT,
280
+ instance_key=CosmxKeys.INSTANCE_KEY,
281
+ transformations={
282
+ fov: Identity(),
283
+ "global": aff,
284
+ "global_only_labels": aff,
285
+ },
286
+ )
285
287
 
286
288
  # TODO: what to do with fov file?
287
289
  # if fov_file is not None:
@@ -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)
@@ -14,6 +14,7 @@ import numpy as np
14
14
  import pandas as pd
15
15
  import scanpy as sc
16
16
  from dask_image.imread import imread
17
+ from geopandas import GeoDataFrame
17
18
  from imageio import imread as imread2
18
19
  from multiscale_spatial_image import MultiscaleSpatialImage
19
20
  from spatial_image import SpatialImage
@@ -38,6 +39,7 @@ def visium_hd(
38
39
  dataset_id: str | None = None,
39
40
  filtered_counts_file: bool = True,
40
41
  bin_size: int | list[int] | None = None,
42
+ bins_as_squares: bool = True,
41
43
  fullres_image_file: str | Path | None = None,
42
44
  load_all_images: bool = False,
43
45
  imread_kwargs: Mapping[str, Any] = MappingProxyType({}),
@@ -63,6 +65,9 @@ def visium_hd(
63
65
  bin_size
64
66
  When specified, load the data of a specific bin size, or a list of bin sizes. By default, it loads all the
65
67
  available bin sizes.
68
+ bins_as_squares
69
+ If `True`, the bins are represented as squares. If `False`, the bins are represented as circles. For a correct
70
+ visualization one should use squares.
66
71
  fullres_image_file
67
72
  Path to the full-resolution image. By default the image is searched in the ``{vx.MICROSCOPE_IMAGE!r}``
68
73
  directory.
@@ -81,15 +86,14 @@ def visium_hd(
81
86
  SpatialData object for the Visium HD data.
82
87
  """
83
88
  path = Path(path)
89
+ all_files = [file for file in path.rglob("*") if file.is_file()]
84
90
  tables = {}
85
91
  shapes = {}
86
92
  images: dict[str, Any] = {}
87
93
 
88
94
  if dataset_id is None:
89
95
  dataset_id = _infer_dataset_id(path)
90
- filename_prefix = f"{dataset_id}_"
91
- else:
92
- filename_prefix = ""
96
+ filename_prefix = f"{dataset_id}_"
93
97
 
94
98
  def load_image(path: Path, suffix: str, scale_factors: list[int] | None = None) -> None:
95
99
  _load_image(
@@ -122,8 +126,12 @@ def visium_hd(
122
126
  ]
123
127
  )
124
128
 
125
- if VisiumHDKeys.BINNED_OUTPUTS in os.listdir(path):
126
- 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])
127
135
  else:
128
136
  path_bins = path
129
137
  all_bin_sizes = _get_bins(path_bins)
@@ -219,19 +227,27 @@ def visium_hd(
219
227
  axes=("x", "y"),
220
228
  )
221
229
  # parse shapes
230
+ shapes_name = dataset_id + "_" + bin_size_str
231
+ radius = scalefactors[VisiumHDKeys.SCALEFACTORS_SPOT_DIAMETER_FULLRES] / 2.0
232
+ transformations = {
233
+ "global": transform_original,
234
+ "downscaled_hires": transform_hires,
235
+ "downscaled_lowres": transform_lowres,
236
+ }
222
237
  circles = ShapesModel.parse(
223
238
  adata.obsm["spatial"],
224
239
  geometry=0,
225
- radius=scalefactors[VisiumHDKeys.SCALEFACTORS_SPOT_DIAMETER_FULLRES] / 2.0,
240
+ radius=radius,
226
241
  index=adata.obs[VisiumHDKeys.INSTANCE_KEY].copy(),
227
- transformations={
228
- "global": transform_original,
229
- "downscaled_hires": transform_hires,
230
- "downscaled_lowres": transform_lowres,
231
- },
242
+ transformations=transformations,
232
243
  )
233
- shapes_name = dataset_id + "_" + bin_size_str
234
- shapes[shapes_name] = circles
244
+ if not bins_as_squares:
245
+ shapes[shapes_name] = circles
246
+ else:
247
+ squares_series = circles.buffer(radius, cap_style=3)
248
+ shapes[shapes_name] = ShapesModel.parse(
249
+ GeoDataFrame(geometry=squares_series), transformations=transformations
250
+ )
235
251
 
236
252
  # parse table
237
253
  adata.obs[VisiumHDKeys.REGION_KEY] = shapes_name
@@ -253,15 +269,26 @@ def visium_hd(
253
269
  fullres_image_filenames = [
254
270
  f for f in os.listdir(path_fullres) if os.path.isfile(os.path.join(path_fullres, f))
255
271
  ]
256
- if len(fullres_image_filenames) > 1:
257
- warnings.warn(
258
- f"Multiple files found in {path_fullres}, using the first one: {fullres_image_filenames[0]}. Please"
259
- " specify the path to the full resolution image manually using the `fullres_image_file` argument.",
260
- UserWarning,
261
- stacklevel=2,
262
- )
263
- fullres_image_filename = fullres_image_filenames[0]
264
- 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
265
292
 
266
293
  if fullres_image_file is not None:
267
294
  load_image(
@@ -271,8 +298,15 @@ def visium_hd(
271
298
  )
272
299
 
273
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
+ )
274
308
  load_image(
275
- path=path / VisiumHDKeys.IMAGE_HIRES_FILE,
309
+ path=hires_image_path[0],
276
310
  suffix="_hires_image",
277
311
  )
278
312
  set_transformation(
@@ -282,8 +316,15 @@ def visium_hd(
282
316
  )
283
317
 
284
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
+ )
285
326
  load_image(
286
- path=path / VisiumHDKeys.IMAGE_LOWRES_FILE,
327
+ path=lowres_image_path[0],
287
328
  suffix="_lowres_image",
288
329
  )
289
330
  set_transformation(
@@ -293,9 +334,16 @@ def visium_hd(
293
334
  )
294
335
 
295
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
+ )
296
344
  if load_all_images:
297
345
  load_image(
298
- path=path / VisiumHDKeys.IMAGE_CYTASSIST,
346
+ path=cytassist_path[0],
299
347
  suffix="_cytassist_image",
300
348
  )
301
349
  image = images[dataset_id + "_cytassist_image"]
@@ -339,7 +387,7 @@ def _load_image(
339
387
  # dask_image doesn't recognize .btf automatically and imageio v3 throws error due to pixel limit -> use imageio v2
340
388
  data = imread2(path, **imread_kwargs).squeeze().transpose(2, 0, 1)
341
389
  image = DataArray(data, dims=("c", "y", "x"))
342
- 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)
343
391
  images[dataset_id + suffix] = parsed
344
392
  else:
345
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
@@ -56,7 +56,7 @@ def xenium(
56
56
  *,
57
57
  cells_boundaries: bool = True,
58
58
  nucleus_boundaries: bool = True,
59
- cells_as_circles: bool = True,
59
+ cells_as_circles: bool | None = None,
60
60
  cells_labels: bool = True,
61
61
  nucleus_labels: bool = True,
62
62
  transcripts: bool = True,
@@ -96,10 +96,8 @@ def xenium(
96
96
  nucleus_boundaries
97
97
  Whether to read nucleus boundaries (polygons).
98
98
  cells_as_circles
99
- Whether to read cells also as circles. Useful for performant visualization. The radii of the nuclei,
100
- not the ones of cells, will be used; using the radii of cells would make the visualization too cluttered
101
- (the cell boundaries are computed as a maximum expansion of the nuclei location and therefore the
102
- corresponding circles would show considerable overlap).
99
+ Whether to read cells also as circles (the center and the radius of each circle is computed from the
100
+ corresponding labels cell).
103
101
  cells_labels
104
102
  Whether to read cell labels (raster). The polygonal version of the cell labels are simplified
105
103
  for visualization purposes, and using the raster version is recommended for analysis.
@@ -113,7 +111,9 @@ def xenium(
113
111
  morphology_focus
114
112
  Whether to read the morphology focus image.
115
113
  aligned_images
116
- 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.
117
117
  cells_table
118
118
  Whether to read the cell annotations in the `AnnData` table.
119
119
  n_jobs
@@ -128,7 +128,34 @@ def xenium(
128
128
  Returns
129
129
  -------
130
130
  :class:`spatialdata.SpatialData`
131
+
132
+ Notes
133
+ -----
134
+ Old versions. Until spatialdata-io v0.1.3post0: previously, `cells_as_circles` was `True` by default; the table was associated to the
135
+ circles when `cells_as_circles` was `True`, and the table was associated to the polygons when `cells_as_circles`
136
+ was `False`; the radii of the circles were computed form the nuclei instead of the cells.
137
+
138
+ Performance. You can improve visualization performance (at the cost of accuracy) by setting `cells_as_circles` to `True`.
139
+
140
+ Examples
141
+ --------
142
+ This code shows how to change the annotation target of the table from the cell circles to the cell labels.
143
+ >>> from spatialdata_io import xenium
144
+ >>> sdata = xenium("path/to/raw/data", cells_as_circles=True)
145
+ >>> sdata["table"].obs["region"] = "cell_labels"
146
+ >>> sdata.set_table_annotates_spatialelement(
147
+ ... table_name="table", region="cell_labels", region_key="region", instance_key="cell_labels"
148
+ ... )
149
+ >>> sdata.write("path/to/data.zarr")
131
150
  """
151
+ if cells_as_circles is None:
152
+ cells_as_circles = True
153
+ warnings.warn(
154
+ "The default value of `cells_as_circles` will change to `False` in the next release. "
155
+ "Please pass `True` explicitly to maintain the current behavior.",
156
+ DeprecationWarning,
157
+ stacklevel=3,
158
+ )
132
159
  image_models_kwargs, labels_models_kwargs = _initialize_raster_models_kwargs(
133
160
  image_models_kwargs, labels_models_kwargs
134
161
  )
@@ -138,7 +165,7 @@ def xenium(
138
165
  # to trigger the warning if the version cannot be parsed
139
166
  version = _parse_version_of_xenium_analyzer(specs, hide_warning=False)
140
167
 
141
- specs["region"] = "cell_circles" if cells_as_circles else "cell_boundaries"
168
+ specs["region"] = "cell_circles" if cells_as_circles else "cell_labels"
142
169
 
143
170
  # the table is required in some cases
144
171
  if not cells_table:
@@ -210,14 +237,16 @@ def xenium(
210
237
  if cell_labels_indices_mapping is not None and table is not None:
211
238
  if not pd.DataFrame.equals(cell_labels_indices_mapping["cell_id"], table.obs[str(XeniumKeys.CELL_ID)]):
212
239
  warnings.warn(
213
- "The cell_id column in the cell_labels_table does not match the cell_id column derived from the cell "
214
- "labels data. This could be due to trying to read a new version that is not supported yet. Please "
215
- "report this issue.",
240
+ "The cell_id column in the cell_labels_table does not match the cell_id column derived from the "
241
+ "cell labels data. This could be due to trying to read a new version that is not supported yet. "
242
+ "Please report this issue.",
216
243
  UserWarning,
217
244
  stacklevel=2,
218
245
  )
219
246
  else:
220
247
  table.obs["cell_labels"] = cell_labels_indices_mapping["label_index"]
248
+ if not cells_as_circles:
249
+ table.uns[TableModel.ATTRS_KEY][TableModel.INSTANCE_KEY] = "cell_labels"
221
250
 
222
251
  if nucleus_boundaries:
223
252
  polygons["nucleus_boundaries"] = _get_polygons(
@@ -486,6 +515,7 @@ def _get_points(path: Path, specs: dict[str, Any]) -> Table:
486
515
  feature_key=XeniumKeys.FEATURE_NAME,
487
516
  instance_key=XeniumKeys.CELL_ID,
488
517
  transformations={"global": transform},
518
+ sort=True,
489
519
  )
490
520
  return points
491
521
 
@@ -506,7 +536,7 @@ def _get_tables_and_circles(
506
536
  table = TableModel.parse(adata, region=specs["region"], region_key="region", instance_key=str(XeniumKeys.CELL_ID))
507
537
  if cells_as_circles:
508
538
  transform = Scale([1.0 / specs["pixel_size"], 1.0 / specs["pixel_size"]], axes=("x", "y"))
509
- radii = np.sqrt(adata.obs[XeniumKeys.CELL_NUCLEUS_AREA].to_numpy() / np.pi)
539
+ radii = np.sqrt(adata.obs[XeniumKeys.CELL_AREA].to_numpy() / np.pi)
510
540
  circles = ShapesModel.parse(
511
541
  circ,
512
542
  geometry=0,
@@ -523,7 +553,7 @@ def _get_images(
523
553
  file: str,
524
554
  imread_kwargs: Mapping[str, Any] = MappingProxyType({}),
525
555
  image_models_kwargs: Mapping[str, Any] = MappingProxyType({}),
526
- ) -> SpatialImage | MultiscaleSpatialImage:
556
+ ) -> DataArray | DataTree:
527
557
  image = imread(path / file, **imread_kwargs)
528
558
  if "c_coords" in image_models_kwargs and "dummy" in image_models_kwargs["c_coords"]:
529
559
  # Napari currently interprets 4 channel images as RGB; a series of PRs to fix this is almost ready but they will
@@ -532,7 +562,7 @@ def _get_images(
532
562
  # let's add a dummy channel as a temporary workaround.
533
563
  image = da.concatenate([image, da.zeros_like(image[0:1])], axis=0)
534
564
  return Image2DModel.parse(
535
- 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
536
566
  )
537
567
 
538
568
 
@@ -540,7 +570,7 @@ def _add_aligned_images(
540
570
  path: Path,
541
571
  imread_kwargs: Mapping[str, Any] = MappingProxyType({}),
542
572
  image_models_kwargs: Mapping[str, Any] = MappingProxyType({}),
543
- ) -> dict[str, MultiscaleSpatialImage]:
573
+ ) -> dict[str, DataTree]:
544
574
  """Discover and parse aligned images."""
545
575
  images = {}
546
576
  ome_tif_files = list(path.glob("*.ome.tif"))
@@ -571,7 +601,8 @@ def xenium_aligned_image(
571
601
  alignment_file: str | Path | None,
572
602
  imread_kwargs: Mapping[str, Any] = MappingProxyType({}),
573
603
  image_models_kwargs: Mapping[str, Any] = MappingProxyType({}),
574
- ) -> MultiscaleSpatialImage:
604
+ dims: tuple[str, ...] | None = None,
605
+ ) -> DataTree:
575
606
  """
576
607
  Read an image aligned to a Xenium dataset, with an optional alignment file.
577
608
 
@@ -583,6 +614,12 @@ def xenium_aligned_image(
583
614
  Path to the alignment file, if not passed it is assumed that the image is aligned.
584
615
  image_models_kwargs
585
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).
586
623
 
587
624
  Returns
588
625
  -------
@@ -600,19 +637,28 @@ def xenium_aligned_image(
600
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
601
638
  # not a problem because the transformation is constructed to be consistent, but if is the case, the data orientation
602
639
  # would be transposed compared to the original image, not ideal.
603
- if len(image.shape) == 4:
604
- assert image.shape[0] == 1
605
- assert image.shape[-1] == 3
606
- image = image.squeeze(0)
607
- 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")
608
654
  else:
609
- assert len(image.shape) == 3
610
- assert image.shape[0] in [3, 4]
611
- if image.shape[0] == 4:
612
- # as explained before in _get_images(), we need to add a dummy channel until we support 4-channel images as
613
- # non-RGBA images in napari
614
- image = da.concatenate([image, da.zeros_like(image[0:1])], axis=0)
615
- 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)
616
662
 
617
663
  if alignment_file is None:
618
664
  transformation = Identity()
@@ -735,3 +781,6 @@ def prefix_suffix_uint32_from_cell_id_str(cell_id_str: ArrayLike) -> tuple[Array
735
781
  cell_id_prefix = [int(x, 16) for x in cell_id_prefix_hex]
736
782
 
737
783
  return np.array(cell_id_prefix, dtype=np.uint32), np.array(dataset_suffix_int)
784
+
785
+
786
+ ##
@@ -51,13 +51,13 @@ def test_roundtrip_with_data_limits() -> None:
51
51
  "dataset,expected",
52
52
  [
53
53
  ("Xenium_V1_human_Breast_2fov_outs", "{'y': (0, 3529), 'x': (0, 5792), 'z': (10, 25)}"),
54
- ("Xenium_V1_human_Lung_2fov_outs", "{'y': (-1, 3556), 'x': (0, 5794), 'z': (7, 32)}"),
54
+ ("Xenium_V1_human_Lung_2fov_outs", "{'y': (0, 3553), 'x': (0, 5793), 'z': (7, 32)}"),
55
55
  ],
56
56
  )
57
57
  def test_example_data(dataset: str, expected: str) -> None:
58
58
  f = Path("./data") / dataset
59
59
  assert f.is_dir()
60
- sdata = xenium(f)
60
+ sdata = xenium(f, cells_as_circles=False)
61
61
  from spatialdata import get_extent
62
62
 
63
63
  extent = get_extent(sdata, exact=False)