spatialproteomics 0.7.9__tar.gz → 0.8.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.
Files changed (37) hide show
  1. {spatialproteomics-0.7.9/spatialproteomics.egg-info → spatialproteomics-0.8.0}/PKG-INFO +1 -1
  2. {spatialproteomics-0.7.9 → spatialproteomics-0.8.0}/pyproject.toml +1 -1
  3. {spatialproteomics-0.7.9 → spatialproteomics-0.8.0}/spatialproteomics/pl/plot.py +10 -6
  4. {spatialproteomics-0.7.9 → spatialproteomics-0.8.0}/spatialproteomics/pl/utils.py +7 -4
  5. {spatialproteomics-0.7.9 → spatialproteomics-0.8.0}/spatialproteomics/pp/preprocessing.py +145 -23
  6. {spatialproteomics-0.7.9 → spatialproteomics-0.8.0}/spatialproteomics/pp/utils.py +4 -4
  7. {spatialproteomics-0.7.9 → spatialproteomics-0.8.0/spatialproteomics.egg-info}/PKG-INFO +1 -1
  8. {spatialproteomics-0.7.9 → spatialproteomics-0.8.0}/LICENSE +0 -0
  9. {spatialproteomics-0.7.9 → spatialproteomics-0.8.0}/README.md +0 -0
  10. {spatialproteomics-0.7.9 → spatialproteomics-0.8.0}/setup.cfg +0 -0
  11. {spatialproteomics-0.7.9 → spatialproteomics-0.8.0}/spatialproteomics/__init__.py +0 -0
  12. {spatialproteomics-0.7.9 → spatialproteomics-0.8.0}/spatialproteomics/base_logger.py +0 -0
  13. {spatialproteomics-0.7.9 → spatialproteomics-0.8.0}/spatialproteomics/constants.py +0 -0
  14. {spatialproteomics-0.7.9 → spatialproteomics-0.8.0}/spatialproteomics/container.py +0 -0
  15. {spatialproteomics-0.7.9 → spatialproteomics-0.8.0}/spatialproteomics/image_container/ImageContainer.py +0 -0
  16. {spatialproteomics-0.7.9 → spatialproteomics-0.8.0}/spatialproteomics/image_container/__init__.py +0 -0
  17. {spatialproteomics-0.7.9 → spatialproteomics-0.8.0}/spatialproteomics/la/__init__.py +0 -0
  18. {spatialproteomics-0.7.9 → spatialproteomics-0.8.0}/spatialproteomics/la/label.py +0 -0
  19. {spatialproteomics-0.7.9 → spatialproteomics-0.8.0}/spatialproteomics/la/utils.py +0 -0
  20. {spatialproteomics-0.7.9 → spatialproteomics-0.8.0}/spatialproteomics/nh/__init__.py +0 -0
  21. {spatialproteomics-0.7.9 → spatialproteomics-0.8.0}/spatialproteomics/nh/neighborhood.py +0 -0
  22. {spatialproteomics-0.7.9 → spatialproteomics-0.8.0}/spatialproteomics/nh/utils.py +0 -0
  23. {spatialproteomics-0.7.9 → spatialproteomics-0.8.0}/spatialproteomics/pl/__init__.py +0 -0
  24. {spatialproteomics-0.7.9 → spatialproteomics-0.8.0}/spatialproteomics/pp/__init__.py +0 -0
  25. {spatialproteomics-0.7.9 → spatialproteomics-0.8.0}/spatialproteomics/pp/intensity.py +0 -0
  26. {spatialproteomics-0.7.9 → spatialproteomics-0.8.0}/spatialproteomics/sd/__init__.py +0 -0
  27. {spatialproteomics-0.7.9 → spatialproteomics-0.8.0}/spatialproteomics/sd/utils.py +0 -0
  28. {spatialproteomics-0.7.9 → spatialproteomics-0.8.0}/spatialproteomics/tl/__init__.py +0 -0
  29. {spatialproteomics-0.7.9 → spatialproteomics-0.8.0}/spatialproteomics/tl/tool.py +0 -0
  30. {spatialproteomics-0.7.9 → spatialproteomics-0.8.0}/spatialproteomics/tl/utils.py +0 -0
  31. {spatialproteomics-0.7.9 → spatialproteomics-0.8.0}/spatialproteomics.egg-info/SOURCES.txt +0 -0
  32. {spatialproteomics-0.7.9 → spatialproteomics-0.8.0}/spatialproteomics.egg-info/dependency_links.txt +0 -0
  33. {spatialproteomics-0.7.9 → spatialproteomics-0.8.0}/spatialproteomics.egg-info/requires.txt +0 -0
  34. {spatialproteomics-0.7.9 → spatialproteomics-0.8.0}/spatialproteomics.egg-info/top_level.txt +0 -0
  35. {spatialproteomics-0.7.9 → spatialproteomics-0.8.0}/tests/test_container.py +0 -0
  36. {spatialproteomics-0.7.9 → spatialproteomics-0.8.0}/tests/test_integration.py +0 -0
  37. {spatialproteomics-0.7.9 → spatialproteomics-0.8.0}/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.9
