spatialproteomics 0.8.4__tar.gz → 0.8.6__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 (37) hide show
  1. {spatialproteomics-0.8.4/spatialproteomics.egg-info → spatialproteomics-0.8.6}/PKG-INFO +6 -7
  2. {spatialproteomics-0.8.4 → spatialproteomics-0.8.6}/README.md +2 -6
  3. {spatialproteomics-0.8.4 → spatialproteomics-0.8.6}/pyproject.toml +3 -1
  4. {spatialproteomics-0.8.4 → spatialproteomics-0.8.6}/spatialproteomics/container.py +0 -3
  5. {spatialproteomics-0.8.4 → spatialproteomics-0.8.6}/spatialproteomics/la/label.py +3 -1
  6. {spatialproteomics-0.8.4 → spatialproteomics-0.8.6}/spatialproteomics/pp/preprocessing.py +3 -3
  7. {spatialproteomics-0.8.4 → spatialproteomics-0.8.6}/spatialproteomics/tl/__init__.py +2 -1
  8. {spatialproteomics-0.8.4 → spatialproteomics-0.8.6}/spatialproteomics/tl/tool.py +191 -4
  9. {spatialproteomics-0.8.4 → spatialproteomics-0.8.6}/spatialproteomics/tl/utils.py +38 -1
  10. {spatialproteomics-0.8.4 → spatialproteomics-0.8.6/spatialproteomics.egg-info}/PKG-INFO +6 -7
  11. {spatialproteomics-0.8.4 → spatialproteomics-0.8.6}/spatialproteomics.egg-info/requires.txt +4 -0
  12. {spatialproteomics-0.8.4 → spatialproteomics-0.8.6}/LICENSE +0 -0
  13. {spatialproteomics-0.8.4 → spatialproteomics-0.8.6}/setup.cfg +0 -0
  14. {spatialproteomics-0.8.4 → spatialproteomics-0.8.6}/spatialproteomics/__init__.py +0 -0
  15. {spatialproteomics-0.8.4 → spatialproteomics-0.8.6}/spatialproteomics/base_logger.py +0 -0
  16. {spatialproteomics-0.8.4 → spatialproteomics-0.8.6}/spatialproteomics/constants.py +0 -0
  17. {spatialproteomics-0.8.4 → spatialproteomics-0.8.6}/spatialproteomics/image_container/ImageContainer.py +0 -0
  18. {spatialproteomics-0.8.4 → spatialproteomics-0.8.6}/spatialproteomics/image_container/__init__.py +0 -0
  19. {spatialproteomics-0.8.4 → spatialproteomics-0.8.6}/spatialproteomics/la/__init__.py +0 -0
  20. {spatialproteomics-0.8.4 → spatialproteomics-0.8.6}/spatialproteomics/la/utils.py +0 -0
  21. {spatialproteomics-0.8.4 → spatialproteomics-0.8.6}/spatialproteomics/nh/__init__.py +0 -0
  22. {spatialproteomics-0.8.4 → spatialproteomics-0.8.6}/spatialproteomics/nh/neighborhood.py +0 -0
  23. {spatialproteomics-0.8.4 → spatialproteomics-0.8.6}/spatialproteomics/nh/utils.py +0 -0
  24. {spatialproteomics-0.8.4 → spatialproteomics-0.8.6}/spatialproteomics/pl/__init__.py +0 -0
  25. {spatialproteomics-0.8.4 → spatialproteomics-0.8.6}/spatialproteomics/pl/plot.py +0 -0
  26. {spatialproteomics-0.8.4 → spatialproteomics-0.8.6}/spatialproteomics/pl/utils.py +0 -0
  27. {spatialproteomics-0.8.4 → spatialproteomics-0.8.6}/spatialproteomics/pp/__init__.py +0 -0
  28. {spatialproteomics-0.8.4 → spatialproteomics-0.8.6}/spatialproteomics/pp/intensity.py +0 -0
  29. {spatialproteomics-0.8.4 → spatialproteomics-0.8.6}/spatialproteomics/pp/utils.py +0 -0
  30. {spatialproteomics-0.8.4 → spatialproteomics-0.8.6}/spatialproteomics/sd/__init__.py +0 -0
  31. {spatialproteomics-0.8.4 → spatialproteomics-0.8.6}/spatialproteomics/sd/utils.py +0 -0
  32. {spatialproteomics-0.8.4 → spatialproteomics-0.8.6}/spatialproteomics.egg-info/SOURCES.txt +0 -0
  33. {spatialproteomics-0.8.4 → spatialproteomics-0.8.6}/spatialproteomics.egg-info/dependency_links.txt +0 -0
  34. {spatialproteomics-0.8.4 → spatialproteomics-0.8.6}/spatialproteomics.egg-info/top_level.txt +0 -0
  35. {spatialproteomics-0.8.4 → spatialproteomics-0.8.6}/tests/test_container.py +0 -0
  36. {spatialproteomics-0.8.4 → spatialproteomics-0.8.6}/tests/test_integration.py +0 -0
  37. {spatialproteomics-0.8.4 → spatialproteomics-0.8.6}/tests/test_to_zarr.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spatialproteomics
