spatialproteomics 0.7.10__tar.gz → 0.8.1__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.7.10/spatialproteomics.egg-info → spatialproteomics-0.8.1}/PKG-INFO +1 -1
  2. {spatialproteomics-0.7.10 → spatialproteomics-0.8.1}/pyproject.toml +1 -1
  3. {spatialproteomics-0.7.10 → spatialproteomics-0.8.1}/spatialproteomics/pl/plot.py +4 -0
  4. {spatialproteomics-0.7.10 → spatialproteomics-0.8.1}/spatialproteomics/pl/utils.py +3 -0
  5. {spatialproteomics-0.7.10 → spatialproteomics-0.8.1}/spatialproteomics/pp/preprocessing.py +59 -26
  6. {spatialproteomics-0.7.10 → spatialproteomics-0.8.1}/spatialproteomics/pp/utils.py +104 -22
  7. {spatialproteomics-0.7.10 → spatialproteomics-0.8.1/spatialproteomics.egg-info}/PKG-INFO +1 -1
  8. {spatialproteomics-0.7.10 → spatialproteomics-0.8.1}/LICENSE +0 -0
  9. {spatialproteomics-0.7.10 → spatialproteomics-0.8.1}/README.md +0 -0
  10. {spatialproteomics-0.7.10 → spatialproteomics-0.8.1}/setup.cfg +0 -0
  11. {spatialproteomics-0.7.10 → spatialproteomics-0.8.1}/spatialproteomics/__init__.py +0 -0
  12. {spatialproteomics-0.7.10 → spatialproteomics-0.8.1}/spatialproteomics/base_logger.py +0 -0
  13. {spatialproteomics-0.7.10 → spatialproteomics-0.8.1}/spatialproteomics/constants.py +0 -0
  14. {spatialproteomics-0.7.10 → spatialproteomics-0.8.1}/spatialproteomics/container.py +0 -0
  15. {spatialproteomics-0.7.10 → spatialproteomics-0.8.1}/spatialproteomics/image_container/ImageContainer.py +0 -0
  16. {spatialproteomics-0.7.10 → spatialproteomics-0.8.1}/spatialproteomics/image_container/__init__.py +0 -0
  17. {spatialproteomics-0.7.10 → spatialproteomics-0.8.1}/spatialproteomics/la/__init__.py +0 -0
  18. {spatialproteomics-0.7.10 → spatialproteomics-0.8.1}/spatialproteomics/la/label.py +0 -0
  19. {spatialproteomics-0.7.10 → spatialproteomics-0.8.1}/spatialproteomics/la/utils.py +0 -0
  20. {spatialproteomics-0.7.10 → spatialproteomics-0.8.1}/spatialproteomics/nh/__init__.py +0 -0
  21. {spatialproteomics-0.7.10 → spatialproteomics-0.8.1}/spatialproteomics/nh/neighborhood.py +0 -0
  22. {spatialproteomics-0.7.10 → spatialproteomics-0.8.1}/spatialproteomics/nh/utils.py +0 -0
  23. {spatialproteomics-0.7.10 → spatialproteomics-0.8.1}/spatialproteomics/pl/__init__.py +0 -0
  24. {spatialproteomics-0.7.10 → spatialproteomics-0.8.1}/spatialproteomics/pp/__init__.py +0 -0
  25. {spatialproteomics-0.7.10 → spatialproteomics-0.8.1}/spatialproteomics/pp/intensity.py +0 -0
  26. {spatialproteomics-0.7.10 → spatialproteomics-0.8.1}/spatialproteomics/sd/__init__.py +0 -0
  27. {spatialproteomics-0.7.10 → spatialproteomics-0.8.1}/spatialproteomics/sd/utils.py +0 -0
  28. {spatialproteomics-0.7.10 → spatialproteomics-0.8.1}/spatialproteomics/tl/__init__.py +0 -0
  29. {spatialproteomics-0.7.10 → spatialproteomics-0.8.1}/spatialproteomics/tl/tool.py +0 -0
  30. {spatialproteomics-0.7.10 → spatialproteomics-0.8.1}/spatialproteomics/tl/utils.py +0 -0
  31. {spatialproteomics-0.7.10 → spatialproteomics-0.8.1}/spatialproteomics.egg-info/SOURCES.txt +0 -0
  32. {spatialproteomics-0.7.10 → spatialproteomics-0.8.1}/spatialproteomics.egg-info/dependency_links.txt +0 -0
  33. {spatialproteomics-0.7.10 → spatialproteomics-0.8.1}/spatialproteomics.egg-info/requires.txt +0 -0
  34. {spatialproteomics-0.7.10 → spatialproteomics-0.8.1}/spatialproteomics.egg-info/top_level.txt +0 -0
  35. {spatialproteomics-0.7.10 → spatialproteomics-0.8.1}/tests/test_container.py +0 -0
  36. {spatialproteomics-0.7.10 → spatialproteomics-0.8.1}/tests/test_integration.py +0 -0
  37. {spatialproteomics-0.7.10 → spatialproteomics-0.8.1}/tests/test_to_zarr.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spatialproteomics
