spatialproteomics 0.8.2__tar.gz → 0.8.3__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.2/spatialproteomics.egg-info → spatialproteomics-0.8.3}/PKG-INFO +1 -1
  2. {spatialproteomics-0.8.2 → spatialproteomics-0.8.3}/pyproject.toml +1 -1
  3. {spatialproteomics-0.8.2 → spatialproteomics-0.8.3}/spatialproteomics/pl/plot.py +30 -14
  4. {spatialproteomics-0.8.2 → spatialproteomics-0.8.3}/spatialproteomics/pp/utils.py +76 -40
  5. {spatialproteomics-0.8.2 → spatialproteomics-0.8.3/spatialproteomics.egg-info}/PKG-INFO +1 -1
  6. {spatialproteomics-0.8.2 → spatialproteomics-0.8.3}/LICENSE +0 -0
  7. {spatialproteomics-0.8.2 → spatialproteomics-0.8.3}/README.md +0 -0
  8. {spatialproteomics-0.8.2 → spatialproteomics-0.8.3}/setup.cfg +0 -0
  9. {spatialproteomics-0.8.2 → spatialproteomics-0.8.3}/spatialproteomics/__init__.py +0 -0
  10. {spatialproteomics-0.8.2 → spatialproteomics-0.8.3}/spatialproteomics/base_logger.py +0 -0
  11. {spatialproteomics-0.8.2 → spatialproteomics-0.8.3}/spatialproteomics/constants.py +0 -0
  12. {spatialproteomics-0.8.2 → spatialproteomics-0.8.3}/spatialproteomics/container.py +0 -0
  13. {spatialproteomics-0.8.2 → spatialproteomics-0.8.3}/spatialproteomics/image_container/ImageContainer.py +0 -0
  14. {spatialproteomics-0.8.2 → spatialproteomics-0.8.3}/spatialproteomics/image_container/__init__.py +0 -0
  15. {spatialproteomics-0.8.2 → spatialproteomics-0.8.3}/spatialproteomics/la/__init__.py +0 -0
  16. {spatialproteomics-0.8.2 → spatialproteomics-0.8.3}/spatialproteomics/la/label.py +0 -0
  17. {spatialproteomics-0.8.2 → spatialproteomics-0.8.3}/spatialproteomics/la/utils.py +0 -0
  18. {spatialproteomics-0.8.2 → spatialproteomics-0.8.3}/spatialproteomics/nh/__init__.py +0 -0
  19. {spatialproteomics-0.8.2 → spatialproteomics-0.8.3}/spatialproteomics/nh/neighborhood.py +0 -0
  20. {spatialproteomics-0.8.2 → spatialproteomics-0.8.3}/spatialproteomics/nh/utils.py +0 -0
  21. {spatialproteomics-0.8.2 → spatialproteomics-0.8.3}/spatialproteomics/pl/__init__.py +0 -0
  22. {spatialproteomics-0.8.2 → spatialproteomics-0.8.3}/spatialproteomics/pl/utils.py +0 -0
  23. {spatialproteomics-0.8.2 → spatialproteomics-0.8.3}/spatialproteomics/pp/__init__.py +0 -0
  24. {spatialproteomics-0.8.2 → spatialproteomics-0.8.3}/spatialproteomics/pp/intensity.py +0 -0
  25. {spatialproteomics-0.8.2 → spatialproteomics-0.8.3}/spatialproteomics/pp/preprocessing.py +0 -0
  26. {spatialproteomics-0.8.2 → spatialproteomics-0.8.3}/spatialproteomics/sd/__init__.py +0 -0
  27. {spatialproteomics-0.8.2 → spatialproteomics-0.8.3}/spatialproteomics/sd/utils.py +0 -0
  28. {spatialproteomics-0.8.2 → spatialproteomics-0.8.3}/spatialproteomics/tl/__init__.py +0 -0
  29. {spatialproteomics-0.8.2 → spatialproteomics-0.8.3}/spatialproteomics/tl/tool.py +0 -0
  30. {spatialproteomics-0.8.2 → spatialproteomics-0.8.3}/spatialproteomics/tl/utils.py +0 -0
  31. {spatialproteomics-0.8.2 → spatialproteomics-0.8.3}/spatialproteomics.egg-info/SOURCES.txt +0 -0
  32. {spatialproteomics-0.8.2 → spatialproteomics-0.8.3}/spatialproteomics.egg-info/dependency_links.txt +0 -0
  33. {spatialproteomics-0.8.2 → spatialproteomics-0.8.3}/spatialproteomics.egg-info/requires.txt +0 -0
  34. {spatialproteomics-0.8.2 → spatialproteomics-0.8.3}/spatialproteomics.egg-info/top_level.txt +0 -0
  35. {spatialproteomics-0.8.2 → spatialproteomics-0.8.3}/tests/test_container.py +0 -0
  36. {spatialproteomics-0.8.2 → spatialproteomics-0.8.3}/tests/test_integration.py +0 -0
  37. {spatialproteomics-0.8.2 → spatialproteomics-0.8.3}/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.2