3
- Version: 0.8.4
3
+ Version: 0.8.6
4
4
  Summary: Spatialproteomics is an interoperable toolbox for analyzing highly multiplexed fluorescence image data
5
5
  Author: Matthias Meyer-Bender, Harald Vohringer
6
6
  Requires-Python: >=3.10
@@ -33,6 +33,8 @@ Provides-Extra: anndata
33
33
  Requires-Dist: anndata; extra == "anndata"
34
34
  Provides-Extra: astir
35
35
  Requires-Dist: astir>=0.1.5; extra == "astir"
36
+ Provides-Extra: spotiflow
37
+ Requires-Dist: spotiflow; extra == "spotiflow"
36
38
  Provides-Extra: dev
37
39
  Requires-Dist: pytest>=7.1.2; extra == "dev"
38
40
  Requires-Dist: pytest-cov>=3.0.0; extra == "dev"
@@ -41,6 +43,7 @@ Requires-Dist: isort>=5.10.1; extra == "dev"
41
43
  Requires-Dist: flake8>=5.0.4; extra == "dev"
42
44
  Requires-Dist: bandit>=1.7.4; extra == "dev"
43
45
  Requires-Dist: safety>=2.1.1; extra == "dev"
46
+ Requires-Dist: typing_extensions>=4.16; extra == "dev"
44
47
  Provides-Extra: all
45
48
  Requires-Dist: spatialdata; extra == "all"
46
49
  Requires-Dist: cellpose; extra == "all"
@@ -60,7 +63,7 @@ Dynamic: license-file
60
63
 
61
64
  ## Principles
62
65
 
63
- Multiplexed imaging data comprises at least 3 dimensions (i.e. `channels`, `x`, and `y`) and has often additional data such as segmentation masks or cell type annotations associated with it. In `spatialproteomics`, we use `xarray` to create a data structure that keeps all of these data dimension in sync. This data structure can then be used to apply all sorts of operations to the data. Users can segment cells, perform different image processing steps, quantify protein expression, predict cell types, and plot their data in various ways. By providing researchers with those tools, `spatialproteomics` can be used to quickly explore highly multiplexed spatial proteomics data directly within jupyter notebooks.
66
+ Multiplexed imaging data comprises at least 3 dimensions (i.e. `channels`, `x`, and `y`) and often contains additional data such as segmentation masks or cell type annotations associated with it. In `spatialproteomics`, we use `xarray` to create a data structure that keeps all of these data dimension in sync. This data structure can then be used to apply all sorts of operations to the data. Users can segment cells, perform different image processing steps, quantify protein expression, predict cell types, and plot their data in various ways. By providing researchers with those tools, `spatialproteomics` can be used to quickly explore highly multiplexed spatial proteomics data directly within jupyter notebooks.
64
67
 
65
68
  <p align="center" width="100%">
66
69
  <img src="docs/_static/img/supplementary_figure_1.png" alt="The spatialproteomics data structure enables synchronized subsetting across shared dimensions." style="width:70%;">