3
- Version: 0.7.10
3
+ Version: 0.8.1
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "spatialproteomics"
3
- version = "0.7.10"
3
+ version = "0.8.1"
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"
@@ -245,6 +245,10 @@ class PlotAccessor:
245
245
  min_val = obs_colors["min"]
246
246
  max_val = obs_colors["max"]
247
247
 
248
+ # handling the special case where all values in the segmentation are the same,
249
+ # which would lead to a division by zero in the normalization step
250
+ max_val = max_val if max_val > min_val else min_val + 1
251
+
248
252
  # Create the colorbar with dynamic positioning and size
249
253
  sm = plt.cm.ScalarMappable(cmap=cmap, norm=plt.Normalize(vmin=min_val, vmax=max_val))
250
254
  sm.set_array([])
@@ -219,6 +219,9 @@ def _render_obs(
219
219
  vmin: float = None,
220
220
  vmax: float = None,
221
221
  ) -> np.ndarray:
222
+ # handling the special case where all values in the segmentation are the same,
223
+ # which would lead to a division by zero in the normalization step
224
+ vmax = vmax if vmax > vmin else vmin + 1
222
225
  # normalize the segmentation to be in the range [0, 1]
223
226
  segmentation = (segmentation - vmin) / (vmax - vmin)
224
227
 
@@ -313,6 +313,7 @@ def filter_by_obs(
313
313
  func: Callable,
314
314
  segmentation_key: str = SDLayers.SEGMENTATION,
315
315
  table_key: str = SDLayers.TABLE,
316
+ reindex: bool = False,
316
317
  copy: bool = False,
317
318
  ):
318
319
  """
@@ -324,6 +325,7 @@ def filter_by_obs(
324
325
  func (Callable): A filtering function that takes in the values of the feature and returns a boolean array.
325
326
  segmentation_key (str): The key of the segmentation mask in the object. Default is SDLayers.SEGMENTATION.
326
327
  table_key (str): The key of the table in the object. Default is SDLayers.TABLE.
328
+ reindex (bool): Whether to reindex the obs after filtering. Default is False.
327
329
  copy (bool): If True, a copy of the object is returned. Default is False.
328
330
  """
329
331
  import spatialdata
