spatialdata-io 0.1.7__tar.gz → 0.2.0__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.
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/.github/release.yml +11 -5
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/.github/workflows/prepare_test_data.yaml +3 -1
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/.github/workflows/test.yaml +2 -8
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/.pre-commit-config.yaml +5 -5
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/LICENSE +1 -1
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/PKG-INFO +19 -6
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/README.md +17 -4
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/_version.py +9 -4
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/src/spatialdata_io/__main__.py +4 -2
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/src/spatialdata_io/_constants/_constants.py +3 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/src/spatialdata_io/converters/generic_to_zarr.py +2 -2
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/src/spatialdata_io/readers/cosmx.py +1 -9
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/src/spatialdata_io/readers/generic.py +2 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/src/spatialdata_io/readers/visium.py +17 -7
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/src/spatialdata_io/readers/visium_hd.py +154 -33
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/src/spatialdata_io/readers/xenium.py +29 -10
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/tests/test_generic.py +1 -1
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/.bumpversion.cfg +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/.codecov.yaml +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/.editorconfig +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/.flake8 +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/.github/codecov.yml +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/.github/workflows/release.yaml +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/.gitignore +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/.mypy.ini +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/.readthedocs.yaml +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/docs/Makefile +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/docs/_static/.gitkeep +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/docs/_templates/.gitkeep +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/docs/_templates/autosummary/class.rst +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/docs/api.md +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/docs/changelog.md +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/docs/cli.md +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/docs/conf.py +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/docs/contributing.md +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/docs/extensions/.gitkeep +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/docs/extensions/typed_returns.py +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/docs/index.md +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/docs/make.bat +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/docs/references.bib +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/docs/references.md +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/pyproject.toml +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/src/spatialdata_io/__init__.py +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/src/spatialdata_io/_constants/__init__.py +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/src/spatialdata_io/_constants/_enum.py +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/src/spatialdata_io/_docs.py +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/src/spatialdata_io/_utils.py +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/src/spatialdata_io/converters/__init__.py +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/src/spatialdata_io/converters/legacy_anndata.py +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/src/spatialdata_io/experimental/__init__.py +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/src/spatialdata_io/readers/__init__.py +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/src/spatialdata_io/readers/_utils/__init__.py +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/src/spatialdata_io/readers/_utils/_read_10x_h5.py +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/src/spatialdata_io/readers/_utils/_utils.py +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/src/spatialdata_io/readers/codex.py +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/src/spatialdata_io/readers/curio.py +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/src/spatialdata_io/readers/dbit.py +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/src/spatialdata_io/readers/iss.py +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/src/spatialdata_io/readers/macsima.py +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/src/spatialdata_io/readers/mcmicro.py +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/src/spatialdata_io/readers/merscope.py +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/src/spatialdata_io/readers/seqfish.py +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/src/spatialdata_io/readers/steinbock.py +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/src/spatialdata_io/readers/stereoseq.py +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/tests/__init__.py +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/tests/_utils.py +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/tests/conftest.py +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/tests/converters/__init__.py +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/tests/converters/test_legacy_anndata.py +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/tests/test_basic.py +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/tests/test_macsima.py +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/tests/test_seqfish.py +0 -0
- {spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/tests/test_xenium.py +0 -0
|
@@ -5,18 +5,24 @@ changelog:
|
|
|
5
5
|
authors:
|
|
6
6
|
- pre-commit-ci
|
|
7
7
|
categories:
|
|
8
|
-
- title:
|
|
8
|
+
- title: Added
|
|
9
9
|
labels:
|
|
10
|
-
- "release-
|
|
11
|
-
- title: Minor
|
|
12
|
-
labels:
|
|
13
|
-
- "release-minor"
|
|
10
|
+
- "release-added"
|
|
14
11
|
- title: Changed
|
|
15
12
|
labels:
|
|
16
13
|
- "release-changed"
|
|
14
|
+
- title: Deprecated
|
|
15
|
+
labels:
|
|
16
|
+
- "release-deprecated"
|
|
17
|
+
- title: Removed
|
|
18
|
+
labels:
|
|
19
|
+
- "release-removed"
|
|
17
20
|
- title: Fixed
|
|
18
21
|
labels:
|
|
19
22
|
- "release-fixed"
|
|
23
|
+
- title: Security
|
|
24
|
+
labels:
|
|
25
|
+
- "release-security"
|
|
20
26
|
- title: Other Changes
|
|
21
27
|
labels:
|
|
22
28
|
- "*"
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
name: Prepare test data
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
|
+
schedule:
|
|
5
|
+
- cron: "0 0 1 * *" # run once a month to prevent artifact expiration
|
|
4
6
|
workflow_dispatch:
|
|
5
7
|
# uncomment and adjust the branch name if you need to add new datasets to the artifact
|
|
6
8
|
# push:
|
|
@@ -44,7 +46,7 @@ jobs:
|
|
|
44
46
|
done
|
|
45
47
|
|
|
46
48
|
- name: Upload artifacts
|
|
47
|
-
uses: actions/upload-artifact@
|
|
49
|
+
uses: actions/upload-artifact@v4
|
|
48
50
|
with:
|
|
49
51
|
name: data
|
|
50
52
|
path: ./data
|
|
@@ -37,7 +37,7 @@ jobs:
|
|
|
37
37
|
run: |
|
|
38
38
|
echo "::set-output name=dir::$(pip cache dir)"
|
|
39
39
|
- name: Restore pip cache
|
|
40
|
-
uses: actions/cache@
|
|
40
|
+
uses: actions/cache@v3
|
|
41
41
|
with:
|
|
42
42
|
path: ${{ steps.pip-cache-dir.outputs.dir }}
|
|
43
43
|
key: pip-${{ runner.os }}-${{ env.pythonLocation }}-${{ hashFiles('**/pyproject.toml') }}
|
|
@@ -53,18 +53,12 @@ jobs:
|
|
|
53
53
|
|
|
54
54
|
- name: Download artifact of test data
|
|
55
55
|
if: matrix.python == '3.12'
|
|
56
|
-
uses: dawidd6/action-download-artifact@
|
|
56
|
+
uses: dawidd6/action-download-artifact@v9
|
|
57
57
|
with:
|
|
58
58
|
workflow: prepare_test_data.yaml
|
|
59
59
|
name: data
|
|
60
60
|
path: ./data
|
|
61
61
|
|
|
62
|
-
- name: List the data directory
|
|
63
|
-
if: matrix.python == '3.12'
|
|
64
|
-
run: |
|
|
65
|
-
ls -l ./data
|
|
66
|
-
pwd
|
|
67
|
-
|
|
68
62
|
- name: Test
|
|
69
63
|
env:
|
|
70
64
|
MPLBACKEND: agg
|
|
@@ -7,11 +7,11 @@ default_stages:
|
|
|
7
7
|
minimum_pre_commit_version: 2.16.0
|
|
8
8
|
repos:
|
|
9
9
|
- repo: https://github.com/psf/black
|
|
10
|
-
rev:
|
|
10
|
+
rev: 25.1.0
|
|
11
11
|
hooks:
|
|
12
12
|
- id: black
|
|
13
13
|
- repo: https://github.com/rbubley/mirrors-prettier
|
|
14
|
-
rev: v3.
|
|
14
|
+
rev: v3.5.1
|
|
15
15
|
hooks:
|
|
16
16
|
- id: prettier
|
|
17
17
|
- repo: https://github.com/asottile/blacken-docs
|
|
@@ -19,11 +19,11 @@ repos:
|
|
|
19
19
|
hooks:
|
|
20
20
|
- id: blacken-docs
|
|
21
21
|
- repo: https://github.com/PyCQA/isort
|
|
22
|
-
rev:
|
|
22
|
+
rev: 6.0.0
|
|
23
23
|
hooks:
|
|
24
24
|
- id: isort
|
|
25
25
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
|
26
|
-
rev: v1.
|
|
26
|
+
rev: v1.15.0
|
|
27
27
|
hooks:
|
|
28
28
|
- id: mypy
|
|
29
29
|
additional_dependencies: [numpy, types-PyYAML]
|
|
@@ -59,7 +59,7 @@ repos:
|
|
|
59
59
|
- --remove-unused-variable
|
|
60
60
|
- --ignore-init-module-imports
|
|
61
61
|
- repo: https://github.com/PyCQA/flake8
|
|
62
|
-
rev: 7.1.
|
|
62
|
+
rev: 7.1.2
|
|
63
63
|
hooks:
|
|
64
64
|
- id: flake8
|
|
65
65
|
additional_dependencies:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spatialdata-io
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
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
|
|
@@ -9,7 +9,7 @@ Author: scverse
|
|
|
9
9
|
Maintainer-email: scverse <scverse@scverse.scverse>
|
|
10
10
|
License: BSD 3-Clause License
|
|
11
11
|
|
|
12
|
-
Copyright (c) 2022, scverse
|
|
12
|
+
Copyright (c) 2022, scverse®
|
|
13
13
|
All rights reserved.
|
|
14
14
|
|
|
15
15
|
Redistribution and use in source and binary forms, with or without
|
|
@@ -181,7 +181,20 @@ Marconato, L., Palla, G., Yamauchi, K.A. et al. SpatialData: an open and univers
|
|
|
181
181
|
|
|
182
182
|
[scverse-discourse]: https://discourse.scverse.org/
|
|
183
183
|
[issue-tracker]: https://github.com/scverse/spatialdata-io/issues
|
|
184
|
-
[changelog]: https://spatialdata.scverse.org/projects/io/en/
|
|
185
|
-
[link-docs]: https://spatialdata.scverse.org/projects/io/en/
|
|
186
|
-
[link-api]: https://spatialdata.scverse.org/projects/io/en/
|
|
187
|
-
[link-cli]: https://spatialdata.scverse.org/projects/io/en/
|
|
184
|
+
[changelog]: https://spatialdata.scverse.org/projects/io/en/stable/changelog.html
|
|
185
|
+
[link-docs]: https://spatialdata.scverse.org/projects/io/en/stable/
|
|
186
|
+
[link-api]: https://spatialdata.scverse.org/projects/io/en/stable/api.html
|
|
187
|
+
[link-cli]: https://spatialdata.scverse.org/projects/io/en/stable/cli.html
|
|
188
|
+
[//]: # "numfocus-fiscal-sponsor-attribution"
|
|
189
|
+
|
|
190
|
+
spatialdata-io is part of the scverse® project ([website](https://scverse.org), [governance](https://scverse.org/about/roles)) and is fiscally sponsored by [NumFOCUS](https://numfocus.org/).
|
|
191
|
+
If you like scverse® and want to support our mission, please consider making a tax-deductible [donation](https://numfocus.org/donate-to-scverse) to help the project pay for developer time, professional services, travel, workshops, and a variety of other needs.
|
|
192
|
+
|
|
193
|
+
<div align="center">
|
|
194
|
+
<a href="https://numfocus.org/project/scverse">
|
|
195
|
+
<img
|
|
196
|
+
src="https://raw.githubusercontent.com/numfocus/templates/master/images/numfocus-logo.png"
|
|
197
|
+
width="200"
|
|
198
|
+
>
|
|
199
|
+
</a>
|
|
200
|
+
</div>
|
|
@@ -107,7 +107,20 @@ Marconato, L., Palla, G., Yamauchi, K.A. et al. SpatialData: an open and univers
|
|
|
107
107
|
|
|
108
108
|
[scverse-discourse]: https://discourse.scverse.org/
|
|
109
109
|
[issue-tracker]: https://github.com/scverse/spatialdata-io/issues
|
|
110
|
-
[changelog]: https://spatialdata.scverse.org/projects/io/en/
|
|
111
|
-
[link-docs]: https://spatialdata.scverse.org/projects/io/en/
|
|
112
|
-
[link-api]: https://spatialdata.scverse.org/projects/io/en/
|
|
113
|
-
[link-cli]: https://spatialdata.scverse.org/projects/io/en/
|
|
110
|
+
[changelog]: https://spatialdata.scverse.org/projects/io/en/stable/changelog.html
|
|
111
|
+
[link-docs]: https://spatialdata.scverse.org/projects/io/en/stable/
|
|
112
|
+
[link-api]: https://spatialdata.scverse.org/projects/io/en/stable/api.html
|
|
113
|
+
[link-cli]: https://spatialdata.scverse.org/projects/io/en/stable/cli.html
|
|
114
|
+
[//]: # "numfocus-fiscal-sponsor-attribution"
|
|
115
|
+
|
|
116
|
+
spatialdata-io is part of the scverse® project ([website](https://scverse.org), [governance](https://scverse.org/about/roles)) and is fiscally sponsored by [NumFOCUS](https://numfocus.org/).
|
|
117
|
+
If you like scverse® and want to support our mission, please consider making a tax-deductible [donation](https://numfocus.org/donate-to-scverse) to help the project pay for developer time, professional services, travel, workshops, and a variety of other needs.
|
|
118
|
+
|
|
119
|
+
<div align="center">
|
|
120
|
+
<a href="https://numfocus.org/project/scverse">
|
|
121
|
+
<img
|
|
122
|
+
src="https://raw.githubusercontent.com/numfocus/templates/master/images/numfocus-logo.png"
|
|
123
|
+
width="200"
|
|
124
|
+
>
|
|
125
|
+
</a>
|
|
126
|
+
</div>
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
# file generated by
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
2
|
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
|
|
5
|
+
|
|
3
6
|
TYPE_CHECKING = False
|
|
4
7
|
if TYPE_CHECKING:
|
|
5
|
-
from typing import Tuple
|
|
8
|
+
from typing import Tuple
|
|
9
|
+
from typing import Union
|
|
10
|
+
|
|
6
11
|
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
7
12
|
else:
|
|
8
13
|
VERSION_TUPLE = object
|
|
@@ -12,5 +17,5 @@ __version__: str
|
|
|
12
17
|
__version_tuple__: VERSION_TUPLE
|
|
13
18
|
version_tuple: VERSION_TUPLE
|
|
14
19
|
|
|
15
|
-
__version__ = version = '0.
|
|
16
|
-
__version_tuple__ = version_tuple = (0,
|
|
20
|
+
__version__ = version = '0.2.0'
|
|
21
|
+
__version_tuple__ = version_tuple = (0, 2, 0)
|
|
@@ -621,8 +621,8 @@ def macsima_wrapper(
|
|
|
621
621
|
@click.option("--name", "-n", type=str, help="name of the element to be stored")
|
|
622
622
|
@click.option(
|
|
623
623
|
"--data-axes",
|
|
624
|
-
type=
|
|
625
|
-
help="Axes of the data for image files. Valid values are 'cyx' and 'czyx'.",
|
|
624
|
+
type=str,
|
|
625
|
+
help="Axes of the data for image files. Valid values are permutations of 'cyx' and 'czyx'.",
|
|
626
626
|
)
|
|
627
627
|
@click.option(
|
|
628
628
|
"--coordinate-system",
|
|
@@ -638,6 +638,8 @@ def read_generic_wrapper(
|
|
|
638
638
|
coordinate_system: str | None = None,
|
|
639
639
|
) -> None:
|
|
640
640
|
"""Read generic data to SpatialData"""
|
|
641
|
+
if data_axes is not None and "".join(sorted(data_axes)) not in ["cxy", "cxyz"]:
|
|
642
|
+
raise ValueError("data_axes must be a permutation of 'cyx' or 'czyx'.")
|
|
641
643
|
generic_to_zarr(input=input, output=output, name=name, data_axes=data_axes, coordinate_system=coordinate_system)
|
|
642
644
|
|
|
643
645
|
|
|
@@ -148,6 +148,9 @@ class XeniumKeys(ModeEnum):
|
|
|
148
148
|
|
|
149
149
|
# specs keys
|
|
150
150
|
ANALYSIS_SW_VERSION = "analysis_sw_version"
|
|
151
|
+
# spec which contains xeniumranger version whenever xeniumranger is used to resegment the data; the new, resegmented data
|
|
152
|
+
# needs to be parsed by considering the xeniumranger version
|
|
153
|
+
XENIUM_RANGER = "xenium_ranger"
|
|
151
154
|
|
|
152
155
|
# zarr file with labels file and cell summary keys
|
|
153
156
|
CELLS_ZARR = "cells.zarr.zip"
|
{spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/src/spatialdata_io/converters/generic_to_zarr.py
RENAMED
|
@@ -69,14 +69,14 @@ def generic_to_zarr(
|
|
|
69
69
|
)
|
|
70
70
|
|
|
71
71
|
if output.exists():
|
|
72
|
-
sdata = SpatialData.
|
|
72
|
+
sdata = SpatialData.read(output)
|
|
73
73
|
if name in sdata:
|
|
74
74
|
raise ValueError(
|
|
75
75
|
f"Name {name} already exists in {output}; please provide a different name or delete the "
|
|
76
76
|
f"existing element."
|
|
77
77
|
)
|
|
78
78
|
sdata[name] = element
|
|
79
|
-
sdata.write_element(
|
|
79
|
+
sdata.write_element(element_name=name)
|
|
80
80
|
print(f"Element {name} written to {output}")
|
|
81
81
|
else:
|
|
82
82
|
sdata = SpatialData.init_from_elements(elements={name: element})
|
|
@@ -15,14 +15,10 @@ from anndata import AnnData
|
|
|
15
15
|
from dask.dataframe import DataFrame as DaskDataFrame
|
|
16
16
|
from dask_image.imread import imread
|
|
17
17
|
from scipy.sparse import csr_matrix
|
|
18
|
-
|
|
19
|
-
# from spatialdata._core.core_utils import xy_cs
|
|
20
18
|
from skimage.transform import estimate_transform
|
|
21
19
|
from spatialdata import SpatialData
|
|
22
20
|
from spatialdata._logging import logger
|
|
23
21
|
from spatialdata.models import Image2DModel, Labels2DModel, PointsModel, TableModel
|
|
24
|
-
|
|
25
|
-
# from spatialdata._core.ngff.ngff_coordinate_system import NgffAxis # , CoordinateSystem
|
|
26
22
|
from spatialdata.transformations.transformations import Affine, Identity
|
|
27
23
|
|
|
28
24
|
from spatialdata_io._constants._constants import CosmxKeys
|
|
@@ -30,10 +26,6 @@ from spatialdata_io._docs import inject_docs
|
|
|
30
26
|
|
|
31
27
|
__all__ = ["cosmx"]
|
|
32
28
|
|
|
33
|
-
# x_axis = NgffAxis(name="x", type="space", unit="discrete")
|
|
34
|
-
# y_axis = NgffAxis(name="y", type="space", unit="discrete")
|
|
35
|
-
# c_axis = NgffAxis(name="c", type="channel", unit="index")
|
|
36
|
-
|
|
37
29
|
|
|
38
30
|
@inject_docs(cx=CosmxKeys)
|
|
39
31
|
def cosmx(
|
|
@@ -177,7 +169,7 @@ def cosmx(
|
|
|
177
169
|
fovs_images_and_labels = set(fovs_images).intersection(set(fovs_labels))
|
|
178
170
|
fovs_diff = fovs_images_and_labels.difference(set(fovs_counts))
|
|
179
171
|
if len(fovs_diff):
|
|
180
|
-
|
|
172
|
+
logger.warning(
|
|
181
173
|
f"Found images and labels for {len(fovs_images)} FOVs, but only {len(fovs_counts)} FOVs in the counts file.\n"
|
|
182
174
|
+ f"The following FOVs are missing: {fovs_diff} \n"
|
|
183
175
|
+ "... will use only fovs in Table."
|
|
@@ -33,6 +33,7 @@ def visium(
|
|
|
33
33
|
fullres_image_file: str | Path | None = None,
|
|
34
34
|
tissue_positions_file: str | Path | None = None,
|
|
35
35
|
scalefactors_file: str | Path | None = None,
|
|
36
|
+
var_names_make_unique: bool = True,
|
|
36
37
|
imread_kwargs: Mapping[str, Any] = MappingProxyType({}),
|
|
37
38
|
image_models_kwargs: Mapping[str, Any] = MappingProxyType({}),
|
|
38
39
|
**kwargs: Any,
|
|
@@ -72,6 +73,8 @@ def visium(
|
|
|
72
73
|
Path to the tissue positions file.
|
|
73
74
|
scalefactors_file
|
|
74
75
|
Path to the scalefactors file.
|
|
76
|
+
var_names_make_unique
|
|
77
|
+
If `True`, call `.var_names_make_unique()` on each `AnnData` table.
|
|
75
78
|
imread_kwargs
|
|
76
79
|
Keyword arguments passed to :func:`dask_image.imread.imread`.
|
|
77
80
|
image_models_kwargs
|
|
@@ -114,7 +117,10 @@ def visium(
|
|
|
114
117
|
assert counts_file is not None
|
|
115
118
|
|
|
116
119
|
if library_id is None and dataset_id is None:
|
|
117
|
-
raise ValueError(
|
|
120
|
+
raise ValueError(
|
|
121
|
+
"Cannot determine the `library_id`. Please provide `dataset_id`; the `dataset_id` value will be used to "
|
|
122
|
+
"name the elements in the `SpatialData` object."
|
|
123
|
+
)
|
|
118
124
|
|
|
119
125
|
if dataset_id is not None:
|
|
120
126
|
if dataset_id != library_id and library_id is not None:
|
|
@@ -202,14 +208,16 @@ def visium(
|
|
|
202
208
|
radius=scalefactors["spot_diameter_fullres"] / 2.0,
|
|
203
209
|
index=adata.obs["spot_id"].copy(),
|
|
204
210
|
transformations={
|
|
205
|
-
|
|
206
|
-
"
|
|
207
|
-
"
|
|
211
|
+
dataset_id: transform_original,
|
|
212
|
+
f"{dataset_id}_downscaled_hires": transform_hires,
|
|
213
|
+
f"{dataset_id}_downscaled_lowres": transform_lowres,
|
|
208
214
|
},
|
|
209
215
|
)
|
|
210
216
|
shapes[dataset_id] = circles
|
|
211
217
|
adata.obs["region"] = dataset_id
|
|
212
218
|
table = TableModel.parse(adata, region=dataset_id, region_key="region", instance_key="spot_id")
|
|
219
|
+
if var_names_make_unique:
|
|
220
|
+
table.var_names_make_unique()
|
|
213
221
|
|
|
214
222
|
images = {}
|
|
215
223
|
if fullres_image_file is not None:
|
|
@@ -220,7 +228,7 @@ def visium(
|
|
|
220
228
|
images[dataset_id + "_full_image"] = Image2DModel.parse(
|
|
221
229
|
full_image,
|
|
222
230
|
scale_factors=[2, 2, 2, 2],
|
|
223
|
-
transformations={
|
|
231
|
+
transformations={dataset_id: transform_original},
|
|
224
232
|
rgb=None,
|
|
225
233
|
**image_models_kwargs,
|
|
226
234
|
)
|
|
@@ -231,14 +239,16 @@ def visium(
|
|
|
231
239
|
image_hires = imread(path / VisiumKeys.IMAGE_HIRES_FILE, **imread_kwargs).squeeze().transpose(2, 0, 1)
|
|
232
240
|
image_hires = DataArray(image_hires, dims=("c", "y", "x"))
|
|
233
241
|
images[dataset_id + "_hires_image"] = Image2DModel.parse(
|
|
234
|
-
image_hires,
|
|
242
|
+
image_hires,
|
|
243
|
+
transformations={f"{dataset_id}_downscaled_hires": Identity(), dataset_id: transform_hires.inverse()},
|
|
244
|
+
rgb=None,
|
|
235
245
|
)
|
|
236
246
|
if (path / VisiumKeys.IMAGE_LOWRES_FILE).exists():
|
|
237
247
|
image_lowres = imread(path / VisiumKeys.IMAGE_LOWRES_FILE, **imread_kwargs).squeeze().transpose(2, 0, 1)
|
|
238
248
|
image_lowres = DataArray(image_lowres, dims=("c", "y", "x"))
|
|
239
249
|
images[dataset_id + "_lowres_image"] = Image2DModel.parse(
|
|
240
250
|
image_lowres,
|
|
241
|
-
transformations={"
|
|
251
|
+
transformations={f"{dataset_id}_downscaled_lowres": Identity(), dataset_id: transform_lowres.inverse()},
|
|
242
252
|
rgb=None,
|
|
243
253
|
)
|
|
244
254
|
|
|
@@ -17,16 +17,17 @@ from geopandas import GeoDataFrame
|
|
|
17
17
|
from imageio import imread as imread2
|
|
18
18
|
from multiscale_spatial_image import MultiscaleSpatialImage
|
|
19
19
|
from numpy.random import default_rng
|
|
20
|
+
from skimage.transform import ProjectiveTransform, warp
|
|
20
21
|
from spatial_image import SpatialImage
|
|
21
|
-
from spatialdata import
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
Scale,
|
|
27
|
-
Sequence,
|
|
28
|
-
set_transformation,
|
|
22
|
+
from spatialdata import (
|
|
23
|
+
SpatialData,
|
|
24
|
+
get_extent,
|
|
25
|
+
rasterize_bins,
|
|
26
|
+
rasterize_bins_link_table_to_labels,
|
|
29
27
|
)
|
|
28
|
+
from spatialdata._types import ArrayLike
|
|
29
|
+
from spatialdata.models import Image2DModel, ShapesModel, TableModel
|
|
30
|
+
from spatialdata.transformations import Affine, Identity, Scale, set_transformation
|
|
30
31
|
from xarray import DataArray
|
|
31
32
|
|
|
32
33
|
from spatialdata_io._constants._constants import VisiumHDKeys
|
|
@@ -45,6 +46,7 @@ def visium_hd(
|
|
|
45
46
|
annotate_table_by_labels: bool = False,
|
|
46
47
|
fullres_image_file: str | Path | None = None,
|
|
47
48
|
load_all_images: bool = False,
|
|
49
|
+
var_names_make_unique: bool = True,
|
|
48
50
|
imread_kwargs: Mapping[str, Any] = MappingProxyType({}),
|
|
49
51
|
image_models_kwargs: Mapping[str, Any] = MappingProxyType({}),
|
|
50
52
|
anndata_kwargs: Mapping[str, Any] = MappingProxyType({}),
|
|
@@ -61,7 +63,8 @@ def visium_hd(
|
|
|
61
63
|
path
|
|
62
64
|
Path to directory containing the *10x Genomics* Visium HD output.
|
|
63
65
|
dataset_id
|
|
64
|
-
Unique identifier of the dataset
|
|
66
|
+
Unique identifier of the dataset, used to name the elements of the `SpatialData` object. If `None`, it tries to
|
|
67
|
+
infer it from the file name of the feature slice file.
|
|
65
68
|
filtered_counts_file
|
|
66
69
|
It sets the value of `counts_file` to ``{vx.FILTERED_COUNTS_FILE!r}`` (when `True`) or to
|
|
67
70
|
``{vx.RAW_COUNTS_FILE!r}`` (when `False`).
|
|
@@ -80,6 +83,8 @@ def visium_hd(
|
|
|
80
83
|
load_all_images
|
|
81
84
|
If `False`, load only the full resolution, high resolution and low resolution images. If `True`, also the
|
|
82
85
|
following images: ``{vx.IMAGE_CYTASSIST!r}``.
|
|
86
|
+
var_names_make_unique
|
|
87
|
+
If `True`, call `.var_names_make_unique()` on each `AnnData` table.
|
|
83
88
|
imread_kwargs
|
|
84
89
|
Keyword arguments for :func:`imageio.imread`.
|
|
85
90
|
image_models_kwargs
|
|
@@ -100,7 +105,8 @@ def visium_hd(
|
|
|
100
105
|
|
|
101
106
|
if dataset_id is None:
|
|
102
107
|
dataset_id = _infer_dataset_id(path)
|
|
103
|
-
|
|
108
|
+
|
|
109
|
+
filename_prefix = _get_filename_prefix(path, dataset_id)
|
|
104
110
|
|
|
105
111
|
def load_image(path: Path, suffix: str, scale_factors: list[int] | None = None) -> None:
|
|
106
112
|
_load_image(
|
|
@@ -236,9 +242,9 @@ def visium_hd(
|
|
|
236
242
|
shapes_name = dataset_id + "_" + bin_size_str
|
|
237
243
|
radius = scalefactors[VisiumHDKeys.SCALEFACTORS_SPOT_DIAMETER_FULLRES] / 2.0
|
|
238
244
|
transformations = {
|
|
239
|
-
|
|
240
|
-
"
|
|
241
|
-
"
|
|
245
|
+
dataset_id: transform_original,
|
|
246
|
+
f"{dataset_id}_downscaled_hires": transform_hires,
|
|
247
|
+
f"{dataset_id}_downscaled_lowres": transform_lowres,
|
|
242
248
|
}
|
|
243
249
|
circles = ShapesModel.parse(
|
|
244
250
|
adata.obsm["spatial"],
|
|
@@ -265,6 +271,8 @@ def visium_hd(
|
|
|
265
271
|
region_key=str(VisiumHDKeys.REGION_KEY),
|
|
266
272
|
instance_key=str(VisiumHDKeys.INSTANCE_KEY),
|
|
267
273
|
)
|
|
274
|
+
if var_names_make_unique:
|
|
275
|
+
tables[bin_size_str].var_names_make_unique()
|
|
268
276
|
|
|
269
277
|
# read full resolution image
|
|
270
278
|
if fullres_image_file is not None:
|
|
@@ -314,7 +322,10 @@ def visium_hd(
|
|
|
314
322
|
)
|
|
315
323
|
set_transformation(
|
|
316
324
|
images[dataset_id + "_hires_image"],
|
|
317
|
-
{
|
|
325
|
+
{
|
|
326
|
+
f"{dataset_id}_downscaled_hires": Identity(),
|
|
327
|
+
dataset_id: transform_hires.inverse(),
|
|
328
|
+
},
|
|
318
329
|
set_all=True,
|
|
319
330
|
)
|
|
320
331
|
|
|
@@ -332,7 +343,10 @@ def visium_hd(
|
|
|
332
343
|
)
|
|
333
344
|
set_transformation(
|
|
334
345
|
images[dataset_id + "_lowres_image"],
|
|
335
|
-
{
|
|
346
|
+
{
|
|
347
|
+
f"{dataset_id}_downscaled_lowres": Identity(),
|
|
348
|
+
dataset_id: transform_lowres.inverse(),
|
|
349
|
+
},
|
|
336
350
|
set_all=True,
|
|
337
351
|
)
|
|
338
352
|
|
|
@@ -351,15 +365,60 @@ def visium_hd(
|
|
|
351
365
|
)
|
|
352
366
|
image = images[dataset_id + "_cytassist_image"]
|
|
353
367
|
transform_matrices = _get_transform_matrices(metadata, hd_layout)
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
368
|
+
projective0 = transform_matrices["cytassist_colrow_to_spot_colrow"]
|
|
369
|
+
projective1 = transform_matrices["spot_colrow_to_microscope_colrow"]
|
|
370
|
+
projective = projective1 @ projective0
|
|
371
|
+
projective /= projective[2, 2]
|
|
372
|
+
if _projective_matrix_is_affine(projective):
|
|
373
|
+
affine = Affine(projective, input_axes=("x", "y"), output_axes=("x", "y"))
|
|
374
|
+
set_transformation(image, affine, dataset_id)
|
|
375
|
+
else:
|
|
376
|
+
# the projective matrix is not affine, we will separate the affine part and the projective shift, and apply
|
|
377
|
+
# the projective shift to the image
|
|
378
|
+
affine_matrix, projective_shift = _decompose_projective_matrix(projective)
|
|
379
|
+
affine = Affine(affine_matrix, input_axes=("x", "y"), output_axes=("x", "y"))
|
|
380
|
+
|
|
381
|
+
# determine the size of the transformed image
|
|
382
|
+
bounding_box = get_extent(image, coordinate_system=dataset_id)
|
|
383
|
+
x0, x1 = bounding_box["x"]
|
|
384
|
+
y0, y1 = bounding_box["y"]
|
|
385
|
+
x1 -= 1
|
|
386
|
+
y1 -= 1
|
|
387
|
+
corners = [(x0, y0), (x1, y0), (x1, y1), (x0, y1)]
|
|
388
|
+
|
|
389
|
+
transformed_corners = []
|
|
390
|
+
for x, y in corners:
|
|
391
|
+
px, py = _projective_matrix_transform_point(projective_shift, x, y)
|
|
392
|
+
transformed_corners.append((px, py))
|
|
393
|
+
transformed_corners_array = np.array(transformed_corners)
|
|
394
|
+
transformed_bounds = (
|
|
395
|
+
np.min(transformed_corners_array[:, 0]),
|
|
396
|
+
np.min(transformed_corners_array[:, 1]),
|
|
397
|
+
np.max(transformed_corners_array[:, 0]),
|
|
398
|
+
np.max(transformed_corners_array[:, 1]),
|
|
399
|
+
)
|
|
400
|
+
# the first two components are <= 0, we just discard them since the cytassist image has a lot of padding
|
|
401
|
+
# and therefore we can safely discard pixels with negative coordinates
|
|
402
|
+
transformed_shape = (np.ceil(transformed_bounds[2]), np.ceil(transformed_bounds[3]))
|
|
403
|
+
|
|
404
|
+
# flip xy
|
|
405
|
+
transformed_shape = (transformed_shape[1], transformed_shape[0])
|
|
406
|
+
|
|
407
|
+
# the cytassist image is a small, single-scale image, so we can compute it in memory
|
|
408
|
+
numpy_data = image.transpose("y", "x", "c").data.compute()
|
|
409
|
+
warped = warp(
|
|
410
|
+
numpy_data, ProjectiveTransform(projective_shift).inverse, output_shape=transformed_shape, order=1
|
|
411
|
+
)
|
|
412
|
+
warped = np.round(warped * 255).astype(np.uint8)
|
|
413
|
+
warped = Image2DModel.parse(warped, dims=("y", "x", "c"), transformations={dataset_id: affine}, rgb=True)
|
|
414
|
+
|
|
415
|
+
# we replace the cytassist image with the warped image
|
|
416
|
+
images[dataset_id + "_cytassist_image"] = warped
|
|
357
417
|
|
|
358
418
|
sdata = SpatialData(tables=tables, images=images, shapes=shapes, labels=labels)
|
|
359
419
|
|
|
360
420
|
if annotate_table_by_labels:
|
|
361
421
|
for bin_size_str in bin_sizes:
|
|
362
|
-
|
|
363
422
|
shapes_name = dataset_id + "_" + bin_size_str
|
|
364
423
|
|
|
365
424
|
# add labels layer (rasterized bins).
|
|
@@ -388,7 +447,8 @@ def _infer_dataset_id(path: Path) -> str:
|
|
|
388
447
|
files = [file.name for file in path.iterdir() if file.is_file() and file.name.endswith(suffix)]
|
|
389
448
|
if len(files) == 0 or len(files) > 1:
|
|
390
449
|
raise ValueError(
|
|
391
|
-
f"Cannot infer `dataset_id` from the feature slice file in {path}, please pass `dataset_id` as an
|
|
450
|
+
f"Cannot infer `dataset_id` from the feature slice file in {path}, please pass `dataset_id` as an "
|
|
451
|
+
f"argument. The `dataset_id` value will be used to name the elements in the `SpatialData` object."
|
|
392
452
|
)
|
|
393
453
|
return files[0].replace(suffix, "")
|
|
394
454
|
|
|
@@ -424,20 +484,63 @@ def _load_image(
|
|
|
424
484
|
), "When the image is not in RGB, the first dimension should be the number of channels."
|
|
425
485
|
|
|
426
486
|
image = DataArray(data, dims=("c", "y", "x"))
|
|
427
|
-
parsed = Image2DModel.parse(
|
|
487
|
+
parsed = Image2DModel.parse(
|
|
488
|
+
image,
|
|
489
|
+
scale_factors=scale_factors,
|
|
490
|
+
rgb=None,
|
|
491
|
+
transformations={dataset_id: Identity()},
|
|
492
|
+
**image_models_kwargs,
|
|
493
|
+
)
|
|
428
494
|
images[dataset_id + suffix] = parsed
|
|
429
495
|
else:
|
|
430
496
|
warnings.warn(f"File {path} does not exist, skipping it.", UserWarning, stacklevel=2)
|
|
431
497
|
return None
|
|
432
498
|
|
|
433
499
|
|
|
434
|
-
def
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
500
|
+
def _projective_matrix_transform_point(projective_shift: ArrayLike, x: float, y: float) -> tuple[float, float]:
|
|
501
|
+
v = np.array([x, y, 1])
|
|
502
|
+
v = projective_shift @ v
|
|
503
|
+
v /= v[2]
|
|
504
|
+
return v[0], v[1]
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _projective_matrix_is_affine(projective_matrix: ArrayLike) -> bool:
|
|
508
|
+
assert np.allclose(projective_matrix[2, 2], 1), "A projective matrix should have a 1 in the bottom right corner."
|
|
509
|
+
return np.allclose(projective_matrix[2, :2], [0, 0])
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _decompose_projective_matrix(projective_matrix: ArrayLike) -> tuple[ArrayLike, ArrayLike]:
|
|
513
|
+
"""
|
|
514
|
+
Decompose a projective transformation matrix into an affine transformation and a projective shift.
|
|
515
|
+
|
|
516
|
+
Parameters
|
|
517
|
+
----------
|
|
518
|
+
projective_matrix
|
|
519
|
+
Projective transformation matrix.
|
|
520
|
+
|
|
521
|
+
Returns
|
|
522
|
+
-------
|
|
523
|
+
A tuple where the first element is the affine matrix and the second element is the projective shift.
|
|
524
|
+
|
|
525
|
+
Let P be the initial projective matrix and A the affine matrix. The projective shift S is defined as: S = A^-1 @ P.
|
|
526
|
+
"""
|
|
527
|
+
assert np.allclose(projective_matrix[2, 2], 1), "A projective matrix should have a 1 in the bottom right corner."
|
|
528
|
+
affine_matrix = projective_matrix.copy()
|
|
529
|
+
affine_matrix[2] = [0, 0, 1]
|
|
530
|
+
# equivalent to np.linalg.inv(affine_matrix) @ projective_matrix, but more numerically stable
|
|
531
|
+
projective_shift = np.linalg.solve(affine_matrix, projective_matrix)
|
|
532
|
+
projective_shift /= projective_shift[2, 2]
|
|
533
|
+
return affine_matrix, projective_shift
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _get_filename_prefix(path: Path, dataset_id: str) -> str:
|
|
537
|
+
if (path / f"{dataset_id}_{VisiumHDKeys.FEATURE_SLICE_FILE.value}").exists():
|
|
538
|
+
return f"{dataset_id}_"
|
|
539
|
+
assert (path / VisiumHDKeys.FEATURE_SLICE_FILE.value).exists(), (
|
|
540
|
+
f"Cannot locate the feature slice file, please ensure the file is present in the {path} directory and/or adjust"
|
|
541
|
+
"the `dataset_id` parameter"
|
|
542
|
+
)
|
|
543
|
+
return ""
|
|
441
544
|
|
|
442
545
|
|
|
443
546
|
def _parse_metadata(path: Path, filename_prefix: str) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
@@ -447,11 +550,29 @@ def _parse_metadata(path: Path, filename_prefix: str) -> tuple[dict[str, Any], d
|
|
|
447
550
|
return metadata, hd_layout
|
|
448
551
|
|
|
449
552
|
|
|
450
|
-
def _get_transform_matrices(metadata: dict[str, Any], hd_layout: dict[str, Any]) -> dict[str,
|
|
553
|
+
def _get_transform_matrices(metadata: dict[str, Any], hd_layout: dict[str, Any]) -> dict[str, ArrayLike]:
|
|
554
|
+
"""
|
|
555
|
+
Gets 4 projective transformation matrices, describing how to align the CytAssist, spots and microscope coordinates.
|
|
556
|
+
|
|
557
|
+
Parameters
|
|
558
|
+
----------
|
|
559
|
+
metadata
|
|
560
|
+
Metadata of the Visium HD dataset parsed using `_parse_metadata()` from the feature slice file.
|
|
561
|
+
hd_layout
|
|
562
|
+
Layout of the Visium HD dataset parsed using `_parse_metadata()` from the feature slice file.
|
|
563
|
+
|
|
564
|
+
Returns
|
|
565
|
+
-------
|
|
566
|
+
A dictionary containing four projective transformation matrices:
|
|
567
|
+
- CytAssist col/row to Spot col/row
|
|
568
|
+
- Spot col/row to CytAssist col/row
|
|
569
|
+
- Microscope col/row to Spot col/row
|
|
570
|
+
- Spot col/row to Microscope col/row
|
|
571
|
+
"""
|
|
451
572
|
transform_matrices = {}
|
|
452
573
|
|
|
453
|
-
# not used
|
|
454
|
-
transform_matrices["hd_layout_transform"] =
|
|
574
|
+
# this transformation is parsed but not used in the current implementation
|
|
575
|
+
transform_matrices["hd_layout_transform"] = np.array(hd_layout[VisiumHDKeys.TRANSFORM]).reshape(3, 3)
|
|
455
576
|
|
|
456
577
|
for key in [
|
|
457
578
|
VisiumHDKeys.CYTASSIST_COLROW_TO_SPOT_COLROW,
|
|
@@ -459,7 +580,7 @@ def _get_transform_matrices(metadata: dict[str, Any], hd_layout: dict[str, Any])
|
|
|
459
580
|
VisiumHDKeys.MICROSCOPE_COLROW_TO_SPOT_COLROW,
|
|
460
581
|
VisiumHDKeys.SPOT_COLROW_TO_MICROSCOPE_COLROW,
|
|
461
582
|
]:
|
|
462
|
-
|
|
463
|
-
transform_matrices[key
|
|
583
|
+
coefficients = metadata[VisiumHDKeys.TRANSFORM_MATRICES][key]
|
|
584
|
+
transform_matrices[key] = np.array(coefficients).reshape(3, 3)
|
|
464
585
|
|
|
465
586
|
return transform_matrices
|
|
@@ -298,7 +298,6 @@ def xenium(
|
|
|
298
298
|
f"{XeniumKeys.MORPHOLOGY_FOCUS_CHANNEL_IMAGE.value.format(0)} to "
|
|
299
299
|
f"{XeniumKeys.MORPHOLOGY_FOCUS_CHANNEL_IMAGE.value.format(len(files) - 1)}, found {files}"
|
|
300
300
|
)
|
|
301
|
-
# the 'dummy' channel is a temporary workaround, see _get_images() for more details
|
|
302
301
|
if len(files) == 1:
|
|
303
302
|
channel_names = {
|
|
304
303
|
0: XeniumKeys.MORPHOLOGY_FOCUS_CHANNEL_0.value,
|
|
@@ -309,7 +308,6 @@ def xenium(
|
|
|
309
308
|
1: XeniumKeys.MORPHOLOGY_FOCUS_CHANNEL_1.value,
|
|
310
309
|
2: XeniumKeys.MORPHOLOGY_FOCUS_CHANNEL_2.value,
|
|
311
310
|
3: XeniumKeys.MORPHOLOGY_FOCUS_CHANNEL_3.value,
|
|
312
|
-
4: "dummy",
|
|
313
311
|
}
|
|
314
312
|
# this reads the scale 0 for all the 1 or 4 channels (the other files are parsed automatically)
|
|
315
313
|
# dask.image.imread will call tifffile.imread which will give a warning saying that reading multi-file
|
|
@@ -601,6 +599,8 @@ def xenium_aligned_image(
|
|
|
601
599
|
imread_kwargs: Mapping[str, Any] = MappingProxyType({}),
|
|
602
600
|
image_models_kwargs: Mapping[str, Any] = MappingProxyType({}),
|
|
603
601
|
dims: tuple[str, ...] | None = None,
|
|
602
|
+
rgba: bool = False,
|
|
603
|
+
c_coords: list[str] | None = None,
|
|
604
604
|
) -> DataTree:
|
|
605
605
|
"""
|
|
606
606
|
Read an image aligned to a Xenium dataset, with an optional alignment file.
|
|
@@ -619,6 +619,13 @@ def xenium_aligned_image(
|
|
|
619
619
|
Example: for an image with shape (1, y, 1, x, 3), use dims=("anystring", "y", "dummy", "x", "c"). Values that
|
|
620
620
|
are not "c", "x" or "y" are considered dummy dimensions and will be squeezed (the data must have len 1 for
|
|
621
621
|
those axes).
|
|
622
|
+
rgba
|
|
623
|
+
Interprets the `c` channel as RGBA, by setting the channel names to `r`, `g`, `b` (`a`). When `c_coords` is not
|
|
624
|
+
`None`, this argument is ignored.
|
|
625
|
+
c_coords
|
|
626
|
+
Channel names for the image. By default, the function will try to infer the channel names from the image
|
|
627
|
+
shape and name (by detecting if the name suggests that the image is a H&E image). Example: for an RGB image with
|
|
628
|
+
shape (3, y, x), use c_coords=["r", "g", "b"].
|
|
622
629
|
|
|
623
630
|
Returns
|
|
624
631
|
-------
|
|
@@ -645,10 +652,6 @@ def xenium_aligned_image(
|
|
|
645
652
|
else:
|
|
646
653
|
assert len(image.shape) == 3
|
|
647
654
|
assert image.shape[0] in [3, 4]
|
|
648
|
-
if image.shape[0] == 4:
|
|
649
|
-
# as explained before in _get_images(), we need to add a dummy channel until we support 4-channel images as
|
|
650
|
-
# non-RGBA images in napari
|
|
651
|
-
image = da.concatenate([image, da.zeros_like(image[0:1])], axis=0)
|
|
652
655
|
dims = ("c", "y", "x")
|
|
653
656
|
else:
|
|
654
657
|
logging.info(f"Image has shape {image.shape}, parsing with dims={dims}.")
|
|
@@ -667,10 +670,19 @@ def xenium_aligned_image(
|
|
|
667
670
|
alignment = pd.read_csv(alignment_file, header=None).values
|
|
668
671
|
transformation = Affine(alignment, input_axes=("x", "y"), output_axes=("x", "y"))
|
|
669
672
|
|
|
673
|
+
if c_coords is None and (rgba or image_path.name.endswith(XeniumKeys.ALIGNED_HE_IMAGE_SUFFIX)):
|
|
674
|
+
c_index = dims.index("c")
|
|
675
|
+
n_channels = image.shape[c_index]
|
|
676
|
+
if n_channels == 3:
|
|
677
|
+
c_coords = ["r", "g", "b"]
|
|
678
|
+
elif n_channels == 4:
|
|
679
|
+
c_coords = ["r", "g", "b", "a"]
|
|
680
|
+
|
|
670
681
|
return Image2DModel.parse(
|
|
671
682
|
image,
|
|
672
683
|
dims=dims,
|
|
673
684
|
transformations={"global": transformation},
|
|
685
|
+
c_coords=c_coords,
|
|
674
686
|
**image_models_kwargs,
|
|
675
687
|
)
|
|
676
688
|
|
|
@@ -719,7 +731,17 @@ def _parse_version_of_xenium_analyzer(
|
|
|
719
731
|
specs: dict[str, Any],
|
|
720
732
|
hide_warning: bool = True,
|
|
721
733
|
) -> packaging.version.Version | None:
|
|
722
|
-
|
|
734
|
+
|
|
735
|
+
# After using xeniumranger (e.g. 3.0.1.1) to resegment data from previous versions (e.g. xenium-1.6.0.7), a new dict is added to
|
|
736
|
+
# `specs`, named 'xenium_ranger', which contains the key 'version' and whose value specifies the version of xeniumranger used to
|
|
737
|
+
# resegment the data (e.g. 'xenium-3.0.1.1').
|
|
738
|
+
# When parsing the outs/ folder from the resegmented data, this version (rather than the original 'analysis_sw_version') is used
|
|
739
|
+
# whenever a code branch is dependent on the data version
|
|
740
|
+
if specs.get(XeniumKeys.XENIUM_RANGER):
|
|
741
|
+
string = specs[XeniumKeys.XENIUM_RANGER]["version"]
|
|
742
|
+
else:
|
|
743
|
+
string = specs[XeniumKeys.ANALYSIS_SW_VERSION]
|
|
744
|
+
|
|
723
745
|
pattern = r"^(?:x|X)enium-(\d+\.\d+\.\d+(\.\d+-\d+)?)"
|
|
724
746
|
|
|
725
747
|
result = re.search(pattern, string)
|
|
@@ -780,6 +802,3 @@ def prefix_suffix_uint32_from_cell_id_str(cell_id_str: ArrayLike) -> tuple[Array
|
|
|
780
802
|
cell_id_prefix = [int(x, 16) for x in cell_id_prefix_hex]
|
|
781
803
|
|
|
782
804
|
return np.array(cell_id_prefix, dtype=np.uint32), np.array(dataset_suffix_int)
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
##
|
|
@@ -87,7 +87,7 @@ def test_cli_read_generic_image_invalid_data_axes(runner: CliRunner) -> None:
|
|
|
87
87
|
],
|
|
88
88
|
)
|
|
89
89
|
assert result.exit_code != 0, result.output
|
|
90
|
-
assert "
|
|
90
|
+
assert "data_axes must be a permutation of 'cyx' or 'czyx'." in result.exc_info[1].args[0]
|
|
91
91
|
|
|
92
92
|
|
|
93
93
|
@pytest.mark.parametrize("cli", [True, False])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/src/spatialdata_io/converters/legacy_anndata.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spatialdata_io-0.1.7 → spatialdata_io-0.2.0}/src/spatialdata_io/readers/_utils/_read_10x_h5.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|