@@ -97,8 +100,4 @@ matplotlib
97
100
  ```
98
101
 
99
102
  ## Citation
100
- Spatialproteomics - an interoperable toolbox for analyzing highly multiplexed fluorescence image data
101
-
102
- Matthias Fabian Meyer-Bender, Harald Sager Voehringer, Christina Schniederjohann, Sarah Patricia Koziel, Erin Kim Chung, Ekaterina Popova, Alexander Brobeil, Lisa-Maria Held, Aamir Munir, Scverse Community, Sascha Dietrich, Peter-Martin Bruch, Wolfgang Huber
103
-
104
- bioRxiv 2025.04.29.651202; doi: https://doi.org/10.1101/2025.04.29.651202
103
+ > Meyer-Bender, M., Voehringer, H., Schniederjohann, C., Koziel, S., Chung, E., Popova, E., ... & Huber, W. (2026). Spatialproteomics: an interoperable toolbox for analyzing highly multiplexed fluorescence image data. Nature Methods, 1-10.
@@ -10,7 +10,7 @@
10
10
 
11
11
  ## Principles
12
12
 
13
- Multiplexed imaging data comprises at least 3 dimensions (i.e. `channels`, `x`, and `y`) and has often additional data such as segmentation masks or cell type annotations associated with it. In `spatialproteomics`, we use `xarray` to create a data structure that keeps all of these data dimension in sync. This data structure can then be used to apply all sorts of operations to the data. Users can segment cells, perform different image processing steps, quantify protein expression, predict cell types, and plot their data in various ways. By providing researchers with those tools, `spatialproteomics` can be used to quickly explore highly multiplexed spatial proteomics data directly within jupyter notebooks.
13
+ Multiplexed imaging data comprises at least 3 dimensions (i.e. `channels`, `x`, and `y`) and often contains additional data such as segmentation masks or cell type annotations associated with it. In `spatialproteomics`, we use `xarray` to create a data structure that keeps all of these data dimension in sync. This data structure can then be used to apply all sorts of operations to the data. Users can segment cells, perform different image processing steps, quantify protein expression, predict cell types, and plot their data in various ways. By providing researchers with those tools, `spatialproteomics` can be used to quickly explore highly multiplexed spatial proteomics data directly within jupyter notebooks.
14
14
 
15
15
  <p align="center" width="100%">
16
16
  <img src="docs/_static/img/supplementary_figure_1.png" alt="The spatialproteomics data structure enables synchronized subsetting across shared dimensions." style="width:70%;">
@@ -47,8 +47,4 @@ matplotlib
47
47
  ```
48
48
 
49
49
  ## Citation
50
- Spatialproteomics - an interoperable toolbox for analyzing highly multiplexed fluorescence image data
51
-
52
- Matthias Fabian Meyer-Bender, Harald Sager Voehringer, Christina Schniederjohann, Sarah Patricia Koziel, Erin Kim Chung, Ekaterina Popova, Alexander Brobeil, Lisa-Maria Held, Aamir Munir, Scverse Community, Sascha Dietrich, Peter-Martin Bruch, Wolfgang Huber
53
-
54
- bioRxiv 2025.04.29.651202; doi: https://doi.org/10.1101/2025.04.29.651202
50
+ > Meyer-Bender, M., Voehringer, H., Schniederjohann, C., Koziel, S., Chung, E., Popova, E., ... & Huber, W. (2026). Spatialproteomics: an interoperable toolbox for analyzing highly multiplexed fluorescence image data. Nature Methods, 1-10.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "spatialproteomics"
3
- version = "0.8.4"
3
+ version = "0.8.6"
4
4
  description = "Spatialproteomics is an interoperable toolbox for analyzing highly multiplexed fluorescence image data"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -40,6 +40,7 @@ cellpose = ["cellpose>=3.1.0"]
40
40
  stardist = ["stardist>=0.9.1"]
41
41
  anndata = ["anndata"]
42
42
  astir = ["astir>=0.1.5"]