@@ -347,13 +349,15 @@ def filter_by_obs(
347
349
  # setting all cells that are not in cells to 0
348
350
  segmentation = _remove_unlabeled_cells(segmentation, cells_sel)
349
351
  # relabeling cells in the segmentation mask so the IDs go from 1 to n again
350
- segmentation, relabel_dict = _relabel_cells(segmentation)
352
+ if reindex:
353
+ segmentation, relabel_dict = _relabel_cells(segmentation)
351
354
 
352
355
  # removing the cells which are not in cells_sel
353
356
  adata = adata[adata.obs[SDFeatures.ID].isin(cells_sel), :].copy()
354
357
  # updating the cell coords of the object
355
- adata.obs[SDFeatures.ID] = [relabel_dict[cell] for cell in adata.obs[SDFeatures.ID].values]
356
- adata.obs.index = [f"Cell_{x}" for x in adata.obs[SDFeatures.ID].values]
358
+ if reindex:
359
+ adata.obs[SDFeatures.ID] = [relabel_dict[cell] for cell in adata.obs[SDFeatures.ID].values]
360
+ adata.obs.index = [f"Cell_{x}" for x in adata.obs[SDFeatures.ID].values]
357
361
 
358
362
  # overwriting the segmentation mask in the object
359
363
  # get transformations
@@ -822,7 +826,7 @@ class PreprocessingAccessor:
822
826
  def add_segmentation(
823
827
  self,
824
828
  segmentation: Union[str, np.ndarray] = None,
825
- reindex: bool = True,
829
+ reindex: bool = False,
826
830
  keep_labels: bool = True,
827
831
  add_obs: bool = True,
828
832
  ) -> xr.Dataset:
@@ -1593,7 +1597,9 @@ class PreprocessingAccessor:
1593
1597
 
1594
1598
  return xr.merge([obj, new_img, new_seg], join="outer", compat="no_conflicts")
1595
1599
 
1596
- def filter_by_obs(self, col: str, func: Callable, segmentation_key: str = Layers.SEGMENTATION):
1600
+ def filter_by_obs(
1601
+ self, col: str, func: Callable, segmentation_key: str = Layers.SEGMENTATION, reindex: bool = False
1602
+ ):
1597
1603
  """
1598
1604
  Filter the object by observations based on a given feature and filtering function.
1599
1605
 
@@ -1601,6 +1607,7 @@ class PreprocessingAccessor:
1601
1607
  col (str): The name of the feature to filter by.
1602
1608
  func (Callable): A filtering function that takes in the values of the feature and returns a boolean array.
1603
1609
  segmentation_key (str): The key of the segmentation mask in the object. Default is Layers.SEGMENTATION.
1610
+ reindex (bool): Whether to reindex the cell IDs after filtering. Default is False.
1604
1611
 
1605
1612
  Returns:
1606
1613
  xr.Dataset: The filtered object with the selected cells and updated segmentation mask.
@@ -1610,7 +1617,6 @@ class PreprocessingAccessor:
1610
1617
 
1611
1618
  Notes:
1612
1619
  - This method filters the object by selecting only the cells that satisfy the filtering condition.
1613
- - It also updates the segmentation mask to remove cells that are not selected and relabels the remaining cells.
1614
1620
 
1615
1621
  Example:
1616
1622
  To filter the object by the feature "area" and keep only the cells with an area greater than 70px:
@@ -1639,10 +1645,11 @@ class PreprocessingAccessor:
1639
1645
  segmentation = obj[segmentation_key].values
1640
1646
  # setting all cells that are not in cells to 0
1641
1647
  segmentation = _remove_unlabeled_cells(segmentation, cells_sel)
1642
- # relabeling cells in the segmentation mask so the IDs go from 1 to n again
1643
- segmentation, relabel_dict = _relabel_cells(segmentation)
1644
- # updating the cell coords of the object
1645
- obj.coords[Dims.CELLS] = [relabel_dict[cell] for cell in obj.coords[Dims.CELLS].values]
1648
+ if reindex:
1649
+ # relabeling cells in the segmentation mask so the IDs go from 1 to n again
1650
+ segmentation, relabel_dict = _relabel_cells(segmentation)
1651
+ # updating the cell coords of the object
1652
+ obj.coords[Dims.CELLS] = [relabel_dict[cell] for cell in obj.coords[Dims.CELLS].values]
1646
1653
 
1647
1654
  # creating a data array with the segmentation mask, so that we can merge it to the original
1648
1655
  da = xr.DataArray(
@@ -1659,7 +1666,11 @@ class PreprocessingAccessor:
1659
1666
  return xr.merge([obj, da], join="outer", compat="no_conflicts")
1660
1667
 
1661
1668
  def remove_outlying_cells(
1662
- self, dilation_size: int = 25, threshold: int = 5, segmentation_key: str = Layers.SEGMENTATION
1669
+ self,
1670
+ dilation_size: int = 25,
1671
+ threshold: int = 5,
1672
+ segmentation_key: str = Layers.SEGMENTATION,
1673
+ reindex: bool = False,
1663
1674
  ):
1664
1675
  """
1665
1676
  Removes outlying cells from the image container. It does so by dilating the segmentation mask and removing cells that belong to a connected component with less than 'threshold' cells.
@@ -1672,6 +1683,8 @@ class PreprocessingAccessor:
1672
1683
  The minimum number of cells in a connected component required for the cells to be kept. Default is 5.
1673
1684
  segmentation_key : str
1674
1685
  The key of the segmentation mask in the object. Default is '_segmentation'.
1686
+ reindex : bool
1687
+ Whether to reindex the cell IDs after filtering. Default is False.
1675
1688
 
1676
1689
  Returns
1677
1690
  -------
@@ -1703,10 +1716,11 @@ class PreprocessingAccessor:
1703
1716
  obj = self._obj.sel(query)
1704
1717
  # setting all cells that are not in cells to 0
1705
1718
  segmentation = _remove_unlabeled_cells(segmentation, cells_sel)
1706
- # relabeling cells in the segmentation mask so the IDs go from 1 to n again
1707
- segmentation, relabel_dict = _relabel_cells(segmentation)
1708
- # updating the cell coords of the object
1709
- obj.coords[Dims.CELLS] = [relabel_dict[cell] for cell in obj.coords[Dims.CELLS].values]
1719
+ if reindex:
1720
+ # relabeling cells in the segmentation mask so the IDs go from 1 to n again
1721
+ segmentation, relabel_dict = _relabel_cells(segmentation)
1722
+ # updating the cell coords of the object
1723
+ obj.coords[Dims.CELLS] = [relabel_dict[cell] for cell in obj.coords[Dims.CELLS].values]
1710
1724
 
1711
1725
  # creating a data array with the segmentation mask, so that we can merge it to the original
1712
1726
  da = xr.DataArray(
@@ -1785,12 +1799,21 @@ class PreprocessingAccessor:
1785
1799
  layer_key: str,
1786
1800
  key_added: str = "_merged_segmentation",
1787
1801
  labels: Optional[List[str]] = None,
1788
- threshold: float = 0.8,
1802
+ threshold1: float = 0.7,
1803
+ threshold2: float = 0.8,
1789
1804
  ):
1790
1805
  """
1791
- Merge segmentation masks.
1792
- This can be done in two ways: either by merging a multi-dimensional array from the object directly, or by adding a numpy array.
1793
- You can either just merge a multi-dimensional array, or merge to an existing 1D mask (e. g. a precomputed DAPI segmentation).
1806
+ Merge segmentation masks. Merging works as follows:
1807
+ 1. Assume two segmentation masks, S1 (higher priority, e.g., macrophages) and S2 (lower priority, e.g., lymphocytes)
1808
+ 2. We start by copying the segmentation regions from S2 into S1, but only for pixels where S1 has background
1809
+ 3. Some S2 cells may now be partially overwritten by S1 cells — we drop any S2 cell where less than threshold1
1810
+ fraction of its original area survived (e.g. with threshold1=0.7,
1811
+ an S2 cell that lost more than 30% of its area to S1 is discarded)
1812
+ 4. Of the remaining S2 cells, we additionally drop any that overlap with threshold2 or
1813
+ more of any single S1 cell's area — this prevents S2 cells from being retained when they
1814
+ largely encapsulate an S1 cell (e.g. with threshold2=0.8, an S2 cell is discarded if it
1815
+ covers more than 80% of any S1 cell)
1816
+ 5. The surviving S1 and S2 regions are combined into a final mask with sequential labels
1794
1817
 
1795
1818
  Parameters
1796
1819
  ----------
@@ -1800,7 +1823,9 @@ class PreprocessingAccessor:
1800
1823
  The name of the new segmentation mask to be added to the xarray object. Default is "_merged_segmentation".
1801
1824
  labels : Optional[List[str]]
1802
1825
  Optional. Labels corresponding to each segmentation mask. If provided, must match number of arrays.
1803
- threshold : float
1826
+ threshold1: float
1827
+ Optional. Threshold for merging cells. Default is 0.7.
1828
+ threshold2: float
1804
1829
  Optional. Threshold for merging cells. Default is 0.8.
1805
1830
 
1806
1831
  Returns
@@ -1820,6 +1845,9 @@ class PreprocessingAccessor:
1820
1845
  - The merging process starts with merging the biggest cells first, then the smaller ones.
1821
1846
  - Disconnected cells in the input are handled based on the specified method.
1822
1847
  """
1848
+ assert threshold1 >= 0 and threshold1 <= 1, "threshold1 must be between 0 and 1."
1849
+ assert threshold2 >= 0 and threshold2 <= 1, "threshold2 must be between 0 and 1."
1850
+
1823
1851
  # Make sure layer_key is a list internally
1824
1852
  if isinstance(layer_key, str):
1825
1853
  layer_keys = [layer_key]
@@ -1899,7 +1927,8 @@ class PreprocessingAccessor:
1899
1927
  next_segmentation.squeeze(),
1900
1928
  label1=label_1,
1901
1929
  label2=label_2,
1902
- threshold=threshold,
1930
+ threshold1=threshold1,
1931
+ threshold2=threshold2,
1903
1932
  )