3
+ Version: 0.8.0
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.9"
3
+ version = "0.8.0"
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([])
@@ -294,8 +298,8 @@ class PlotAccessor:
294
298
  normalize: bool = True,
295
299
  merge: bool = True,
296
300
  layer_key: str = Layers.IMAGE,
297
- amin: Optional[float] = None,
298
- amax: Optional[float] = None,
301
+ amin: Optional[float | List] = None,
302
+ amax: Optional[float | List] = None,
299
303
  ) -> xr.Dataset:
300
304
  """
301
305
  Colorizes a stack of images.
@@ -312,10 +316,10 @@ class PlotAccessor:
312
316
  Merge the channel dimension. Default is True.
313
317
  layer_key : str, optional
314
318
  The key of the layer containing the image. Default is '_image'.
315
- amin : float, optional
316
- The minimum absolute value for normalization. Default is None.
317
- amax : float, optional
318
- The maximum absolute value for normalization. Default is None.
319
+ amin : float | List, optional
320
+ The minimum absolute value for normalization (either global or per channel). Default is None.
321
+ amax : float | List, optional
322
+ The maximum absolute value for normalization (either global or per channel). Default is None.
319
323
 
320
324
  Returns
321
325
  -------
@@ -77,8 +77,8 @@ def _colorize(
77
77
  colors: List[str] = ["C1", "C2", "C3", "C4", "C5"],
78
78
  background: str = "black",
79
79
  normalize: bool = True,
80
- amin: Optional[float] = None,
81
- amax: Optional[float] = None,
80
+ amin: Optional[float | List] = None,
81
+ amax: Optional[float | List] = None,
82
82
  ) -> np.ndarray:
83
83
  """
84
84
  Apply colorization to an image based on a given colors.
