spatialdata-io 0.1.1__tar.gz → 0.1.2__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 (60) hide show
  1. spatialdata_io-0.1.2/.github/workflows/release.yaml +31 -0
  2. spatialdata_io-0.1.1/.github/workflows/test_and_deploy.yaml → spatialdata_io-0.1.2/.github/workflows/test.yaml +1 -27
  3. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/.pre-commit-config.yaml +1 -1
  4. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/CHANGELOG.md +12 -1
  5. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/PKG-INFO +10 -8
  6. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/README.md +8 -7
  7. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/_version.py +2 -2
  8. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/docs/api.md +6 -5
  9. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/docs/conf.py +2 -0
  10. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/pyproject.toml +1 -0
  11. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/src/spatialdata_io/__init__.py +2 -0
  12. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/src/spatialdata_io/_constants/_constants.py +57 -0
  13. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/src/spatialdata_io/converters/legacy_anndata.py +3 -3
  14. spatialdata_io-0.1.2/src/spatialdata_io/readers/visium_hd.py +370 -0
  15. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/src/spatialdata_io/readers/xenium.py +24 -20
  16. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/.bumpversion.cfg +0 -0
  17. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/.codecov.yaml +0 -0
  18. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/.editorconfig +0 -0
  19. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/.flake8 +0 -0
  20. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/.github/workflows/build.yaml +0 -0
  21. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/.gitignore +0 -0
  22. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/.mypy.ini +0 -0
  23. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/.readthedocs.yaml +0 -0
  24. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/LICENSE +0 -0
  25. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/docs/Makefile +0 -0
  26. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/docs/_static/.gitkeep +0 -0
  27. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/docs/_templates/.gitkeep +0 -0
  28. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/docs/_templates/autosummary/class.rst +0 -0
  29. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/docs/changelog.md +0 -0
  30. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/docs/contributing.md +0 -0
  31. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/docs/extensions/.gitkeep +0 -0
  32. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/docs/extensions/typed_returns.py +0 -0
  33. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/docs/index.md +0 -0
  34. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/docs/make.bat +0 -0
  35. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/docs/references.bib +0 -0
  36. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/docs/references.md +0 -0
  37. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/docs/template_usage.md +0 -0
  38. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/src/spatialdata_io/_constants/__init__.py +0 -0
  39. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/src/spatialdata_io/_constants/_enum.py +0 -0
  40. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/src/spatialdata_io/_docs.py +0 -0
  41. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/src/spatialdata_io/_utils.py +0 -0
  42. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/src/spatialdata_io/converters/__init__.py +0 -0
  43. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/src/spatialdata_io/experimental/__init__.py +0 -0
  44. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/src/spatialdata_io/readers/__init__.py +0 -0
  45. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/src/spatialdata_io/readers/_utils/__init__.py +0 -0
  46. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/src/spatialdata_io/readers/_utils/_read_10x_h5.py +0 -0
  47. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/src/spatialdata_io/readers/_utils/_utils.py +0 -0
  48. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/src/spatialdata_io/readers/codex.py +0 -0
  49. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/src/spatialdata_io/readers/cosmx.py +0 -0
  50. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/src/spatialdata_io/readers/curio.py +0 -0
  51. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/src/spatialdata_io/readers/dbit.py +0 -0
  52. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/src/spatialdata_io/readers/mcmicro.py +0 -0
  53. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/src/spatialdata_io/readers/merscope.py +0 -0
  54. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/src/spatialdata_io/readers/steinbock.py +0 -0
  55. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/src/spatialdata_io/readers/visium.py +0 -0
  56. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/tests/__init__.py +0 -0
  57. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/tests/converters/__init__.py +0 -0
  58. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/tests/converters/test_legacy_anndata.py +0 -0
  59. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/tests/test_basic.py +0 -0
  60. {spatialdata_io-0.1.1 → spatialdata_io-0.1.2}/tests/test_xenium.py +0 -0
@@ -0,0 +1,31 @@
1
+ name: Release
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ release:
9
+ # requires that you have put your twine API key in your
10
+ # github secrets (see readme for details)
11
+ runs-on: ubuntu-latest
12
+ if: contains(github.ref, 'tags')
13
+ steps:
14
+ - name: Checkout code
15
+ uses: actions/checkout@v3
16
+
17
+ - name: Set up Python 3.10
18
+ uses: actions/setup-python@v4
19
+ with:
20
+ python-version: "3.10"
21
+
22
+ - name: Install hatch
23
+ run: pip install hatch
24
+
25
+ - name: Build project for distribution
26
+ run: hatch build
27
+
28
+ - name: Publish a Python distribution to PyPI
29
+ uses: pypa/gh-action-pypi-publish@release/v1
30
+ with:
31
+ password: ${{ secrets.PYPI_API_TOKEN }}
@@ -28,7 +28,7 @@ jobs:
28
28
  steps:
29
29
  - uses: actions/checkout@v2
30
30
  - name: Set up Python ${{ matrix.python }}
31
- uses: actions/setup-python@v2
31
+ uses: actions/setup-python@v4
32
32
  with:
33
33
  python-version: ${{ matrix.python }}
34
34
 
@@ -62,29 +62,3 @@ jobs:
62
62
  with:
63
63
  name: coverage
64
64
  verbose: true
65
- deploy:
66
- # this will run when you have tagged a commit, starting with "v*"
67
- # and requires that you have put your twine API key in your
68
- # github secrets (see readme for details)
69
- needs: [test]
70
- runs-on: ubuntu-latest
71
- if: contains(github.ref, 'tags')
72
- steps:
73
- - name: Checkout code
74
- uses: actions/checkout@v3
75
-
76
- - name: Set up Python 3.10
77
- uses: actions/setup-python@v4
78
- with:
79
- python-version: "3.10"
80
-
81
- - name: Install hatch
82
- run: pip install hatch
83
-
84
- - name: Build project for distribution
85
- run: hatch build
86
-
87
- - name: Publish a Python distribution to PyPI
88
- uses: pypa/gh-action-pypi-publish@release/v1
89
- with:
90
- password: ${{ secrets.PYPI_API_TOKEN }}
@@ -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.15.1
73
+ rev: v3.15.2
74
74
  hooks:
75
75
  - id: pyupgrade
76
76
  args: [--py3-plus, --py39-plus, --keep-runtime-typing]
@@ -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.0] - 2024-03-24
11
+ ## [0.1.2] - 2024-03-30
12
+
13
+ ### Added
14
+
15
+ - (Visium HD) added reader, coauthored by @LLehner
16
+
17
+ ### Fixed
18
+
19
+ - (Xenium) reader for 1.0.1 (paper data) and unknown versions
20
+ - (Xenium) fix in reading "minimalistic" Xenium datasets #132
21
+
22
+ ## [0.1.1] - 2024-03-24
12
23
 
13
24
  ### Added
14
25
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: spatialdata-io
3
- Version: 0.1.1
3
+ Version: 0.1.2
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
@@ -46,6 +46,7 @@ Requires-Dist: joblib
46
46
  Requires-Dist: numpy
47
47
  Requires-Dist: pyarrow
48
48
  Requires-Dist: readfcs
49
+ Requires-Dist: scanpy
49
50
  Requires-Dist: scikit-image
50
51
  Requires-Dist: spatialdata
51
52
  Provides-Extra: dev
@@ -75,21 +76,22 @@ Description-Content-Type: text/markdown
75
76
  [![Documentation][badge-docs]][link-docs]