1904
1933
 
1905
1934
  # Update label mapping
@@ -2082,13 +2111,16 @@ class PreprocessingAccessor:
2082
2111
 
2083
2112
  return xr.merge([obj, da], join="outer", compat="no_conflicts")
2084
2113
 
2085
- def mask_cells(self, mask_key: str = Layers.MASK, segmentation_key=Layers.SEGMENTATION) -> xr.Dataset:
2114
+ def mask_cells(
2115
+ self, mask_key: str = Layers.MASK, segmentation_key=Layers.SEGMENTATION, reindex: bool = False
2116
+ ) -> xr.Dataset:
2086
2117
  """
2087
2118
  Mask cells in the segmentation mask.
2088
2119
 
2089
2120
  Parameters:
2090
2121
  mask_key (str): The key of the mask to use for masking.
2091
2122
  segmentation_key (str): The key of the segmentation mask in the object. Default is Layers.SEGMENTATION.
2123
+ reindex (bool): Whether to reindex the cell IDs after filtering. Default is False.
2092
2124
 
2093
2125
  Returns:
2094
2126
  xr.Dataset: The object with the masked cells in the segmentation mask.
@@ -2117,10 +2149,11 @@ class PreprocessingAccessor:
2117
2149
  segmentation = obj[segmentation_key].values
2118
2150
  # setting all cells that are not in cells to 0
2119
2151
  segmentation = _remove_unlabeled_cells(segmentation, cells_sel)
2120
- # relabeling cells in the segmentation mask so the IDs go from 1 to n again
2121
- segmentation, relabel_dict = _relabel_cells(segmentation)
2122
- # updating the cell coords of the object
2123
- obj.coords[Dims.CELLS] = [relabel_dict[cell] for cell in obj.coords["cells"].values]
2152
+ if reindex:
2153
+ # relabeling cells in the segmentation mask so the IDs go from 1 to n again
2154
+ segmentation, relabel_dict = _relabel_cells(segmentation)
2155
+ # updating the cell coords of the object
2156
+ obj.coords[Dims.CELLS] = [relabel_dict[cell] for cell in obj.coords["cells"].values]
2124
2157
 
2125
2158
  # creating a data array with the segmentation mask, so that we can merge it to the original
2126
2159
  da = xr.DataArray(
@@ -95,62 +95,144 @@ def _relabel_cells(segmentation: np.ndarray) -> Tuple[np.ndarray, dict]:
95
95
  return segmentation_relabeled, value_map
96
96
 
97
97
 
98
- def _merge_segmentation(s1: np.ndarray, s2: np.ndarray, label1: int = 1, label2: int = 2, threshold: float = 1.0):
98
+ def _exceeds_overlap(i2: np.ndarray, s1: np.ndarray, n2: np.ndarray, p1: list, threshold2: float) -> np.ndarray:
99
+ """
100
+ Return a boolean mask over i2 indicating which s2 regions should be KEPT
101
+ (i.e. do not exceed the overlap threshold with any s1 cell).
102
+
103
+ Instead of looping per s2 label, this builds a joint overlap table in one pass
104
+ over the image, then checks the threshold condition vectorized.
105
+
106
+ Parameters
107
+ ----------
108
+ i2 : np.ndarray
109
+ Array of candidate s2 labels (already filtered by threshold1).
110
+ s1 : np.ndarray
111
+ First segmentation mask.
112
+ n2 : np.ndarray
113
+ Relabeled second segmentation mask.
114
+ p1 : list
115
+ Region properties of s1 (from skimage.measure.regionprops).
116
+ threshold2 : float
117
+ Maximum fraction of any s1 cell's area that may overlap with an s2 region
118
+ before that s2 region is excluded.
119
+ """
120
+ # Only consider pixels where both masks are non-zero
121
+ both = (s1 > 0) & (n2 > 0)
122
+ s1_vals = s1[both]
123
+ n2_vals = n2[both]
124
+
125
+ # Precompute s1 cell areas from regionprops
126
+ s1_areas = {p.label: p.area for p in p1}
127
+
128
+ # Count overlapping pixels for each (s1_label, s2_label) pair
129
+ pairs, counts = np.unique(np.stack([s1_vals, n2_vals], axis=1), axis=0, return_counts=True)
130
+
131
+ # Filter to only pairs where the s2 label is a candidate in i2
132
+ i2_set = set(i2.tolist())
133
+ mask = np.array([p[1] in i2_set for p in pairs])
134
+ pairs, counts = pairs[mask], counts[mask]
135
+
136
+ # Compute overlap fraction: intersection / s1 cell area
137
+ s1_areas_arr = np.array([s1_areas[p[0]] for p in pairs])
138
+ fractions = counts / s1_areas_arr
139
+
140
+ # Find s2 labels where any s1 cell exceeds the threshold
141
+ exceeds = pairs[fractions >= threshold2, 1]
142
+ exceeded_set = set(exceeds.tolist())
143
+
144
+ return np.array([label for label in i2 if label not in exceeded_set])
145
+
146
+
147
+ def _merge_segmentation(
148
+ s1: np.ndarray,
149
+ s2: np.ndarray,
150
+ label1: int = 1,
151
+ label2: int = 2,
152
+ threshold1: float = 1.0,
153
+ threshold2: float = 1.0,
154
+ ) -> Tuple[np.ndarray, dict]:
99
155
  """