3
+ Version: 0.8.3
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.2"
3
+ version = "0.8.3"
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"
@@ -1114,6 +1114,18 @@ class PlotAccessor:
1114
1114
  extent=bounds,
1115
1115
  )
1116
1116
 
1117
+ # draw any pending boxes added via add_box()
1118
+ for box in self._obj.attrs.pop("_pending_boxes", []):
1119
+ rect = patches.Rectangle(
1120
+ (box["xlim"][0], box["ylim"][0]),
1121
+ box["xlim"][1] - box["xlim"][0],
1122
+ box["ylim"][1] - box["ylim"][0],
1123
+ edgecolor=box["color"],
1124
+ linewidth=box["linewidth"],
1125
+ facecolor="none",
1126
+ )
1127
+ ax.add_patch(rect)
1128
+
1117
1129
  legend = []
1118
1130
 
1119
1131
  if legend_image:
@@ -1401,20 +1413,24 @@ class PlotAccessor:
1401
1413
  xr.Dataset
1402
1414
  The updated spatialproteomics object.
1403
1415
  """
1404
- if ax is None:
1405
- ax = plt.gca()
1406
-
1407
- # Create a rectangle and add it to the axis
1408
- rect = patches.Rectangle(
1409
- (xlim[0], ylim[0]), # Bottom-left corner
1410
- xlim[1] - xlim[0], # Width
1411
- ylim[1] - ylim[0], # Height
1412
- edgecolor=color,
1413
- linewidth=linewidth,
1414
- facecolor="none", # Ensure it's just an outline
1415
- )
1416
- ax.add_patch(rect)
1417
-
1416
+ if ax is not None:
1417
+ # existing immediate-draw behaviour when ax is explicit
1418
+ rect = patches.Rectangle(
1419
+ (xlim[0], ylim[0]), # bottom left corner
1420
+ xlim[1] - xlim[0], # width
1421
+ ylim[1] - ylim[0], # height
1422
+ edgecolor=color,
1423
+ linewidth=linewidth,
1424
+ facecolor="none",
1425
+ )
1426
+ ax.add_patch(rect)
1427
+ return self._obj
1428
+
1429
+ # no ax provided: defer rendering by storing box params in the dataset attrs
1430
+ # the boxes are then rendered in pl.imshow() when the plot is drawn
1431
+ pending = list(self._obj.attrs.get("_pending_boxes", []))
1432
+ pending.append(dict(xlim=xlim, ylim=ylim, color=color, linewidth=linewidth))
1433
+ self._obj.attrs["_pending_boxes"] = pending
1418
1434
  return self._obj
1419
1435
 
1420
1436
  def autocrop(
@@ -602,12 +602,12 @@ def _threshold(
602
602
  if (quantile is None and intensity is None) or (quantile is not None and intensity is not None):
603
603
  raise ValueError("Please provide a quantile or absolute intensity cut off.")
604
604
 
605
- if isinstance(quantile, (float, int)):
605
+ if isinstance(quantile, (float, int, np.generic)):
606
606
  quantile = np.array([quantile])
607
607
  if isinstance(quantile, list):
608
608
  quantile = np.array(quantile)
609
609
 
610
- if isinstance(intensity, (float, int)):
610
+ if isinstance(intensity, (float, int, np.generic)):
611
611
  intensity = np.array([intensity])
612
612
  if isinstance(intensity, list):
613
613
  intensity = np.array(intensity)
@@ -626,68 +626,104 @@ def _threshold(
626
626
  if quantile is not None:
627
627
  assert len(channels) == len(quantile), "The number of channels must match the number of quantile values."
628
628
  quantile_dict = dict(zip(channels, quantile))
629
- quantile = np.array([quantile_dict.get(channel, 0) for channel in all_channels])
629
+ # we cannot just use 0 in the get() here, because the minimum intensity of a marker might be above 0,
630
+ # #in which case we would be thresholding the wrong value. Instead, we use np.nan and then check for this later on.
631
+ quantile = np.array([quantile_dict.get(channel, np.nan) for channel in all_channels])
630
632
  if intensity is not None:
631
633
  assert len(channels) == len(intensity), "The number of channels must match the number of intensity values."
632
634
  intensity_dict = dict(zip(channels, intensity))
633
- intensity = np.array([intensity_dict.get(channel, 0) for channel in all_channels])
635
+ intensity = np.array([intensity_dict.get(channel, np.nan) for channel in all_channels])
634
636
  else:
635
637
  # If no channels provided, assume the threshold applies to all channels
636
638
  if quantile is not None:
637
639
  assert len(quantile) == 1 or len(quantile) == len(
638
640
  all_channels
639
641
  ), "Quantile threshold must be a single value or a list of values with the same length as the number of channels. If you only want to threshold a subset of channels, you can use the channels argument."
640
- quantile = np.full(len(all_channels), quantile.item() if quantile.size == 1 else quantile)
641
642
  if intensity is not None:
642
643
  assert len(intensity) == 1 or len(intensity) == len(
643
644
  all_channels
644
645
  ), "Intensity threshold must be a single value or a list of values with the same length as the number of channels. If you only want to threshold a subset of channels, you can use the channels argument."
645
646
  assert np.all(intensity >= 0), "Intensity values must be positive."
646
- assert np.all(
647
- intensity <= np.max(image.values)
648
- ), "Intensity values must be smaller than the maximum intensity."
649
- intensity = np.full(len(all_channels), intensity.item() if intensity.size == 1 else intensity)
647
+
648
+ # Get underlying numpy array and its dtype
649
+ img = image.values # shape (C, Y, X)
650
+ img_dtype = img.dtype
650
651
 
651
652
  if quantile is not None:
652
- assert np.all(quantile >= 0) and np.all(quantile <= 1), "Quantile values must be between 0 and 1."
653
+ if quantile.size == 1:
654
+ quantile = np.full(len(all_channels), quantile.item(), dtype=float)
655
+ else:
656
+ quantile = np.asarray(quantile, dtype=float)
653
657
 
654
- if shift:
655
- # calculate quantile (and ensure the correct dtypes in order to be more memory-efficient)
656
- # this is done by first clipping the values below the lower value, and subsequently subtracting the lower value from the result, which allows us to use the original dtype throughout
657
- lower = np.quantile(image.values.reshape(image.values.shape[0], -1), quantile, axis=1).astype(image.dtype)
658
-
659
- filtered = np.clip(
660
- image, a_min=np.expand_dims(np.diag(lower) if lower.ndim > 1 else lower, (1, 2)), a_max=None
661
- ).astype(image.dtype) - np.expand_dims(np.diag(lower) if lower.ndim > 1 else lower, (1, 2)).astype(
662
- image.dtype
663
- )
658
+ if intensity is not None:
659
+ if intensity.size == 1:
660
+ intensity = np.full(len(all_channels), intensity.item(), dtype=img_dtype)
664
661
  else:
665
- # Calculate the quantile-based intensity threshold for each channel.
666
- flattened_values = image.values.reshape(
667
- image.values.shape[0], -1
668
- ) # Flatten height and width for each channel.
669
- lower = np.array(
670
- [np.quantile(flattened_values[i], q) for i, q in enumerate(quantile)]
671
- ) # Compute quantile per channel.
662
+ # cast each provided value to image dtype
663
+ intensity = np.asarray(intensity, dtype=img_dtype)
664
+
665
+ max_val = np.max(img)
666
+ assert np.all(intensity <= max_val), "Intensity values must be smaller than the maximum intensity."
672
667
 
673
- # Reshape lower to match the broadcasting requirements.
674
- lower = lower[:, np.newaxis, np.newaxis] # Reshape to add height and width dimensions.
668
+ filtered = None
675
669
 
676
- # Use np.where to apply the quantile threshold without shifting.
677
- filtered = np.where(image.values >= lower, image.values, 0)
670
+ # --- QUANTILE BRANCH ---
671
+ if quantile is not None:
672
+ assert np.all(quantile[~np.isnan(quantile)] >= 0) and np.all(
673
+ quantile[~np.isnan(quantile)] <= 1
674
+ ), "Quantile values must be between 0 and 1."
678
675
 
676
+ flat = img.reshape(img.shape[0], -1)
677
+ lower = np.array([np.quantile(flat[i], q) if not np.isnan(q) else 0.0 for i, q in enumerate(quantile)]).astype(
678
+ img_dtype
679
+ )
680
+
681
+ lower = lower[:, np.newaxis, np.newaxis]
682
+ filtered = np.empty_like(img)
683
+
684
+ if shift:
685
+ for c in range(img.shape[0]):
686
+ lc = lower[c, 0, 0]
687
+ if np.isnan(quantile[c]): # no-op channel: copy as-is
688
+ np.copyto(filtered[c], img[c])
689
+ else:
690
+ np.maximum(img[c], lc, out=filtered[c])
691
+ filtered[c] -= lc
692
+ else:
693
+ for c in range(img.shape[0]):
694
+ lc = lower[c, 0, 0]
695
+ np.copyto(filtered[c], img[c])
696
+ if not np.isnan(quantile[c]): # only threshold specified channels
697
+ mask = filtered[c] < lc
698
+ filtered[c][mask] = img_dtype.type(0)
699
+
700
+ # --- INTENSITY BRANCH (dtype-safe & channel-wise) ---
679
701
  if intensity is not None:
702
+ base = img if filtered is None else filtered
703
+ result = np.empty_like(base)
704
+ n_channels = base.shape[0]
705
+
680
706
  if shift:
681
- # calculate intensity
682
- filtered = (image - intensity.reshape(-1, 1, 1)).clip(min=0)
707
+ for c in range(n_channels):
708
+ if np.isnan(intensity[c]): # no-op channel
709
+ np.copyto(result[c], base[c])
710
+ else:
711
+ thr = img_dtype.type(intensity[c])
712
+ np.maximum(base[c], thr, out=result[c])
713
+ result[c] -= thr
683
714
  else:
684
- # Reshape intensity to broadcast correctly across all dimensions.
685
- if len(intensity) == 1:
686
- intensity = intensity[0] # This will make it a scalar for simple broadcasting.
687
- else:
688
- intensity = intensity[:, np.newaxis, np.newaxis] # Add two new axes for broadcasting.
689
- # Apply thresholding: set all values below the intensity threshold to 0.
690
- filtered = np.where(image.values >= intensity, image.values, 0)
715
+ zero = img_dtype.type(0)
716
+ for c in range(n_channels):
717
+ np.copyto(result[c], base[c])
718
+ if not np.isnan(intensity[c]): # only threshold specified channels
719
+ thr = img_dtype.type(intensity[c])
720
+ mask = result[c] < thr
721
+ result[c][mask] = zero
722
+
723
+ filtered = result
724
+
725
+ if filtered is None:
726
+ raise RuntimeError("Internal error: neither quantile nor intensity branch produced a result.")
691
727
 
692
728
  return filtered
693
729
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spatialproteomics
3
- Version: 0.8.2
3
+ Version: 0.8.3
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