76
77
  [![DOI](https://zenodo.org/badge/544045123.svg)](https://zenodo.org/badge/latestdoi/544045123)
77
78
 
78
- [badge-tests]: https://github.com/scverse/spatialdata-io/actions/workflows/test_and_deploy.yaml/badge.svg
79
- [link-tests]: https://github.com/scverse/spatialdata-io/actions/workflows/test_and_deploy.yaml
79
+ [badge-tests]: https://github.com/scverse/spatialdata-io/actions/workflows/test.yaml/badge.svg
80
+ [link-tests]: https://github.com/scverse/spatialdata-io/actions/workflows/test.yaml
80
81
  [badge-docs]: https://img.shields.io/readthedocs/spatialdata-io
81
82
 
82
83
  This package contains reader functions to load common spatial omics formats into SpatialData. Currently, we provide support for:
83
84
 
84
- - NanoString CosMx
85
- - MCMICRO (output data)
86
- - Steinbock (output data)
87
85
  - 10x Genomics Visium
86
+ - 10x Genomics Visium HD
88
87
  - 10x Genomics Xenium
89
- - Curio Seeker
90
88
  - Akoya PhenoCycler (formerly CODEX)
91
- - Vizgen MERSCOPE (MERFISH)
89
+ - Curio Seeker
92
90
  - DBiT-seq
91
+ - MCMICRO (output data)
92
+ - NanoString CosMx
93
+ - Steinbock (output data)
94
+ - Vizgen MERSCOPE (MERFISH)
93
95
 
94
96
  ## Getting started
95
97
 
@@ -6,21 +6,22 @@
6
6
  [![Documentation][badge-docs]][link-docs]
7
7
  [![DOI](https://zenodo.org/badge/544045123.svg)](https://zenodo.org/badge/latestdoi/544045123)
8
8
 
9
- [badge-tests]: https://github.com/scverse/spatialdata-io/actions/workflows/test_and_deploy.yaml/badge.svg
10
- [link-tests]: https://github.com/scverse/spatialdata-io/actions/workflows/test_and_deploy.yaml
9
+ [badge-tests]: https://github.com/scverse/spatialdata-io/actions/workflows/test.yaml/badge.svg
10
+ [link-tests]: https://github.com/scverse/spatialdata-io/actions/workflows/test.yaml
11
11
  [badge-docs]: https://img.shields.io/readthedocs/spatialdata-io
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
- - NanoString CosMx
16
- - MCMICRO (output data)
17
- - Steinbock (output data)
18
15
  - 10x Genomics Visium
16
+ - 10x Genomics Visium HD
19
17
  - 10x Genomics Xenium
20
- - Curio Seeker
21
18
  - Akoya PhenoCycler (formerly CODEX)
22
- - Vizgen MERSCOPE (MERFISH)
19
+ - Curio Seeker
23
20
  - DBiT-seq
21
+ - MCMICRO (output data)
22
+ - NanoString CosMx
23
+ - Steinbock (output data)
24
+ - Vizgen MERSCOPE (MERFISH)
24
25
 
25
26
  ## Getting started
26
27
 
@@ -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.1'
16
- __version_tuple__ = version_tuple = (0, 1, 1)
15
+ __version__ = version = '0.1.2'
16
+ __version_tuple__ = version_tuple = (0, 1, 2)
@@ -15,14 +15,15 @@ I/O for the `spatialdata` project.
15
15
  :toctree: generated
16
16
 
17
17
  codex
18
- curio
19
18
  cosmx
19
+ curio
20
+ dbit
21
+ mcmicro
22
+ merscope
23
+ steinbock
20
24
  visium
25
+ visium_hd
21
26
  xenium
22
- steinbock
23
- merscope
24
- mcmicro
25
- dbit
26
27
  ```
27
28
 
28
29
  ### Conversion functions
@@ -98,6 +98,7 @@ intersphinx_mapping = {
98
98
  "xarray": ("https://docs.xarray.dev/en/stable/", None),
99
99
  "datatree": ("https://datatree.readthedocs.io/en/latest/", None),
100
100
  "shapely": ("https://shapely.readthedocs.io/en/stable/", None),
101
+ "imageio": ("https://imageio.readthedocs.io/en/stable/", None),
101
102
  }
102
103
 
103
104
  # List of patterns, relative to source directory, that match files and
@@ -125,6 +126,7 @@ nitpick_ignore = [
125
126
  ("py:class", "Path"),
126
127
  ("py:class", "AnnData"),
127
128
  ("py:class", "SpatialData"),
129
+ ("py:func", "imageio.imread"), # maybe this can be fixed
128
130
  ]
129
131
 
130
132
 
@@ -24,6 +24,7 @@ urls.Home-page = "https://github.com/scverse/spatialdata-io"
24
24
  dependencies = [
25
25
  "anndata",
26
26
  "numpy",
27
+ "scanpy",
27
28
  "spatialdata",
28
29
  "scikit-image",
29
30
  "h5py",
@@ -8,6 +8,7 @@ from spatialdata_io.readers.mcmicro import mcmicro
8
8
  from spatialdata_io.readers.merscope import merscope
9
9
  from spatialdata_io.readers.steinbock import steinbock
10
10
  from spatialdata_io.readers.visium import visium
11
+ from spatialdata_io.readers.visium_hd import visium_hd
11
12
  from spatialdata_io.readers.xenium import (
12
13
  xenium,
13
14
  xenium_aligned_image,
@@ -26,6 +27,7 @@ __all__ = [
26
27
  "xenium_aligned_image",
27
28
  "xenium_explorer_selection",
28
29
  "dbit",
30
+ "visium_hd",
29
31
  ]
30
32
 
31
33
  __version__ = version("spatialdata-io")
@@ -227,3 +227,60 @@ class DbitKeys(ModeEnum):
227
227
  BARCODE_POSITION = "barcode_list"
228
228
  # image
229
229
  IMAGE_LOWRES_FILE = "tissue_lowres_image.png"
230
+
231
+
232
+ @unique
233
+ class VisiumHDKeys(ModeEnum):
234
+ """Keys for *10X Genomics Visium hd* formatted dataset."""
235
+
236
+ # directories
237
+ SPATIAL = "spatial"
238
+ DEFAULT_BIN = "square_008um"
239
+ BIN_PREFIX = "square_"
240
+ MICROSCOPE_IMAGE = "microscope_image"
241
+
242
+ # counts and locations files
243
+ FILTERED_COUNTS_FILE = "filtered_feature_bc_matrix.h5"
244
+ RAW_COUNTS_FILE = "raw_feature_bc_matrix.h5"
245
+ TISSUE_POSITIONS_FILE = "tissue_positions.parquet"
246
+
247
+ # images
248
+ IMAGE_HIRES_FILE = "spatial/tissue_hires_image.png"
249
+ IMAGE_LOWRES_FILE = "spatial/tissue_lowres_image.png"
250
+ IMAGE_CYTASSIST = "spatial/cytassist_image.tiff"
251
+
252
+ # scalefactors
253
+ SCALEFACTORS_FILE = "scalefactors_json.json"
254
+
255
+ # scalefactors keys
256
+ SCALEFACTORS_HIRES = "tissue_hires_scalef"
257
+ SCALEFACTORS_LOWRES = "tissue_lowres_scalef"
258
+ SCALEFACTORS_SPOT_DIAMETER_FULLRES = "spot_diameter_fullres"
259
+ SCALEFACTORS_BIN_SIZE_UM = "bin_size_um"
260
+ SCALEFACTORS_MICRONS_PER_PIXEL = "microns_per_pixel"
261
+
262
+ # locations keys
263
+ LOCATIONS_X = "pxl_col_in_fullres"
264
+ LOCATIONS_Y = "pxl_row_in_fullres"
265
+ BARCODE = "barcode"
266
+ IN_TISSUE = "in_tissue"
267
+ ARRAY_ROW = "array_row"
268
+ ARRAY_COL = "array_col"
269
+
270
+ # table keys
271
+ REGION_KEY = "region"
272
+ INSTANCE_KEY = "location_id"
273
+
274
+ # feature slice file (it contains transformation matrices in the metadata)
275
+ FEATURE_SLICE_FILE = "feature_slice.h5"
276
+
277
+ # METADATA_KEYS
278
+ METADATA_JSON = "metadata_json"
279
+ HD_LAYOUT_JSON = "hd_layout_json"
280
+ TRANSFORM = "transform"
281
+ TRANSFORM_MATRICES = "transform_matrices"
282
+ CYTASSIST_COLROW_TO_SPOT_COLROW = ("cytassist_colrow_to_spot_colrow",)
283
+ SPOT_COLROW_TO_CYTASSIST_COLROW = ("spot_colrow_to_cytassist_colrow",)
284
+ MICROSCOPE_COLROW_TO_SPOT_COLROW = ("microscope_colrow_to_spot_colrow",)
285
+ SPOT_COLROW_TO_MICROSCOPE_COLROW = ("spot_colrow_to_microscope_colrow",)
286
+ FILE_FORMAT = "file_format"
@@ -8,7 +8,7 @@ from spatialdata import (
8
8
  SpatialData,
9
9
  get_centroids,
10
10
  get_extent,
11
- join_sdata_spatialelement_table,
11
+ join_spatialelement_table,
12
12
  to_circles,
13
13
  )
14
14
  from spatialdata._core.operations._utils import transform_to_data_extent
@@ -149,8 +149,8 @@ def to_legacy_anndata(
149
149
  shapes = to_circles(element)
150
150
  circles_sdata = SpatialData(tables={table_name: table}, shapes={region_name: shapes})
151
151
 
152
- joined_elements, new_table = join_sdata_spatialelement_table(
153
- sdata=circles_sdata, spatial_element_name=region_name, table_name=table_name, how="inner", match_rows="left"
152
+ joined_elements, new_table = join_spatialelement_table(
153
+ sdata=circles_sdata, spatial_element_names=region_name, table_name=table_name, how="inner", match_rows="left"
154
154
  )
155
155
 
156
156
  # the table after the filtering must not be empty
@@ -0,0 +1,370 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import re
6
+ import warnings
7
+ from collections.abc import Mapping
8
+ from pathlib import Path
9
+ from types import MappingProxyType
10
+ from typing import Any
11
+
12
+ import h5py
13
+ import numpy as np
14
+ import pandas as pd
15
+ import scanpy as sc
16
+ from dask_image.imread import imread
17
+ from imageio import imread as imread2
18
+ from multiscale_spatial_image import MultiscaleSpatialImage
19
+ from spatial_image import SpatialImage
20
+ from spatialdata import SpatialData
21
+ from spatialdata.models import Image2DModel, ShapesModel, TableModel
22
+ from spatialdata.transformations import (
23
+ Affine,
24
+ Identity,
25
+ Scale,
26
+ Sequence,
27
+ set_transformation,
28
+ )
29
+ from xarray import DataArray
30
+
31
+ from spatialdata_io._constants._constants import VisiumHDKeys
32
+ from spatialdata_io._docs import inject_docs
33
+
34
+
35
+ @inject_docs(vx=VisiumHDKeys)
36
+ def visium_hd(
37
+ path: str | Path,
38
+ dataset_id: str | None = None,
39
+ filtered_counts_file: bool = True,
40
+ bin_size: int | list[int] | None = None,
41
+ fullres_image_file: str | Path | None = None,
42
+ load_all_images: bool = False,
43
+ imread_kwargs: Mapping[str, Any] = MappingProxyType({}),
44
+ image_models_kwargs: Mapping[str, Any] = MappingProxyType({}),
45
+ anndata_kwargs: Mapping[str, Any] = MappingProxyType({}),
46
+ ) -> SpatialData:
47
+ """
48
+ Read *10x Genomics* Visium HD formatted dataset.
49
+
50
+ .. seealso::
51
+
52
+ - `Space Ranger output <https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/output/overview>`_.
53
+
54
+ Parameters
55
+ ----------
56
+ path
57
+ Path to directory containing the *10x Genomics* Visium HD output.
58
+ dataset_id
59
+ Unique identifier of the dataset. If `None`, it tries to infer it from the file name of the feature slice file.
60
+ filtered_counts_file
61
+ It sets the value of `counts_file` to ``{vx.FILTERED_COUNTS_FILE!r}`` (when `True`) or to
62
+ ``{vx.RAW_COUNTS_FILE!r}`` (when `False`).
63
+ bin_size
64
+ When specified, load the data of a specific bin size, or a list of bin sizes. By default, it loads all the
65
+ available bin sizes.
66
+ fullres_image_file
67
+ Path to the full-resolution image. By default the image is searched in the ``{vx.MICROSCOPE_IMAGE!r}``
68
+ directory.
69
+ load_all_images
70
+ If `False`, load only the full resolution, high resolution and low resolution images. If `True`, also the
71
+ following images: ``{vx.IMAGE_CYTASSIST!r}``.
72
+ imread_kwargs
73
+ Keyword arguments for :func:`imageio.imread`.
74
+ image_models_kwargs
75
+ Keyword arguments for :class:`spatialdata.models.Image2DModel`.
76
+ anndata_kwargs
77
+ Keyword arguments for :func:`anndata.read_h5ad`.
78
+
79
+ Returns
80
+ -------
81
+ SpatialData object for the Visium HD data.
82
+ """
83
+ path = Path(path)
84
+ tables = {}
85
+ shapes = {}
86
+ images: dict[str, Any] = {}
87
+
88
+ if dataset_id is None:
89
+ dataset_id = _infer_dataset_id(path)
90
+ filename_prefix = f"{dataset_id}_"
91
+ else:
92
+ filename_prefix = ""
93
+
94
+ def load_image(path: Path, suffix: str, scale_factors: list[int] | None = None) -> None:
95
+ _load_image(
96
+ path=path,
97
+ images=images,
98
+ suffix=suffix,
99
+ dataset_id=dataset_id,
100
+ imread_kwargs=imread_kwargs,
101
+ image_models_kwargs=image_models_kwargs,
102
+ scale_factors=scale_factors,
103
+ )
104
+
105
+ metadata, hd_layout = _parse_metadata(path, filename_prefix)
106
+ transform_matrices = _get_transform_matrices(metadata, hd_layout)
107
+ file_format = hd_layout[VisiumHDKeys.FILE_FORMAT]
108
+ if file_format != "1.0":
109
+ warnings.warn(
110
+ f"File format {file_format} is not supported. A more recent file format may be supported in a newer version"
111
+ f"of the spatialdata-io package.",
112
+ UserWarning,
113
+ stacklevel=2,
114
+ )
115
+
116
+ path_bins = path
117
+ all_bin_sizes = sorted(
118
+ [
119
+ bin_size
120
+ for bin_size in os.listdir(path_bins)
121
+ if os.path.isdir(os.path.join(path_bins, bin_size)) and bin_size.startswith(VisiumHDKeys.BIN_PREFIX)
122
+ ]
123
+ )
124
+ if bin_size is None:
125
+ bin_sizes = all_bin_sizes
126
+ elif isinstance(bin_size, int) or isinstance(bin_size, list) and len(bin_size) == 0:
127
+ if f"square_{bin_size:03}um" not in all_bin_sizes:
128
+ warnings.warn(
129
+ f"Requested bin size {bin_size} not found (available {all_bin_sizes}). Using all available bins.",
130
+ UserWarning,
131
+ stacklevel=2,
132
+ )
133
+ bin_sizes = all_bin_sizes
134
+ else:
135
+ bin_sizes = [f"square_{bin_size:03}um"]
136
+
137
+ # iterate over the given bins and load the data
138
+ for bin_size_str in bin_sizes:
139
+ path_bin = path_bins / bin_size_str
140
+ counts_file = VisiumHDKeys.FILTERED_COUNTS_FILE if filtered_counts_file else VisiumHDKeys.RAW_COUNTS_FILE
141
+ adata = sc.read_10x_h5(
142
+ path_bin / counts_file,
143
+ gex_only=False,
144
+ **anndata_kwargs,
145
+ )
146
+
147
+ path_bin_spatial = path_bin / VisiumHDKeys.SPATIAL
148
+
149
+ with open(path_bin_spatial / VisiumHDKeys.SCALEFACTORS_FILE) as file:
150
+ scalefactors = json.load(file)
151
+
152
+ # consistency check
153
+ found_bin_size = re.search(r"\d{3}", bin_size_str)
154
+ assert found_bin_size is not None
155
+ assert float(found_bin_size.group()) == scalefactors[VisiumHDKeys.SCALEFACTORS_BIN_SIZE_UM]
156
+ assert np.isclose(
157
+ scalefactors[VisiumHDKeys.SCALEFACTORS_BIN_SIZE_UM]
158
+ / scalefactors[VisiumHDKeys.SCALEFACTORS_SPOT_DIAMETER_FULLRES],
159
+ scalefactors[VisiumHDKeys.SCALEFACTORS_MICRONS_PER_PIXEL],
160
+ )
161
+
162
+ tissue_positions_file = path_bin_spatial / VisiumHDKeys.TISSUE_POSITIONS_FILE
163
+
164
+ # read coordinates and set up adata.obs and adata.obsm
165
+ coords = pd.read_parquet(tissue_positions_file)
166
+ assert all(
167
+ coords.columns.values
168
+ == [
169
+ VisiumHDKeys.BARCODE,
170
+ VisiumHDKeys.IN_TISSUE,
171
+ VisiumHDKeys.ARRAY_ROW,
172
+ VisiumHDKeys.ARRAY_COL,
173
+ VisiumHDKeys.LOCATIONS_Y,
174
+ VisiumHDKeys.LOCATIONS_X,
175
+ ]
176
+ )
177
+ coords.set_index(VisiumHDKeys.BARCODE, inplace=True, drop=True)
178
+ coords_filtered = coords.loc[adata.obs.index]
179
+ adata.obs = pd.merge(adata.obs, coords_filtered, how="left", left_index=True, right_index=True)
180
+ # compatibility to legacy squidpy
181
+ adata.obsm["spatial"] = adata.obs[[VisiumHDKeys.LOCATIONS_X, VisiumHDKeys.LOCATIONS_Y]].values
182
+ # dropping the spatial coordinates (will be stored in shapes)
183
+ adata.obs.drop(
184
+ columns=[
185
+ VisiumHDKeys.LOCATIONS_X,
186
+ VisiumHDKeys.LOCATIONS_Y,
187
+ ],
188
+ inplace=True,
189
+ )
190
+ adata.obs[VisiumHDKeys.INSTANCE_KEY] = np.arange(len(adata))
191
+
192
+ # scaling
193
+ transform_original = Identity()
194
+ transform_lowres = Scale(
195
+ np.array(
196
+ [
197
+ scalefactors[VisiumHDKeys.SCALEFACTORS_LOWRES],
198
+ scalefactors[VisiumHDKeys.SCALEFACTORS_LOWRES],
199
+ ]
200
+ ),
201
+ axes=("x", "y"),
202
+ )
203
+ transform_hires = Scale(
204
+ np.array(
205
+ [
206
+ scalefactors[VisiumHDKeys.SCALEFACTORS_HIRES],
207
+ scalefactors[VisiumHDKeys.SCALEFACTORS_HIRES],
208
+ ]
209
+ ),
210
+ axes=("x", "y"),
211
+ )
212
+ # parse shapes
213
+ circles = ShapesModel.parse(
214
+ adata.obsm["spatial"],
215
+ geometry=0,
216
+ radius=scalefactors[VisiumHDKeys.SCALEFACTORS_SPOT_DIAMETER_FULLRES] / 2.0,
217
+ index=adata.obs[VisiumHDKeys.INSTANCE_KEY].copy(),
218
+ transformations={
219
+ "global": transform_original,
220
+ "downscaled_hires": transform_hires,
221
+ "downscaled_lowres": transform_lowres,
222
+ },
223
+ )
224
+ shapes_name = dataset_id + "_" + bin_size_str
225
+ shapes[shapes_name] = circles
226
+
227
+ # parse table
228
+ adata.obs[VisiumHDKeys.REGION_KEY] = shapes_name
229
+ adata.obs[VisiumHDKeys.REGION_KEY] = adata.obs[VisiumHDKeys.REGION_KEY].astype("category")
230
+ tables[bin_size_str] = TableModel.parse(
231
+ adata,
232
+ region=shapes_name,
233
+ region_key=str(VisiumHDKeys.REGION_KEY),
234
+ instance_key=str(VisiumHDKeys.INSTANCE_KEY),
235
+ )
236
+
237
+ # read full resolution image
238
+ if fullres_image_file is not None:
239
+ fullres_image_file = Path(fullres_image_file)
240
+ else:
241
+ path_fullres = path / VisiumHDKeys.MICROSCOPE_IMAGE
242
+ if path_fullres.exists():
243
+ fullres_image_filenames = [
244
+ f for f in os.listdir(path_fullres) if os.path.isfile(os.path.join(path_fullres, f))
245
+ ]
246
+ if len(fullres_image_filenames) > 1:
247
+ warnings.warn(
248
+ f"Multiple files found in {path_fullres}, using the first one: {fullres_image_filenames[0]}. Please"
249
+ " specify the path to the full resolution image manually using the `fullres_image_file` argument.",
250
+ UserWarning,
251
+ stacklevel=2,
252
+ )
253
+ fullres_image_filename = fullres_image_filenames[0]
254
+ fullres_image_file = path_fullres / fullres_image_filename
255
+
256
+ if fullres_image_file is not None:
257
+ load_image(
258
+ path=path / fullres_image_file,
259
+ suffix="_full_image",
260
+ scale_factors=[2, 2, 2, 2],
261
+ )
262
+
263
+ # hires image
264
+ load_image(
265
+ path=path / VisiumHDKeys.IMAGE_HIRES_FILE,
266
+ suffix="_hires_image",
267
+ )
268
+ set_transformation(
269
+ images[dataset_id + "_hires_image"],
270
+ {"downscaled_hires": Identity()},
271
+ set_all=True,
272
+ )
273
+
274
+ # lowres image
275
+ load_image(
276
+ path=path / VisiumHDKeys.IMAGE_LOWRES_FILE,
277
+ suffix="_lowres_image",
278
+ )
279
+ set_transformation(
280
+ images[dataset_id + "_lowres_image"],
281
+ {"downscaled_lowres": Identity()},
282
+ set_all=True,
283
+ )
284
+
285
+ # cytassist image
286
+ if load_all_images:
287
+ load_image(
288
+ path=path / VisiumHDKeys.IMAGE_CYTASSIST,
289
+ suffix="_cytassist_image",
290
+ )
291
+ image = images[dataset_id + "_cytassist_image"]
292
+ affine0 = transform_matrices["cytassist_colrow_to_spot_colrow"]
293
+ affine1 = transform_matrices["spot_colrow_to_microscope_colrow"]
294
+ set_transformation(image, Sequence([affine0, affine1]), "global")
295
+
296
+ return SpatialData(tables=tables, images=images, shapes=shapes)
297
+
298
+
299
+ def _infer_dataset_id(path: Path) -> str:
300
+ suffix = f"_{VisiumHDKeys.FEATURE_SLICE_FILE.value}"
301
+ files = [f for f in os.listdir(path) if os.path.isfile(os.path.join(path, f)) and f.endswith(suffix)]
302
+ if len(files) == 0 or len(files) > 1:
303
+ raise ValueError(
304
+ f"Cannot infer `dataset_id` from the feature slice file in {path}, please pass `dataset_id` as an argument."
305
+ )
306
+ return files[0].replace(suffix, "")
307
+
308
+
309
+ def _load_image(
310
+ path: Path,
311
+ images: dict[str, SpatialImage | MultiscaleSpatialImage],
312
+ suffix: str,
313
+ dataset_id: str,
314
+ imread_kwargs: Mapping[str, Any],
315
+ image_models_kwargs: Mapping[str, Any],
316
+ scale_factors: list[int] | None,
317
+ ) -> None:
318
+ if path.exists():
319
+ if path.suffix != ".btf":
320
+ data = imread(path, **imread_kwargs)
321
+ if len(data.shape) == 4:
322
+ # this happens for the cytassist, hires and lowres images; the umi image doesn't need processing
323
+ data = data.squeeze().transpose(2, 0, 1)
324
+ else:
325
+ if "MAX_IMAGE_PIXELS" in imread_kwargs:
326
+ from PIL import Image as ImagePIL
327
+
328
+ ImagePIL.MAX_IMAGE_PIXELS = dict(imread_kwargs).pop("MAX_IMAGE_PIXELS")
329
+ # dask_image doesn't recognize .btf automatically and imageio v3 throws error due to pixel limit -> use imageio v2
330
+ data = imread2(path, **imread_kwargs).squeeze().transpose(2, 0, 1)
331
+ image = DataArray(data, dims=("c", "y", "x"))
332
+ parsed = Image2DModel.parse(image, scale_factors=scale_factors, **image_models_kwargs)
333
+ images[dataset_id + suffix] = parsed
334
+ else:
335
+ warnings.warn(f"File {path} does not exist, skipping it.", UserWarning, stacklevel=2)
336
+ return None
337
+
338
+
339
+ def _get_affine(coefficients: list[int]) -> Affine:
340
+ matrix = np.array(coefficients).reshape(3, 3)
341
+ # the last row doesn't match with machine precision, let's check the matrix it's still close to a homogeneous
342
+ # matrix, and fix this
343
+ assert np.allclose(matrix[2], [0, 0, 1], atol=1e-2), matrix
344
+ matrix[2] = [0, 0, 1]
345
+ return Affine(matrix, input_axes=("x", "y"), output_axes=("x", "y"))
346
+
347
+
348
+ def _parse_metadata(path: Path, filename_prefix: str) -> tuple[dict[str, Any], dict[str, Any]]:
349
+ with h5py.File(path / f"{filename_prefix}{VisiumHDKeys.FEATURE_SLICE_FILE.value}", "r") as f5:
350
+ metadata = json.loads(dict(f5.attrs)[VisiumHDKeys.METADATA_JSON])
351
+ hd_layout = json.loads(metadata[VisiumHDKeys.HD_LAYOUT_JSON])
352
+ return metadata, hd_layout
353
+
354
+
355
+ def _get_transform_matrices(metadata: dict[str, Any], hd_layout: dict[str, Any]) -> dict[str, Affine]:
356
+ transform_matrices = {}
357
+
358
+ # not used
359
+ transform_matrices["hd_layout_transform"] = _get_affine(hd_layout[VisiumHDKeys.TRANSFORM])
360
+
361
+ for key in [
362
+ VisiumHDKeys.CYTASSIST_COLROW_TO_SPOT_COLROW,
363
+ VisiumHDKeys.SPOT_COLROW_TO_CYTASSIST_COLROW,
364
+ VisiumHDKeys.MICROSCOPE_COLROW_TO_SPOT_COLROW,
365
+ VisiumHDKeys.SPOT_COLROW_TO_MICROSCOPE_COLROW,
366
+ ]:
367
+ data = metadata[VisiumHDKeys.TRANSFORM_MATRICES][key]
368
+ transform_matrices[key.value] = _get_affine(data)
369
+
370
+ return transform_matrices
@@ -61,6 +61,7 @@ def xenium(
61
61
  transcripts: bool = True,
62
62
  morphology_mip: bool = True,
63
63
  morphology_focus: bool = True,
64
+ aligned_images: bool = True,
64
65
  imread_kwargs: Mapping[str, Any] = MappingProxyType({}),
65
66
  image_models_kwargs: Mapping[str, Any] = MappingProxyType({}),
66
67
  labels_models_kwargs: Mapping[str, Any] = MappingProxyType({}),
@@ -107,9 +108,11 @@ def xenium(
107
108
  transcripts
108
109
  Whether to read transcripts.
109
110
  morphology_mip
110
- Whether to read morphology mip.
111
+ Whether to read the morphology mip image (available in versions < 2.0.0).
111
112
  morphology_focus
112
- Whether to read morphology focus.
113
+ Whether to read the morphology focus image.
114
+ aligned_images
115
+ Whether to also parse, when available, additional H&E or IF aligned images.
113
116
  imread_kwargs
114
117
  Keyword arguments to pass to the image reader.
115
118
  image_models_kwargs
@@ -147,7 +150,7 @@ def xenium(
147
150
  else:
148
151
  table = return_values
149
152
 
150
- if version >= packaging.version.parse("2.0.0"):
153
+ if version is not None and version >= packaging.version.parse("2.0.0"):
151
154
  cell_summary_table = _get_cells_metadata_table_from_zarr(path, XeniumKeys.CELLS_ZARR, specs)
152
155
  if not cell_summary_table[XeniumKeys.CELL_ID].equals(table.obs[XeniumKeys.CELL_ID]):
153
156
  warnings.warn(
@@ -190,17 +193,17 @@ def xenium(
190
193
  labels_name="cell_labels",
191
194
  labels_models_kwargs=labels_models_kwargs,
192
195
  )
193
- if cell_labels_indices_mapping is not None:
194
- if not pd.DataFrame.equals(cell_labels_indices_mapping["cell_id"], table.obs[str(XeniumKeys.CELL_ID)]):
195
- warnings.warn(
196
- "The cell_id column in the cell_labels_table does not match the cell_id column derived from the cell "
197
- "labels data. This could be due to trying to read a new version that is not supported yet. Please "
198
- "report this issue.",
199
- UserWarning,
200
- stacklevel=2,
201
- )
202
- else:
203
- table.obs["cell_labels"] = cell_labels_indices_mapping["label_index"]
196
+ if cell_labels_indices_mapping is not None:
197
+ if not pd.DataFrame.equals(cell_labels_indices_mapping["cell_id"], table.obs[str(XeniumKeys.CELL_ID)]):
198
+ warnings.warn(
199
+ "The cell_id column in the cell_labels_table does not match the cell_id column derived from the cell "
200
+ "labels data. This could be due to trying to read a new version that is not supported yet. Please "
201
+ "report this issue.",
202
+ UserWarning,
203
+ stacklevel=2,
204
+ )
205
+ else:
206
+ table.obs["cell_labels"] = cell_labels_indices_mapping["label_index"]
204
207
 
205
208
  if nucleus_boundaries:
206
209
  polygons["nucleus_boundaries"] = _get_polygons(
@@ -223,7 +226,7 @@ def xenium(
223
226
  if transcripts:
224
227
  points["transcripts"] = _get_points(path, specs)
225
228
 
226
- if version < packaging.version.parse("2.0.0"):
229
+ if version is None or version < packaging.version.parse("2.0.0"):
227
230
  if morphology_mip:
228
231
  images["morphology_mip"] = _get_images(
229
232
  path,
@@ -302,9 +305,10 @@ def xenium(
302
305
  sdata = SpatialData(**elements_dict)
303
306
 
304
307
  # find and add additional aligned images
305
- aligned_images = _add_aligned_images(path, imread_kwargs, image_models_kwargs)
306
- for key, value in aligned_images.items():
307
- sdata.images[key] = value
308
+ if aligned_images:
309
+ extra_images = _add_aligned_images(path, imread_kwargs, image_models_kwargs)
310
+ for key, value in extra_images.items():
311
+ sdata.images[key] = value
308
312
 
309
313
  return sdata
310
314
 
@@ -380,7 +384,7 @@ def _get_labels_and_indices_mapping(
380
384
  if mask_index == 0:
381
385
  # nuclei currently not supported
382
386
  return labels, None
383
- if version is not None and version < packaging.version.parse("1.3.0"):
387
+ if version is None or version is not None and version < packaging.version.parse("1.3.0"):
384
388
  # supported in version 1.3.0 and not supported in version 1.0.2; conservatively, let's assume it is not
385
389
  # supported in versions < 1.3.0
386
390
  return labels, None
@@ -640,7 +644,7 @@ def _parse_version_of_xenium_analyzer(
640
644
  hide_warning: bool = True,
641
645
  ) -> packaging.version.Version | None:
642
646
  string = specs[XeniumKeys.ANALYSIS_SW_VERSION]
643
- pattern = r"^xenium-(\d+\.\d+\.\d+(\.\d+-\d+)?)"
647
+ pattern = r"^(?:x|X)enium-(\d+\.\d+\.\d+(\.\d+-\d+)?)"
644
648
 
645
649
  result = re.search(pattern, string)
646
650
  # Example
File without changes
File without changes
File without changes