100
156
  Merge two segmentation masks based on specified criteria.
101
157
 
158
+ Regions from s1 are always included (subject to threshold1). Regions from s2 are only
159
+ included if they do not overlap with s1 beyond the threshold1 and threshold2 criteria.
160
+
102
161
  Parameters
103
162
  ----------
104
163
  s1 : numpy.ndarray
105
- First segmentation mask.
164
+ First segmentation mask. Regions here take priority over s2.
106
165
  s2 : numpy.ndarray
107
- Second segmentation mask.
166
+ Second segmentation mask. Regions are only added where they don't conflict with s1.
108
167
  label1 : int
109
- Label for regions from the first mask in the final merged mask. Default is 1.
168
+ Label assigned to s1 regions in the output mapping. Default is 1.
110
169
  label2 : int
111
- Label for regions from the second mask in the final merged mask. Default is 2.
112
- threshold : float, optional
113
- Threshold for area ratio of intersection over union for merging regions.
114
- Default is 1.0, meaning all regions from the second mask are merged.
115
-
170
+ Label assigned to s2 regions in the output mapping. Default is 2.
171
+ threshold1 : float, optional
172
+ Minimum fraction of an s2 region's area that must be preserved (i.e. not masked by s1)
173
+ for it to be included. Default is 1.0, meaning only fully non-overlapping s2 regions
174
+ are included.
175
+ threshold2 : float, optional
176
+ Maximum fraction of any s1 cell's area that may overlap with an s2 region before
177
+ that s2 region is excluded. Default is 1.0, meaning an s2 region is only excluded
178
+ if it fully contains an s1 cell.
116
179
  Returns