@@ -88,8 +88,8 @@ def _colorize(
88
88
  colors (List[str], optional): The list of colors to be used for colorization. Defaults to ["C1", "C2", "C3", "C4", "C5"].
89
89
  background (str, optional): The background color. Defaults to "black".
90
90
  normalize (bool, optional): Whether to normalize the image before colorization. Defaults to True.
91
- amin (Optional[float], optional): The minimum value for normalization. Defaults to None.
92
- amax (Optional[float], optional): The maximum value for normalization. Defaults to None.
91
+ amin (Optional[float | List], optional): The minimum value for normalization. Defaults to None.
92
+ amax (Optional[float | List], optional): The maximum value for normalization. Defaults to None.
93
93
 
94
94
  Returns:
95
95
  np.ndarray: The colorized image.
@@ -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
@@ -743,7 +747,7 @@ class PreprocessingAccessor:
743
747
 
744
748
  def add_channel(self, channels: Union[str, list], array: np.ndarray, layer_key: str = Layers.IMAGE) -> xr.Dataset:
745
749
  """
746
- Adds channel(s) to an existing image container.
750
+ Adds channel(s) to an existing spatialproteomics object.
747
751
 
748
752
  Parameters
749
753
  ----------
@@ -752,12 +756,12 @@ class PreprocessingAccessor:
752
756
  array : np.ndarray
753
757
  The numpy array representing the channel(s) to be added.
754
758
  layer_key : str
755
- The layer key where the channel(s) should be added. Default is 'image'.
759
+ The layer key where the channel(s) should be added. Default is '_image'.
756
760
 
757
761
  Returns
758
762
  -------
759
763
  xr.Dataset
760
- The updated image container with added channel(s).
764
+ The updated spatialproteomics object with added channel(s).
761
765
  """
762
766
  assert type(array) is np.ndarray, "Added channels must be numpy arrays."
763
767
  assert array.ndim in [2, 3], "Added channels must be 2D or 3D arrays."
@@ -795,10 +799,34 @@ class PreprocessingAccessor:
795
799
 
796
800
  return xr.merge([self._obj, da], join="outer", compat="no_conflicts")
797
801
 
802
+ def drop_channel(self, channels: Union[str, list]) -> xr.Dataset:
803
+ """
804
+ Drops channel(s) from an existing spatialproteomics object.
805
+
806
+ Parameters
807
+ ----------
808
+ channels : Union[str, list]
809
+ The name of the channel or a list of channel names to be dropped.
810
+
811
+ Returns
812
+ -------
813
+ xr.Dataset
814
+ The updated spatialproteomics object with dropped channel(s).
815
+ """
816
+ if type(channels) is str:
817
+ channels = [channels]
818
+
819
+ nonexistent_channels = set(channels) - set(self._obj.coords[Dims.CHANNELS].values)
820
+ assert (
821
+ len(nonexistent_channels) == 0
822
+ ), f"Some of the channels to be dropped do not exist in the object: {nonexistent_channels}. Please set the channel_names argument to a list of existing channels."
823
+
824
+ return self._obj.drop_sel({Dims.CHANNELS: channels})
825
+
798
826
  def add_segmentation(
799
827
  self,
800
828
  segmentation: Union[str, np.ndarray] = None,
801
- reindex: bool = True,
829
+ reindex: bool = False,
802
830
  keep_labels: bool = True,
803
831
  add_obs: bool = True,
804
832
  ) -> xr.Dataset:
@@ -1569,7 +1597,9 @@ class PreprocessingAccessor:
1569
1597
 
1570
1598
  return xr.merge([obj, new_img, new_seg], join="outer", compat="no_conflicts")
1571
1599
 
1572
- 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
+ ):
1573
1603
  """
1574
1604
  Filter the object by observations based on a given feature and filtering function.
1575
1605
 
@@ -1577,6 +1607,7 @@ class PreprocessingAccessor:
1577
1607
  col (str): The name of the feature to filter by.
1578
1608
  func (Callable): A filtering function that takes in the values of the feature and returns a boolean array.
1579
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.
1580
1611
 
1581
1612
  Returns:
1582
1613
  xr.Dataset: The filtered object with the selected cells and updated segmentation mask.
@@ -1586,7 +1617,6 @@ class PreprocessingAccessor:
1586
1617
 
1587
1618
  Notes:
1588
1619
  - This method filters the object by selecting only the cells that satisfy the filtering condition.
1589
- - It also updates the segmentation mask to remove cells that are not selected and relabels the remaining cells.
1590
1620
 
1591
1621
  Example:
1592
1622
  To filter the object by the feature "area" and keep only the cells with an area greater than 70px:
@@ -1615,10 +1645,11 @@ class PreprocessingAccessor:
1615
1645
  segmentation = obj[segmentation_key].values
1616
1646
  # setting all cells that are not in cells to 0
1617
1647
  segmentation = _remove_unlabeled_cells(segmentation, cells_sel)
1618
- # relabeling cells in the segmentation mask so the IDs go from 1 to n again
1619
- segmentation, relabel_dict = _relabel_cells(segmentation)
1620
- # updating the cell coords of the object
1621
- 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]
1622
1653
 
1623
1654
  # creating a data array with the segmentation mask, so that we can merge it to the original
1624
1655
  da = xr.DataArray(
@@ -1635,7 +1666,11 @@ class PreprocessingAccessor:
1635
1666
  return xr.merge([obj, da], join="outer", compat="no_conflicts")
1636
1667
 
1637
1668
  def remove_outlying_cells(
1638
- 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,
1639
1674
  ):
1640
1675
  """
1641
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.
@@ -1648,6 +1683,8 @@ class PreprocessingAccessor:
1648
1683
  The minimum number of cells in a connected component required for the cells to be kept. Default is 5.
