spatialproteomics 0.8.0__tar.gz → 0.8.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. {spatialproteomics-0.8.0/spatialproteomics.egg-info → spatialproteomics-0.8.2}/PKG-INFO +1 -1
  2. {spatialproteomics-0.8.0 → spatialproteomics-0.8.2}/pyproject.toml +1 -1
  3. {spatialproteomics-0.8.0 → spatialproteomics-0.8.2}/spatialproteomics/pp/preprocessing.py +21 -6
  4. {spatialproteomics-0.8.0 → spatialproteomics-0.8.2}/spatialproteomics/pp/utils.py +104 -22
  5. {spatialproteomics-0.8.0 → spatialproteomics-0.8.2/spatialproteomics.egg-info}/PKG-INFO +1 -1
  6. {spatialproteomics-0.8.0 → spatialproteomics-0.8.2}/LICENSE +0 -0
  7. {spatialproteomics-0.8.0 → spatialproteomics-0.8.2}/README.md +0 -0
  8. {spatialproteomics-0.8.0 → spatialproteomics-0.8.2}/setup.cfg +0 -0
  9. {spatialproteomics-0.8.0 → spatialproteomics-0.8.2}/spatialproteomics/__init__.py +0 -0
  10. {spatialproteomics-0.8.0 → spatialproteomics-0.8.2}/spatialproteomics/base_logger.py +0 -0
  11. {spatialproteomics-0.8.0 → spatialproteomics-0.8.2}/spatialproteomics/constants.py +0 -0
  12. {spatialproteomics-0.8.0 → spatialproteomics-0.8.2}/spatialproteomics/container.py +0 -0
  13. {spatialproteomics-0.8.0 → spatialproteomics-0.8.2}/spatialproteomics/image_container/ImageContainer.py +0 -0
  14. {spatialproteomics-0.8.0 → spatialproteomics-0.8.2}/spatialproteomics/image_container/__init__.py +0 -0
  15. {spatialproteomics-0.8.0 → spatialproteomics-0.8.2}/spatialproteomics/la/__init__.py +0 -0
  16. {spatialproteomics-0.8.0 → spatialproteomics-0.8.2}/spatialproteomics/la/label.py +0 -0
  17. {spatialproteomics-0.8.0 → spatialproteomics-0.8.2}/spatialproteomics/la/utils.py +0 -0
  18. {spatialproteomics-0.8.0 → spatialproteomics-0.8.2}/spatialproteomics/nh/__init__.py +0 -0
  19. {spatialproteomics-0.8.0 → spatialproteomics-0.8.2}/spatialproteomics/nh/neighborhood.py +0 -0
  20. {spatialproteomics-0.8.0 → spatialproteomics-0.8.2}/spatialproteomics/nh/utils.py +0 -0
  21. {spatialproteomics-0.8.0 → spatialproteomics-0.8.2}/spatialproteomics/pl/__init__.py +0 -0
  22. {spatialproteomics-0.8.0 → spatialproteomics-0.8.2}/spatialproteomics/pl/plot.py +0 -0
  23. {spatialproteomics-0.8.0 → spatialproteomics-0.8.2}/spatialproteomics/pl/utils.py +0 -0
  24. {spatialproteomics-0.8.0 → spatialproteomics-0.8.2}/spatialproteomics/pp/__init__.py +0 -0
  25. {spatialproteomics-0.8.0 → spatialproteomics-0.8.2}/spatialproteomics/pp/intensity.py +0 -0
  26. {spatialproteomics-0.8.0 → spatialproteomics-0.8.2}/spatialproteomics/sd/__init__.py +0 -0
  27. {spatialproteomics-0.8.0 → spatialproteomics-0.8.2}/spatialproteomics/sd/utils.py +0 -0
  28. {spatialproteomics-0.8.0 → spatialproteomics-0.8.2}/spatialproteomics/tl/__init__.py +0 -0
  29. {spatialproteomics-0.8.0 → spatialproteomics-0.8.2}/spatialproteomics/tl/tool.py +0 -0
  30. {spatialproteomics-0.8.0 → spatialproteomics-0.8.2}/spatialproteomics/tl/utils.py +0 -0
  31. {spatialproteomics-0.8.0 → spatialproteomics-0.8.2}/spatialproteomics.egg-info/SOURCES.txt +0 -0
  32. {spatialproteomics-0.8.0 → spatialproteomics-0.8.2}/spatialproteomics.egg-info/dependency_links.txt +0 -0
  33. {spatialproteomics-0.8.0 → spatialproteomics-0.8.2}/spatialproteomics.egg-info/requires.txt +0 -0
  34. {spatialproteomics-0.8.0 → spatialproteomics-0.8.2}/spatialproteomics.egg-info/top_level.txt +0 -0
  35. {spatialproteomics-0.8.0 → spatialproteomics-0.8.2}/tests/test_container.py +0 -0
  36. {spatialproteomics-0.8.0 → spatialproteomics-0.8.2}/tests/test_integration.py +0 -0
  37. {spatialproteomics-0.8.0 → spatialproteomics-0.8.2}/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.0
3
+ Version: 0.8.2
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.8.0"
3
+ version = "0.8.2"
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"
@@ -1799,12 +1799,21 @@ class PreprocessingAccessor:
1799
1799
  layer_key: str,
1800
1800
  key_added: str = "_merged_segmentation",
1801
1801
  labels: Optional[List[str]] = None,
1802
- threshold: float = 0.8,
1802
+ threshold1: float = 0.7,
1803
+ threshold2: float = 0.8,
1803
1804
  ):
1804
1805
  """
1805
- Merge segmentation masks.
1806
- This can be done in two ways: either by merging a multi-dimensional array from the object directly, or by adding a numpy array.
1807
- 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
1808
1817
 
1809
1818
  Parameters
1810
1819
  ----------
@@ -1814,7 +1823,9 @@ class PreprocessingAccessor:
1814
1823
  The name of the new segmentation mask to be added to the xarray object. Default is "_merged_segmentation".
1815
1824
  labels : Optional[List[str]]
1816
1825
  Optional. Labels corresponding to each segmentation mask. If provided, must match number of arrays.
1817
- threshold : float
1826
+ threshold1: float
1827
+ Optional. Threshold for merging cells. Default is 0.7.
1828
+ threshold2: float
1818
1829
  Optional. Threshold for merging cells. Default is 0.8.
1819
1830
 
1820
1831
  Returns
@@ -1834,6 +1845,9 @@ class PreprocessingAccessor:
1834
1845
  - The merging process starts with merging the biggest cells first, then the smaller ones.
1835
1846
  - Disconnected cells in the input are handled based on the specified method.
1836
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
+
1837
1851
  # Make sure layer_key is a list internally
1838
1852
  if isinstance(layer_key, str):
1839
1853
  layer_keys = [layer_key]
@@ -1913,7 +1927,8 @@ class PreprocessingAccessor:
1913
1927
  next_segmentation.squeeze(),
1914
1928
  label1=label_1,
1915
1929
  label2=label_2,
1916
- threshold=threshold,
1930
+ threshold1=threshold1,
1931
+ threshold2=threshold2,
1917
1932
  )
1918
1933
 
1919
1934
  # Update label mapping
@@ -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], dtype=bool)
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.8.0
3
+ Version: 0.8.2
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