117
180
  -------
118
181
  numpy.ndarray
119
- Merged segmentation mask.
182
+ Merged segmentation mask with sequentially relabeled regions.
120
183
  dict
121
- Mapping of labels from the merged mask to the original labels.
122
-
184
+ Mapping of labels in the final mask to their origin: label1 (from s1) or label2 (from s2).
123
185
  Notes
124
186
  -----
125
- This function assumes that `s1` and `s2` are 2D segmentation masks with integer labels.
187
+ This function assumes that s1 and s2 are 2D segmentation masks with integer labels,
188
+ where 0 represents background.
126
189
  """
127
190
  s1 = s1.squeeze()
128
191
  s2 = s2.squeeze()
129
192
 
130
- n2, fmap, bmap = relabel_sequential(s2, offset=s1.max() + 1)
193
+ # Relabel s2 so its labels don't collide with s1's labels
194
+ n2, fmap, _ = relabel_sequential(s2, offset=s1.max() + 1)
195
+
196
+ # Build a combined mask: start with s1, then fill background pixels with s2 regions.
197
+ # This means s1 always takes priority — s2 only fills where s1 has no label.
131
198
  s3 = s1.copy()
132
199
  s3[np.logical_and(n2 > 0, s1 == 0)] = n2[np.logical_and(n2 > 0, s1 == 0)]
133
200
 
201
+ # Collect region properties and labels for filtering
134
202
  p1 = regionprops(s1)
135
203
  p2 = regionprops(n2)
136
204
  p3 = regionprops(s3)
137
-
138
205
  l1 = [p.label for p in p1]
139
206
  l2 = [p.label for p in p2]
140
- l3 = [p.label for p in p3]
207
+ l3 = [p.label for p in p3] # labels still present after combining s1 and s2
141
208
 
142
- # compute intersections
209
+ # i1: s1 regions that survived into the combined mask (should always be all of them)
210
+ # i2: s2 regions that survived into the combined mask (may be partial if they overlapped with s1)
143
211
  i1 = list(set(l1) & set(l3))
144
212
  i2 = list(set(l2) & set(l3))
145
213
 
214
+ # threshold1 filter: only keep s2 regions where the surviving area (in s3) is at least
215
+ # threshold1 fraction of the original s2 area. Regions heavily masked by s1 are dropped.
146
216
  area_ratio = np.array([p.area for p in p3 if p.label in i2]) / np.array([p.area for p in p2 if p.label in i2])
147
- i2 = np.array(i2)[area_ratio >= threshold]
148
- selected_cells = np.concatenate([np.array(i1), i2])
217
+ i2 = np.array(i2)[area_ratio >= threshold1]
218
+
219
+ # threshold2 filter: exclude s2 regions that overlap too much with any single s1 cell.
220
+ # If an s2 region covers >= threshold2 of an s1 cell's area, it's considered conflicting
221
+ # and dropped — even if it passed the threshold1 check.
222
+ i2 = _exceeds_overlap(i2, s1, n2, p1, threshold2)
149
223
 
150
- # make final mask
224
+ # Combine selected s1 and s2 region labels, then clean and relabel the mask
225
+ selected_cells = np.concatenate([np.array(i1), i2])
151
226
  clean_mask = _remove_unlabeled_cells(s3, selected_cells)
152
- final_mask, fmap, bmap = relabel_sequential(clean_mask)
153
- mapping = dict(zip([fmap[i] for i in selected_cells.astype(int)], [label1] * len(i1) + [label2] * len(i2)))
227
+ final_mask, fmap, _ = relabel_sequential(clean_mask)
228
+
229
+ # Build a mapping from final mask labels back to their origin (label1 or label2)
230
+ mapping = dict(
231
+ zip(
232
+ [fmap[i] for i in selected_cells.astype(int)],
233
+ [label1] * len(i1) + [label2] * len(i2),
234
+ )
235
+ )
154
236
 
155
237
  return final_mask, mapping
156
238
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spatialproteomics
3
- Version: 0.7.10
3
+ Version: 0.8.1
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