Skip to content
This repository was archived by the owner on Aug 27, 2025. It is now read-only.

Commit 8e2d0b8

Browse files
version 0, WIP
1 parent 71c934c commit 8e2d0b8

6 files changed

Lines changed: 73 additions & 26 deletions

File tree

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,6 @@ sdata = spatialdata.read_zarr("...")
1717
image_key = "..." # The name of the MultiscaleSpatialImage to be exported
1818

1919
spatialdata_xenium_explorer.write("/path/to/directory", sdata, image_key)
20-
```
20+
```
21+
22+
This will create up to 6 files, among which a file called `experiment.xenium`. Double-click on this file to open it on the [Xenium Explorer](https://www.10xgenomics.com/support/software/xenium-explorer/downloads) (make sure you have the latest version of the software).

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ testpaths = ["tests"]
2626
python_files = "test_*.py"
2727

2828
[tool.black]
29-
line-length = 90
29+
line-length = 100
3030
include = '\.pyi?$'
3131
exclude = '''
3232
/(

spatialdata_xenium_explorer/converter.py

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from pathlib import Path
33

44
from spatialdata import SpatialData
5+
from spatialdata.transformations import Identity, set_transformation
56

67
from . import (
78
write_cell_categories,
@@ -13,29 +14,63 @@
1314
from .constants import FileNames, experiment_dict
1415

1516

16-
def write(path: str, sdata: SpatialData, image_key: str, gene_column: str) -> None:
17+
def _order_instances(sdata: SpatialData, shapes_key: str):
18+
adata = sdata.table
19+
20+
instance_key = adata.uns["spatialdata_attrs"]["instance_key"]
21+
region_key = adata.uns["spatialdata_attrs"]["region_key"]
22+
23+
adata = adata[adata.obs[region_key] == shapes_key].copy()
24+
adata.obs.set_index(instance_key, inplace=True)
25+
adata = adata[sdata.shapes[shapes_key].index].copy()
26+
return adata
27+
28+
29+
def write(
30+
path: str,
31+
sdata: SpatialData,
32+
image_key: str,
33+
shapes_key: str,
34+
points_key: str,
35+
gene_column: str,
36+
layer: str | None = None,
37+
) -> None:
38+
"""
39+
Transform a SpatialData object into inputs for the Xenium Explorer.
40+
Currently only images of type MultiscaleSpatialImage are supported.
41+
42+
Args:
43+
path: Path to the directory where files will be saved.
44+
sdata: SpatialData object.
45+
image_key: Name of the image or interest.
46+
shapes_key: Name of the cell shapes.
47+
points_key: Name of the transcripts (key of `sdata.points`).
48+
gene_column: Column name of the points dataframe containing the gene names.
49+
layer: Layer of `sdata.table` where the gene counts are saved. If `None`, uses `sdata.table.X`.
50+
"""
1751
path: Path = Path(path)
1852
assert (
1953
not path.exists() or path.is_dir()
2054
), f"A path to an existing file was provided. It should be a path to a directory."
2155

2256
path.mkdir(parents=True, exist_ok=True)
2357

24-
adata = sdata.table
58+
adata = _order_instances(sdata, shapes_key)
2559

26-
EXPERIMENT = experiment_dict(..., ..., adata.n_obs)
60+
EXPERIMENT = experiment_dict(image_key, shapes_key, adata.n_obs)
2761
with open(path / FileNames.METADATA, "w") as f:
2862
json.dump(EXPERIMENT, f, indent=4)
2963

30-
write_gene_counts(path / FileNames.TABLE, adata)
64+
write_gene_counts(path / FileNames.TABLE, adata, layer)
3165
write_cell_categories(path / FileNames.CELL_CATEGORIES, adata)
3266

33-
polygons = sdata.shapes["..."]
34-
# TODO: transform polygon coords to pixel
35-
write_polygons(path / FileNames.SHAPES, polygons)
67+
pixels_cs = "__pixels"
68+
set_transformation(sdata.images[image_key], Identity(), pixels_cs)
69+
70+
gdf = sdata.transform_element_to_coordinate_system(sdata.shapes[shapes_key], pixels_cs)
71+
write_polygons(path / FileNames.SHAPES, gdf.geometry)
3672

37-
# TODO : make it memory efficient
3873
write_multiscale(path / FileNames.IMAGE, sdata.images[image_key])
3974

40-
df = sdata.points["..."]
75+
df = sdata.transform_element_to_coordinate_system(sdata.points[points_key], pixels_cs)
4176
write_transcripts(path / FileNames.POINTS, df, gene_column)

spatialdata_xenium_explorer/images.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
1-
from pathlib import Path
2-
31
import numpy as np
42
import tifffile as tf
53
from multiscale_spatial_image import MultiscaleSpatialImage
64

75
from .constants import image_metadata, image_options
86

97

10-
def to_uint8(arr: np.ndarray) -> np.ndarray:
11-
print(f"Writing image of shape {arr.shape}")
12-
return (arr // 256).astype(np.uint8)
8+
def _astype_uint8(arr: np.ndarray) -> np.ndarray:
9+
assert np.issubdtype(
10+
arr.dtype, np.integer
11+
), f"The image dtype has to be an integer dtype. Found {arr.dtype}"
12+
13+
if arr.dtype == np.uint8:
14+
return arr
15+
16+
factor = np.iinfo(np.uint8).max / np.iinfo(arr.dtype).max
17+
return (arr * factor).astype(np.uint8)
1318

1419

1520
def write_multiscale(
16-
output_path: Path,
21+
path: str,
1722
multiscale: MultiscaleSpatialImage,
1823
pixelsize: float = 0.2125,
1924
):
@@ -22,9 +27,10 @@ def write_multiscale(
2227

2328
metadata = image_metadata(channel_names, pixelsize)
2429

25-
with tf.TiffWriter(output_path, bigtiff=True) as tif:
30+
# TODO : make it memory efficient
31+
with tf.TiffWriter(path, bigtiff=True) as tif:
2632
tif.write(
27-
to_uint8(multiscale[scale_names[0]]["image"].values),
33+
_astype_uint8(multiscale[scale_names[0]]["image"].values),
2834
subifds=len(scale_names) - 1,
2935
resolution=(1e4 / pixelsize, 1e4 / pixelsize),
3036
metadata=metadata,
@@ -33,7 +39,7 @@ def write_multiscale(
3339

3440
for i, scale in enumerate(scale_names[1:]):
3541
tif.write(
36-
to_uint8(multiscale[scale]["image"].values),
42+
_astype_uint8(multiscale[scale]["image"].values),
3743
subfiletype=1,
3844
resolution=(
3945
1e4 * 2 ** (i + 1) / pixelsize,

spatialdata_xenium_explorer/points.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from math import ceil
22
from pathlib import Path
33

4+
import dask.dataframe as dd
45
import numpy as np
5-
import pandas as pd
66
import zarr
77

88
from .constants import ExplorerConstants
@@ -15,10 +15,13 @@ def subsample_indices(n_samples, factor: int = 4):
1515

1616
def write_transcripts(
1717
path: Path,
18-
df: pd.DataFrame,
18+
df: dd.DataFrame,
1919
gene: str = "gene",
2020
max_levels: int = 15,
2121
):
22+
# TODO: make everything using dask instead of pandas
23+
df = df.compute()
24+
2225
num_transcripts = len(df)
2326
df[gene] = df[gene].astype("category")
2427

@@ -178,5 +181,3 @@ def write_transcripts(
178181
transcript_id = transcript_id[sub_indices]
179182

180183
grids.attrs.put(GRIDS_ATTRS)
181-
182-
print(g.info)

spatialdata_xenium_explorer/table.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import numpy as np
22
import zarr
33
from anndata import AnnData
4+
from scipy.sparse import csr_matrix
45

56
from .constants import cell_categories_attrs
67

78

8-
def write_gene_counts(path: str, adata: AnnData) -> None:
9-
counts = adata.layers["counts"]
9+
def write_gene_counts(path: str, adata: AnnData, layer: str | None) -> None:
10+
counts = adata.X if layer is None else adata.layers[layer]
11+
counts = csr_matrix(counts)
1012

1113
feature_keys = list(adata.var_names) + ["Total transcripts"]
1214
feature_ids = feature_keys
@@ -69,6 +71,7 @@ def _write_categorical_column(
6971

7072

7173
def write_cell_categories(path: str, adata: AnnData) -> None:
74+
# TODO: consider also columns that can be transformed to a categorical column?
7275
cat_columns = [name for name, cat in adata.obs.dtypes.items() if cat == "category"]
7376

7477
print(f"Saving {len(cat_columns)} cell categories: {', '.join(cat_columns)}")

0 commit comments

Comments
 (0)