43
+ spotiflow = ["spotiflow"]
43
44
  dev = [
44
45
  "pytest>=7.1.2",
45
46
  "pytest-cov>=3.0.0",
@@ -48,6 +49,7 @@ dev = [
48
49
  "flake8>=5.0.4",
49
50
  "bandit>=1.7.4",
50
51
  "safety>=2.1.1",
52
+ "typing_extensions>=4.16", # to avoid anndata issues
51
53
  ]
52
54
  all = ["spatialdata", "cellpose", "stardist", "astir"]
53
55
 
@@ -106,9 +106,6 @@ def load_image_data(
106
106
  if neighborhood is not None:
107
107
  dataset = dataset.nh.add_neighborhoods_from_dataframe(neighborhood, neighborhood_col=neighborhood_col)
108
108
 
109
- else:
110
- dataset = xr.Dataset(data_vars={Layers.IMAGE: im})
111
-
112
109
  return dataset
113
110
 
114
111
 
@@ -797,6 +797,7 @@ class LabelAccessor:
797
797
  cell_col: str = "cell",
798
798
  label_col: str = "label",
799
799
  min_intensity: float = 0.0,
800
+ ignore_neighborhoods: bool = False,
800
801
  ):
801
802
  """
802
803
  Predicts cell types based on the argmax classification of marker intensities.
@@ -808,6 +809,7 @@ class LabelAccessor:
808
809
  cell_col (str, optional): The name of the column to store cell IDs in the output dataframe. Defaults to "cell".
809
810
  label_col (str, optional): The name of the column to store predicted cell types in the output dataframe. Defaults to "label".
810
811
  min_intensity (float, optional): The minimum intensity threshold for assigning cells to a type. Defaults to 0.0.
812
+ ignore_neighborhoods (bool, optional): Whether to remove existing neighborhoods when predicting cell types. Defaults to False.
811
813
 
812
814
  Returns:
813
815
  xr.Dataset: A new spatialproteomics object with the predicted cell types added as labels.
@@ -869,7 +871,7 @@ class LabelAccessor:
869
871
  obj = obj.pp.drop_layers(Layers.LA_PROPERTIES)
870
872
 
871
873
  # adding the new labels
872
- return obj.la.add_labels_from_dataframe(celltype_prediction_df)
874
+ return obj.la.add_labels_from_dataframe(celltype_prediction_df, ignore_neighborhoods=ignore_neighborhoods)
873
875
 
874
876
  def _threshold_label(
875
877
  self, channel: str, threshold: float, layer_key: str = Layers.INTENSITY, label: Optional[str] = None
@@ -866,7 +866,7 @@ class PreprocessingAccessor:
866
866
  from_layer = segmentation
867
867
  segmentation = self._obj[segmentation].values.squeeze()
868
868
 
869
- assert segmentation.ndim == 2, "A segmentation mask must 2 dimensional."
869
+ assert segmentation.ndim == 2, "A segmentation mask must be 2-dimensional."
870
870
  assert ~np.any(segmentation < 0), "A segmentation mask may not contain negative numbers."
871
871
 
872
872
  y_dim, x_dim = segmentation.shape
@@ -939,14 +939,14 @@ class PreprocessingAccessor:
939
939
  """
940
940
  # checking that the layer does not exist yet
941
941
  assert key_added not in self._obj, f"Layer {key_added} already exists."
942
- assert array.ndim in [2, 3], "The array to add mask must 2 or 3-dimensional."
942
+ assert array.ndim in [2, 3], "The array to add mask must be 2- or 3-dimensional."
943
943
 
944
944
  if array.ndim == 2:
945
945
  # in the case of a 2D array
946
946
  y_dim, x_dim = array.shape
947
947
  assert (x_dim == self._obj.sizes[Dims.X]) & (
948
948
  y_dim == self._obj.sizes[Dims.Y]
949
- ), f"The shape of array does not match that of the image. Image has shape ({self._obj.sizes[Dims.Y]}, {self._obj.sizes[Dims.X]}), array has shape {array.shape}."
949
+ ), f"The shape of the array does not match that of the image. Image has shape ({self._obj.sizes[Dims.Y]}, {self._obj.sizes[Dims.X]}), array has shape {array.shape}."
950
950
 
951
951
  # create a data array with the new layer