1649
1684
  segmentation_key : str
1650
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.
1651
1688
 
1652
1689
  Returns
1653
1690
  -------
@@ -1679,10 +1716,11 @@ class PreprocessingAccessor:
1679
1716
  obj = self._obj.sel(query)
1680
1717
  # setting all cells that are not in cells to 0
1681
1718
  segmentation = _remove_unlabeled_cells(segmentation, cells_sel)
1682
- # relabeling cells in the segmentation mask so the IDs go from 1 to n again
1683
- segmentation, relabel_dict = _relabel_cells(segmentation)
1684
- # updating the cell coords of the object
1685
- 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]
1686
1724
 
1687
1725
  # creating a data array with the segmentation mask, so that we can merge it to the original
1688
1726
  da = xr.DataArray(
@@ -2058,13 +2096,16 @@ class PreprocessingAccessor:
2058
2096
 
2059
2097
  return xr.merge([obj, da], join="outer", compat="no_conflicts")
2060
2098
 
2061
- def mask_cells(self, mask_key: str = Layers.MASK, segmentation_key=Layers.SEGMENTATION) -> xr.Dataset:
2099
+ def mask_cells(
2100
+ self, mask_key: str = Layers.MASK, segmentation_key=Layers.SEGMENTATION, reindex: bool = False
2101
+ ) -> xr.Dataset:
2062
2102
  """
2063
2103
  Mask cells in the segmentation mask.
2064
2104
 
2065
2105
  Parameters:
2066
2106
  mask_key (str): The key of the mask to use for masking.
2067
2107
  segmentation_key (str): The key of the segmentation mask in the object. Default is Layers.SEGMENTATION.
2108
+ reindex (bool): Whether to reindex the cell IDs after filtering. Default is False.
2068
2109
 
2069
2110
  Returns:
2070
2111
  xr.Dataset: The object with the masked cells in the segmentation mask.
@@ -2093,10 +2134,11 @@ class PreprocessingAccessor:
2093
2134
  segmentation = obj[segmentation_key].values
2094
2135
  # setting all cells that are not in cells to 0
2095
2136
  segmentation = _remove_unlabeled_cells(segmentation, cells_sel)
2096
- # relabeling cells in the segmentation mask so the IDs go from 1 to n again
2097
- segmentation, relabel_dict = _relabel_cells(segmentation)
2098
- # updating the cell coords of the object
2099
- obj.coords[Dims.CELLS] = [relabel_dict[cell] for cell in obj.coords["cells"].values]
2137
+ if reindex:
2138
+ # relabeling cells in the segmentation mask so the IDs go from 1 to n again
2139
+ segmentation, relabel_dict = _relabel_cells(segmentation)
2140
+ # updating the cell coords of the object
2141
+ obj.coords[Dims.CELLS] = [relabel_dict[cell] for cell in obj.coords["cells"].values]
2100
2142
 
2101
2143
  # creating a data array with the segmentation mask, so that we can merge it to the original
2102
2144
  da = xr.DataArray(
@@ -2199,3 +2241,83 @@ class PreprocessingAccessor:
2199
2241
  # if there are multiple layers with channels as coordinates, this will turn it into float32, since it will introduce NaNs for the new channel in other layers
2200
2242
  obj = self._obj.pp.add_channel(channels=key_added, array=merged)
2201
2243
  return obj
2244
+
2245
+ def find_bright_cells(
2246
+ self,
2247
+ channels: Optional[Union[str, list]] = None,
2248
+ frac_channels: float = 0.5,
2249
+ quantile: float = 0.95,
2250
+ key: str = Layers.INTENSITY,
2251
+ key_added: str = "is_bright",
2252
+ ) -> xr.Dataset:
2253
+ """
2254
+ Flags cells that are bright across multiple channels (e.g., due to a lot of antibodies binding to that cell).
2255
+ A cell is considered bright in a channel if its intensity exceeds the given quantile threshold
2256
+ for that channel. Cells that are bright in at least frac_channels of the
2257
+ considered channels are flagged.
2258
+
2259
+ Parameters
2260
+ ----------
2261
+ channels : Optional[Union[str, list]]
2262
+ The channels to consider. If None, all channels are used.
2263
+ frac_channels : float
2264
+ The fraction of channels in which a cell must be bright to be flagged.
2265
+ Default is 0.5.
2266
+ quantile : float
2267
+ The quantile threshold above which a cell is considered bright in a
2268
+ given channel. Computed per channel. Default is 0.95.
2269
+ key : str
2270
+ The key of the intensity layer to use. Default is Layers.INTENSITY.
2271
+ key_added : str
2272
+ The name of the feature added to obs. Default is 'is_bright'.
2273
+
2274
+ Returns
2275
+ -------
2276
+ xr.Dataset
2277
+ The updated image container with the 'is_bright' flag added to obs.
2278
+
2279
+ Raises
2280
+ ------
2281
+ ValueError
2282
+ If no intensity matrix is found at the specified layer.
2283
+
2284
+ Example
2285
+ -------
2286
+ Flag cells that are bright in at least 50% of all channels at the 95th percentile:
2287
+ >>> ds = ds.pp.find_bright_cells()
2288
+
2289
+ Flag cells that are bright in at least 2 out of 3 specific channels:
2290
+ >>> ds = ds.pp.find_bright_cells(channels=['CD3', 'CD8', 'CD20'], frac_channels=0.66)
2291
+ """
2292
+ if key not in self._obj:
2293
+ raise ValueError(f"No intensity matrix found at layer {key}. Please run pp.add_quantification() first.")
2294
+
2295
+ assert 0 <= frac_channels <= 1, "frac_channels must be between 0 and 1."
2296
+ assert 0 <= quantile <= 1, "quantile must be between 0 and 1."
2297
+
2298
+ # resolve channels
2299
+ all_channels = self._obj.coords[Dims.CHANNELS].values
2300
+ if channels is None:
2301
+ channels = all_channels
2302
+ elif isinstance(channels, str):
2303
+ channels = [channels]
2304
+
2305
+ nonexistent = set(channels) - set(all_channels)
2306
+ assert (
2307
+ len(nonexistent) == 0
2308
+ ), f"The following channels were not found: {nonexistent}. Please adjust the 'channels' argument to only contain existing channels."
2309
+
2310
+ # get the intensity matrix for the selected channels, shape (n_cells, n_channels)
2311
+ intensity = self._obj[key].sel({Dims.CHANNELS: channels}).values
2312
+
2313
+ # compute per-channel quantile thresholds
2314
+ thresholds = np.quantile(intensity, quantile, axis=0) # shape (n_channels,)
2315
+
2316
+ # a cell is bright in a channel if it exceeds that channel's threshold
2317
+ is_above = intensity > thresholds # shape (n_cells, n_channels)
2318
+
2319
+ # flag cells that are bright in at least frac_channels of the considered channels
2320
+ min_channels = int(np.ceil(frac_channels * len(channels)))
2321
+ is_bright = is_above.sum(axis=1) >= min_channels # shape (n_cells,)
2322
+
2323
+ return self._obj.pp.add_feature(key_added, is_bright.astype(float))
@@ -161,8 +161,8 @@ def _normalize(
161
161
  pmax: float = 99.8,
162
162
  eps: float = 1e-20,
163
163
  clip: bool = False,
164
- amin: Optional[float] = None,
165
- amax: Optional[float] = None,
164
+ amin: Optional[float | List] = None,
165
+ amax: Optional[float | List] = None,
166
166
  ) -> np.ndarray:
167
167
  """
168
168
  Performs a min max normalisation.
@@ -181,9 +181,9 @@ def _normalize(
181
181
  clip: bool
182
182
  Ensures that normed image array contains no values greater than 1.
183
183
  amin: float
184
- Absolute value to perform normalization. If set, this overrides pmin.
184
+ Absolute value to perform normalization. If set, this overrides pmin. Can be a list of values, in which case each channel is normalized independently.
185
185
  amax: float
186
- Absolute value to perform normalization. If set, this overrides pmax.
186
+ Absolute value to perform normalization. If set, this overrides pmax. Can be a list of values, in which case each channel is normalized independently.
187
187
 
188
188
  Returns
189
189
  -------
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spatialproteomics
3
- Version: 0.7.9
3
+ Version: 0.8.0
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