952
952
  da = xr.DataArray(
@@ -1,4 +1,4 @@
1
- from .tool import ToolAccessor, astir, cellpose, mesmer, stardist
1
+ from .tool import ToolAccessor, astir, cellpose, mesmer, spotiflow, stardist
2
2
 
3
3
  __all__ = [
4
4
  "ToolAccessor",
@@ -6,4 +6,5 @@ __all__ = [
6
6
  "mesmer",
7
7
  "stardist",
8
8
  "astir",
9
+ "spotiflow",
9
10
  ]
@@ -7,7 +7,12 @@ import xarray as xr
7
7
 
8
8
  from ..base_logger import logger
9
9
  from ..constants import Dims, Features, Layers, Props, SDLayers
10
- from ..sd.utils import _get_channels_spatialdata, _process_adata, _process_image
10
+ from ..sd.utils import (
11
+ _get_channels_spatialdata,
12
+ _process_adata,
13
+ _process_image,
14
+ _process_segmentation,
15
+ )
11
16
  from .utils import (
12
17
  _astir,
13
18
  _cellpose,
@@ -15,6 +20,7 @@ from .utils import (
15
20
  _convert_masks_to_data_array,
16
21
  _get_channels,
17
22
  _mesmer,
23
+ _spotiflow,
18
24
  _stardist,
19
25
  )
20
26
 
@@ -262,6 +268,96 @@ def astir(
262
268
  return sdata
263
269
 
264
270
 
271
+ def spotiflow(
272
+ sdata,
273
+ channel: Optional[str] = None,
274
+ image_key: str = SDLayers.IMAGE,
275
+ segmentation_key: str = SDLayers.SEGMENTATION,
276
+ table_key: str = SDLayers.TABLE,
277
+ key_added: str = "spotiflow",
278
+ key_added_suffix: str = "_spots",
279
+ pretrained_name: str = "general",
280
+ model_kwargs: Optional[dict] = {"verbose": False},
281
+ predict_kwargs: Optional[dict] = {"verbose": False},
282
+ copy: bool = False,
283
+ ):
284
+ """
285
+ This function applies the Spotiflow algorithm to detect spots in the provided image data.
286
+ It extracts the image data from the spatialdata object, applies the Spotiflow algorithm,
287
+ and adds the detected spots to the spatialdata object.
288
+ The detected spots are stored in the points attribute of the spatialdata object.
289
+
290
+ Args:
291
+ sdata (spatialdata.SpatialData): The spatialdata object containing the image data.
292
+ channel (Optional[str]): The channel(s) to be used for spot detection. If None, all channels will be used.
293
+ image_key (str, optional): The key for the image data in the spatialdata object. Defaults to "image".
294
+ segmentation_key (str, optional): The key for the segmentation mask in the spatialdata object. Defaults to "segmentation".
295
+ table_key (str, optional): The key for the table in the spatialdata object. Defaults to "table".
296
+ key_added (str, optional): The key under which the detected spots will be stored in the points attribute of the spatialdata object. Defaults to "spotiflow".
297
+ key_added_suffix (str, optional): The suffix to be added to the channel names in the detected spots. Defaults to "_spots".
298
+ pretrained_name (str, optional): The name of the pretrained model to be used for spot detection. Defaults to "general".
299
+ model_kwargs (Optional[dict], optional): Additional keyword arguments to be passed to the Spotiflow model. Defaults to {'verbose': False}.
300
+ predict_kwargs (Optional[dict], optional): Additional keyword arguments to be passed to the Spotiflow prediction method. Defaults to {'verbose': False}.
301
+ copy (bool, optional): Whether to create a copy of the spatialdata object. Defaults to False.
302
+
303
+ Returns:
304
+ spatialdata.SpatialData: The spatialdata object with the detected spots added to the points attribute.
305
+ """
306
+ import spatialdata as sd
307
+ from spatialdata.transformations import get_transformation, set_transformation
308
+
309
+ assert image_key in sdata, f"Image key {image_key} not found in the object."
310
+ assert segmentation_key in sdata, f"Segmentation key {segmentation_key} not found in the object."
311
+ assert table_key in sdata, f"Table key {table_key} not found in the object."
312
+
313
+ if copy:
314
+ sdata = cp.deepcopy(sdata)
315
+
316
+ channels = _get_channels_spatialdata(channel)
317
+ img = _process_image(sdata, channels=channel, image_key=image_key, key_added=None)
318
+ adata = _process_adata(sdata, table_key=table_key)
319
+
320
+ points_df = _spotiflow(
321
+ img,
322
+ channel_names=channels,
323
+ pretrained_name=pretrained_name,
324
+ model_kwargs=model_kwargs,
325
+ predict_kwargs=predict_kwargs,
326
+ )
327
+
328
+ seg = _process_segmentation(sdata, segmentation_key=segmentation_key)
329
+ yy = points_df["y"].round().astype(int).clip(0, seg.shape[0] - 1)
330
+ xx = points_df["x"].round().astype(int).clip(0, seg.shape[1] - 1)
331
+ points_df["cell_id"] = seg[yy, xx]
332
+ points_df = points_df[points_df["cell_id"] > 0] # drop background detections
333
+
334
+ # --- counts per cell, per channel -------------------------------------
335
+ counts = points_df.groupby(["cell_id", "channel"]).size().unstack(fill_value=0)
336
+ counts.columns = [f"{ch}{key_added_suffix}" for ch in counts.columns]
337
+
338
+ # --- counts per cell, per channel -------------------------------------
339
+ counts = points_df.groupby(["cell_id", "channel"]).size().unstack(fill_value=0)
340
+ counts.columns = [f"{ch}{key_added_suffix}" for ch in counts.columns]
341
+ new_cols = counts.columns.tolist() # remember which columns are new
342
+
343
+ # get transformations
344
+ transformation = get_transformation(sdata[image_key])
345
+
346
+ # write points to spatialdata
347
+ sdata.points[key_added] = sd.models.PointsModel.parse(points_df, coordinates={"x": "x", "y": "y"})
348
+ set_transformation(sdata.points[key_added], transformation)
349
+
350
+ # merge counts to existing obs
351
+ obs = adata.obs
352
+ obs = obs.merge(counts, left_on="id", right_index=True, how="left")
353
+ obs[new_cols] = obs[new_cols].fillna(0).astype(int) # only fill the newly merged columns
354
+
355
+ sdata.tables[table_key].obs = obs
356
+
357
+ if copy:
358
+ return sdata
359
+
360
+
265
361
  # === SPATIALPROTEOMICS ACCESSOR ===
266
362
  @xr.register_dataset_accessor("tl")
267
363
  class ToolAccessor:
@@ -418,7 +514,7 @@ class ToolAccessor:
418
514
  self._obj.pp[channels]._image.values,
419
515
  scale=scale,
420
516
  n_tiles=n_tiles,
421
- normalize=n_tiles,
517
+ normalize=normalize,
422
518
  predict_big=predict_big,
423
519
  postprocess_func=postprocess_func,
424
520
  **kwargs,
@@ -583,11 +679,83 @@ class ToolAccessor:
583
679
  assigned_cell_types, cell_col=cell_id_col, label_col=cell_type_col
584
680
  )
585
681
 
682
+ def spotiflow(
683
+ self,
684
+ channel: Optional[str] = None,
685
+ image_key: str = Layers.IMAGE,
686
+ segmentation_key: str = Layers.SEGMENTATION,
687
+ key_added_suffix: str = "_spots",
688
+ pretrained_name: str = "general",
689
+ return_spots: bool = False,
690
+ model_kwargs: Optional[dict] = {"verbose": False},
691
+ predict_kwargs: Optional[dict] = {"verbose": False},
692
+ ):
693
+ """
694
+ This method detects spots in the image using the Spotiflow algorithm and counts the number of spots per cell, per channel.
695
+
696
+ Parameters
697
+ ----------
698
+ channel : str, optional
699
+ Channel to use for spot detection. If None, all channels are used.
700
+ image_key : str, optional
701
+ Key to the image layer in the spatialproteomics object. Default is 'image'.
702
+ segmentation_key : str, optional
703
+ Key to the segmentation layer in the spatialproteomics object. Default is 'segmentation'.
704
+ key_added_suffix : str, optional
705
+ Suffix to add to the channel names in the resulting counts. Default is '_spots'.
706
+ pretrained_name : str, optional
707
+ Name of the pretrained model to use for spot detection. Default is 'general'.
708
+ return_spots : bool, optional
709
+ Whether to return the detected spots as a DataFrame. Default is False.
710
+ model_kwargs : dict, optional
711
+ Additional keyword arguments to pass to the Spotiflow model. Default is {'verbose': False}.
712
+ predict_kwargs : dict, optional
713
+ Additional keyword arguments to pass to the Spotiflow prediction method. Default is {'verbose': False}.
714
+
715
+ Returns
716
+ -------
717
+ xr.Dataset
718
+ Dataset containing the counts of spots per cell, per channel. If return_spots is True, also returns a DataFrame of the detected spots with their coordinates and assigned cell IDs.
719
+ """
720
+ assert image_key in self._obj, f"Image key {image_key} not found in the object."
721
+ assert segmentation_key in self._obj, f"Segmentation key {segmentation_key} not found in the object."
722
+ channels = _get_channels(self._obj, key_added=None, channel=channel)
723
+ img = self._obj.pp[channels][image_key].values
724
+ points_df = _spotiflow(
725
+ img,
726
+ channel_names=channels,
727
+ pretrained_name=pretrained_name,
728
+ model_kwargs=model_kwargs,
729
+ predict_kwargs=predict_kwargs,
730
+ )
731
+
732
+ # assign each point to a cell via the segmentation mask
733
+ seg = self._obj[segmentation_key].values
734
+ yy = points_df["y"].round().astype(int).clip(0, seg.shape[0] - 1)
735
+ xx = points_df["x"].round().astype(int).clip(0, seg.shape[1] - 1)
736
+ points_df["cell_id"] = seg[yy, xx]
737
+ points_df = points_df[points_df["cell_id"] > 0] # drop background detections
738
+
739
+ # count points per cell, per channel
740
+ counts = (
741
+ points_df.groupby(["cell_id", "channel"])
742
+ .size()
743
+ .unstack(fill_value=0)
744
+ .reindex(index=self._obj.cells.values, columns=channels, fill_value=0)
745
+ )
746
+ counts.columns = [f"{ch}{key_added_suffix}" for ch in counts.columns]
747
+
748
+ if return_spots:
749
+ return self._obj.pp.add_obs_from_dataframe(counts.reset_index(drop=True)), points_df
750
+
751
+ return self._obj.pp.add_obs_from_dataframe(counts.reset_index(drop=True))
752
+
586
753
  def convert_to_anndata(
587
754
  self,
588
755
  expression_matrix_key: str = Layers.INTENSITY,
589
756
  obs_key: str = Layers.OBS,
590
757
  additional_layers: Optional[dict] = None,
758
+ additional_obsm: Optional[dict] = None,
591
759
  additional_uns: Optional[dict] = None,
592
760
  ):
593
761
  """
@@ -601,6 +769,8 @@ class ToolAccessor:
601
769
  The key of the observation data in the spatialproteomics object. Default is '_obs'.
602
770
  additional_layers : dict, optional
603
771
  Additional layers to include in the anndata.AnnData object. The keys are the names of the layers and the values are the corresponding keys in the spatialproteomics object.
772
+ additional_obsm : dict, optional
773
+ Additional obsm data to include in the anndata.AnnData object. The keys are the names of the obsm data and the values are the corresponding keys in the spatialproteomics object.
604
774
  additional_uns : dict, optional
605
775
  Additional uns data to include in the anndata.AnnData object. The keys are the names of the uns data and the values are the corresponding keys in the spatialproteomics object.
606
776
 
@@ -612,7 +782,7 @@ class ToolAccessor:
612
782
  Raises
613
783
  ------
614
784
  AssertionError
615
- If the expression matrix key or additional layers are not found in the spatialproteomics object.
785
+ If additional layers, obsm entries, or uns entries are not found in the spatialproteomics object, or if additional layers or obsm entries have incompatible shapes.
616
786
 
617
787
  Notes
618
788
  -----
@@ -633,6 +803,11 @@ class ToolAccessor:
633
803
  for key, layer in additional_layers.items():
634
804
  # checking that the additional layer is present in the object
635
805
  assert layer in self._obj, f"Layer {layer} not found in the object."
806
+ # checking that layer has the same shape as the expression matrix
807
+ assert self._obj[layer].shape == expression_matrix.shape, (
808
+ f"Layer {layer} has shape {self._obj[layer].shape}, but AnnData layers must match "
809
+ f"the shape of the expression matrix {expression_matrix.shape}."
810
+ )
636
811
  adata.layers[key] = self._obj[layer].values
637
812
  adata.var_names = self._obj.coords[Dims.CHANNELS].values
638
813
 
@@ -661,9 +836,21 @@ class ToolAccessor:
661
836
  adata.obs[Features.Y] = adata.obs[Features.Y].astype(float)
662
837
  adata.obsm["spatial"] = np.array(adata.obs[[Features.X, Features.Y]])
663
838
 
839
+ if additional_obsm:
840
+ for key, layer in additional_obsm.items():
841
+ # checking that the additional obsm source layer is present in the object
842
+ assert layer in self._obj, f"Layer {layer} not found in the object."
843
+ # checking that the additional obsm layer has the same number of observations as the expression matrix
844
+ assert self._obj[layer].shape[0] == expression_matrix.shape[0], (
845
+ f"Layer {layer} has {self._obj[layer].shape[0]} observations, but the expression "
846
+ f"matrix has {expression_matrix.shape[0]} observations. AnnData obsm entries must match "
847
+ "the number of observations in the expression matrix."
848
+ )
849
+ adata.obsm[key] = self._obj[layer].values
850
+
664
851
  if additional_uns:
665
852
  for key, layer in additional_uns.items():
666
- # checking that the additional layer is present in the object
853
+ # checking that the additional uns source layer is present in the object
667
854
  assert layer in self._obj, f"Layer {layer} not found in the object."
668
855
  adata.uns[key] = self._obj.pp.get_layer_as_df(layer)
669
856
 
@@ -12,7 +12,7 @@ from ..pp.utils import _normalize
12
12
 
13
13
 
14
14
  def _get_channels(obj, key_added, channel):
15
- if key_added in obj:
15
+ if key_added is not None and key_added in obj:
16
16
  raise KeyError(f'The key "{key_added}" already exists. Please choose another key.')
17
17
 
18
18
  if channel is not None:
@@ -248,6 +248,43 @@ def _astir(
248
248
  return assigned_cell_types
249
249
 
250
250
 
251
+ def _spotiflow(
252
+ img,
253
+ channel_names,
254
+ pretrained_name: str = "general",
255
+ model_kwargs: dict = None,
256
+ predict_kwargs: dict = None,
257
+ ):
258
+ model_kwargs = model_kwargs or {}
259
+ predict_kwargs = predict_kwargs or {}
260
+
261
+ from spotiflow.model import Spotiflow
262
+
263
+ if img.ndim not in [2, 3]:
264
+ raise ValueError(f"Input image must be 2D or 3D, got {img.ndim}.")
265
+
266
+ if img.ndim == 2:
267
+ img = img[np.newaxis, :, :]
268
+ channel_names = [channel_names] if np.isscalar(channel_names) else channel_names
269
+
270
+ assert img.shape[0] == len(channel_names), "Number of channels in image does not match number of channel names."
271
+
272
+ model = Spotiflow.from_pretrained(pretrained_name, **model_kwargs)
273
+
274
+ dfs = []
275
+ for ch_idx, ch_name in enumerate(channel_names):
276
+ single_image = img[ch_idx]
277
+ points, _ = model.predict(single_image, **predict_kwargs)
278
+ # spotiflow returns points as (row, col) == (y, x)
279
+ df = pd.DataFrame(points, columns=["y", "x"])
280
+ df["channel"] = ch_name
281
+ dfs.append(df)
282
+
283
+ df = pd.concat(dfs, ignore_index=True)
284
+ df.index.name = "points"
285
+ return df
286
+
287
+
251
288
  def _compute_transformation(x_coords, y_coords):
252
289
  from spatialdata.transformations import Affine
253
290
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spatialproteomics
3
- Version: 0.8.4
3
+ Version: 0.8.6
4
4
  Summary: Spatialproteomics is an interoperable toolbox for analyzing highly multiplexed fluorescence image data
5
5
  Author: Matthias Meyer-Bender, Harald Vohringer
6
6
  Requires-Python: >=3.10
@@ -33,6 +33,8 @@ Provides-Extra: anndata
33
33
  Requires-Dist: anndata; extra == "anndata"
34
34
  Provides-Extra: astir
35
35
  Requires-Dist: astir>=0.1.5; extra == "astir"
36
+ Provides-Extra: spotiflow
37
+ Requires-Dist: spotiflow; extra == "spotiflow"
36
38
  Provides-Extra: dev
37
39
  Requires-Dist: pytest>=7.1.2; extra == "dev"
38
40
  Requires-Dist: pytest-cov>=3.0.0; extra == "dev"
@@ -41,6 +43,7 @@ Requires-Dist: isort>=5.10.1; extra == "dev"
41
43
  Requires-Dist: flake8>=5.0.4; extra == "dev"
42
44
  Requires-Dist: bandit>=1.7.4; extra == "dev"
43
45
  Requires-Dist: safety>=2.1.1; extra == "dev"
46
+ Requires-Dist: typing_extensions>=4.16; extra == "dev"
44
47
  Provides-Extra: all
45
48
  Requires-Dist: spatialdata; extra == "all"
46
49
  Requires-Dist: cellpose; extra == "all"
@@ -60,7 +63,7 @@ Dynamic: license-file
60
63
 
61
64
  ## Principles
62
65
 
63
- Multiplexed imaging data comprises at least 3 dimensions (i.e. `channels`, `x`, and `y`) and has often additional data such as segmentation masks or cell type annotations associated with it. In `spatialproteomics`, we use `xarray` to create a data structure that keeps all of these data dimension in sync. This data structure can then be used to apply all sorts of operations to the data. Users can segment cells, perform different image processing steps, quantify protein expression, predict cell types, and plot their data in various ways. By providing researchers with those tools, `spatialproteomics` can be used to quickly explore highly multiplexed spatial proteomics data directly within jupyter notebooks.
66
+ Multiplexed imaging data comprises at least 3 dimensions (i.e. `channels`, `x`, and `y`) and often contains additional data such as segmentation masks or cell type annotations associated with it. In `spatialproteomics`, we use `xarray` to create a data structure that keeps all of these data dimension in sync. This data structure can then be used to apply all sorts of operations to the data. Users can segment cells, perform different image processing steps, quantify protein expression, predict cell types, and plot their data in various ways. By providing researchers with those tools, `spatialproteomics` can be used to quickly explore highly multiplexed spatial proteomics data directly within jupyter notebooks.
64
67
 
65
68
  <p align="center" width="100%">
66
69
  <img src="docs/_static/img/supplementary_figure_1.png" alt="The spatialproteomics data structure enables synchronized subsetting across shared dimensions." style="width:70%;">
@@ -97,8 +100,4 @@ matplotlib
97
100
  ```
98
101
 
99
102
  ## Citation
100
- Spatialproteomics - an interoperable toolbox for analyzing highly multiplexed fluorescence image data
101
-
102
- Matthias Fabian Meyer-Bender, Harald Sager Voehringer, Christina Schniederjohann, Sarah Patricia Koziel, Erin Kim Chung, Ekaterina Popova, Alexander Brobeil, Lisa-Maria Held, Aamir Munir, Scverse Community, Sascha Dietrich, Peter-Martin Bruch, Wolfgang Huber
103
-
104
- bioRxiv 2025.04.29.651202; doi: https://doi.org/10.1101/2025.04.29.651202
103
+ > Meyer-Bender, M., Voehringer, H., Schniederjohann, C., Koziel, S., Chung, E., Popova, E., ... & Huber, W. (2026). Spatialproteomics: an interoperable toolbox for analyzing highly multiplexed fluorescence image data. Nature Methods, 1-10.
@@ -32,6 +32,7 @@ isort>=5.10.1
32
32
  flake8>=5.0.4
33
33
  bandit>=1.7.4
34
34
  safety>=2.1.1
35
+ typing_extensions>=4.16
35
36
 
36
37
  [docs]
37
38
  Sphinx>=7.0.0
@@ -44,5 +45,8 @@ IPython>=8.0.0
44
45
  [spatialdata]
45
46
  spatialdata>=0.4.0
46
47
 
48
+ [spotiflow]
49
+ spotiflow
50
+
47
51
  [stardist]
48
52
  stardist>=0.9.1