scikit-ops 0.1.0.dev0__py3-none-any.whl

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 (66) hide show
  1. scikit_ops-0.1.0.dev0.dist-info/METADATA +372 -0
  2. scikit_ops-0.1.0.dev0.dist-info/RECORD +66 -0
  3. scikit_ops-0.1.0.dev0.dist-info/WHEEL +4 -0
  4. scikit_ops-0.1.0.dev0.dist-info/entry_points.txt +2 -0
  5. skop/__init__.py +14 -0
  6. skop/envs/cellpose3/pixi.toml +46 -0
  7. skop/envs/cupy/pixi.toml +35 -0
  8. skop/envs/minimal/pixi.toml +25 -0
  9. skop/envs/pytorch/pixi.toml +88 -0
  10. skop/envs/sdeconv/pixi.toml +85 -0
  11. skop/envs/segment-everything/pixi.toml +96 -0
  12. skop/envs/skimage/pixi.toml +28 -0
  13. skop/envs/stardist-tf/pixi.toml +91 -0
  14. skop/envs/unseg-cv/pixi.toml +27 -0
  15. skop/models.py +145 -0
  16. skop/ops/__init__.py +19 -0
  17. skop/ops/_util.py +115 -0
  18. skop/ops/deconvolve/__init__.py +18 -0
  19. skop/ops/deconvolve/_edges.py +92 -0
  20. skop/ops/deconvolve/_pad.py +117 -0
  21. skop/ops/deconvolve/richardson_lucy.py +100 -0
  22. skop/ops/deconvolve/richardson_lucy_cupy.py +116 -0
  23. skop/ops/detect/__init__.py +28 -0
  24. skop/ops/detect/_result.py +45 -0
  25. skop/ops/detect/fastsam.py +103 -0
  26. skop/ops/detect/jdll_yolo.py +115 -0
  27. skop/ops/detect/object_aware_yolo.py +112 -0
  28. skop/ops/detect/yolo.py +441 -0
  29. skop/ops/edges.py +382 -0
  30. skop/ops/generate.py +77 -0
  31. skop/ops/kernels/__init__.py +39 -0
  32. skop/ops/kernels/_fluorophore.py +42 -0
  33. skop/ops/kernels/_recenter.py +60 -0
  34. skop/ops/kernels/gibson_lanni.py +147 -0
  35. skop/ops/kernels/paraxial.py +128 -0
  36. skop/ops/kernels/psf.py +59 -0
  37. skop/ops/labels.py +144 -0
  38. skop/ops/mask/__init__.py +29 -0
  39. skop/ops/mask/_result.py +40 -0
  40. skop/ops/mask/microsam.py +114 -0
  41. skop/ops/mask/mobilesam.py +176 -0
  42. skop/ops/morphology.py +214 -0
  43. skop/ops/normalize.py +85 -0
  44. skop/ops/segment/__init__.py +24 -0
  45. skop/ops/segment/cellpose3.py +245 -0
  46. skop/ops/segment/cellpose4.py +127 -0
  47. skop/ops/segment/instanseg.py +162 -0
  48. skop/ops/segment/stardist2d.py +192 -0
  49. skop/ops/segment/starfun3d.py +104 -0
  50. skop/ops/segment/unseg/__init__.py +138 -0
  51. skop/ops/segment/unseg/_algorithm.py +1618 -0
  52. skop/ops/smooth.py +452 -0
  53. skop/ops/threshold.py +278 -0
  54. skop/ops/toy.py +114 -0
  55. skop/ops/train/__init__.py +81 -0
  56. skop/ops/train/_util.py +152 -0
  57. skop/ops/train/cellpose3.py +189 -0
  58. skop/ops/train/cellpose4.py +164 -0
  59. skop/ops/train/stardist2d.py +439 -0
  60. skop/ops/workflows/__init__.py +43 -0
  61. skop/ops/workflows/deconvolve/__init__.py +7 -0
  62. skop/ops/workflows/deconvolve/with_psf.py +103 -0
  63. skop/ops/workflows/mask/__init__.py +7 -0
  64. skop/ops/workflows/mask/detect_then_mask.py +97 -0
  65. skop/ops/workflows/segment/__init__.py +7 -0
  66. skop/ops/workflows/segment/connect_2d_in_3d.py +145 -0
@@ -0,0 +1,192 @@
1
+ """StarDist 2D inference, pretrained or from a model on disk.
2
+
3
+ Ported from src/imgops/implementations/stardist2d.py. Shares the
4
+ 'stardist-tf' environment with starfun3d.py.
5
+
6
+ Two ops for the pretrained models rather than one with a ``model`` switch,
7
+ because they do not accept the same thing: the H&E model is trained on stain
8
+ colour and always wants a channel axis, while the fluorescence model wants a
9
+ single channel and collapses one if it is given it. That is a difference in
10
+ what the op *consumes*, so it belongs in ``Axes`` where a front end can see
11
+ it, not in an enum parameter where it is invisible until StarDist raises.
12
+
13
+ ``stardist2d_custom`` is the third, and takes a directory. It cannot make the
14
+ same declaration -- a model on disk is single- or multi-channel depending on
15
+ how it was trained -- so it declares ``c?`` and reads the model's own
16
+ ``config.json`` to decide. A caller with many trained models points this one
17
+ op at each of them in turn; see design 0011.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from typing import Annotated
23
+
24
+ import numpy as np
25
+
26
+ from opspec.op import Axes, op
27
+ from opspec.runtime import progress
28
+ from opspec.types import ImageOf, LabelsOf
29
+
30
+ from .._util import to_gray
31
+
32
+ _FLUO = "2D_versatile_fluo"
33
+ _HE = "2D_versatile_he"
34
+
35
+
36
+ @op(env="stardist-tf")
37
+ def stardist2d_fluo(
38
+ image: Annotated[ImageOf[np.ndarray], Axes("y", "x", "c?")],
39
+ prob_thresh: Annotated[
40
+ float,
41
+ {"widget_type": "FloatSlider", "min": 0.0, "max": 1.0, "step": 0.05},
42
+ ] = 0.5,
43
+ nms_thresh: Annotated[
44
+ float,
45
+ {"widget_type": "FloatSlider", "min": 0.0, "max": 1.0, "step": 0.05},
46
+ ] = 0.4,
47
+ normalize: bool = True,
48
+ ) -> LabelsOf[np.ndarray]:
49
+ """Detect objects in a fluorescence image with pretrained StarDist.
50
+
51
+ Args:
52
+ image: Plane to segment. A trailing RGB(A) axis is collapsed, since
53
+ this model wants a single channel.
54
+ prob_thresh: Object probability threshold.
55
+ nms_thresh: Non-maximum suppression threshold.
56
+ normalize: Whether to percentile-normalize the input first.
57
+
58
+ Returns:
59
+ A label image, one integer per detected object.
60
+
61
+ Note: the model weights are downloaded on first use.
62
+ """
63
+ return _predict(to_gray(image), _FLUO, prob_thresh, nms_thresh, normalize)
64
+
65
+
66
+ @op(env="stardist-tf")
67
+ def stardist2d_he(
68
+ image: Annotated[ImageOf[np.ndarray], Axes("y", "x", "c")],
69
+ prob_thresh: Annotated[
70
+ float,
71
+ {"widget_type": "FloatSlider", "min": 0.0, "max": 1.0, "step": 0.05},
72
+ ] = 0.5,
73
+ nms_thresh: Annotated[
74
+ float,
75
+ {"widget_type": "FloatSlider", "min": 0.0, "max": 1.0, "step": 0.05},
76
+ ] = 0.4,
77
+ normalize: bool = True,
78
+ ) -> LabelsOf[np.ndarray]:
79
+ """Detect objects in an H&E-stained image with pretrained StarDist.
80
+
81
+ Args:
82
+ image: Plane to segment, with its colour channels intact -- this model
83
+ is trained on stain colour, so the channel axis is required.
84
+ prob_thresh: Object probability threshold.
85
+ nms_thresh: Non-maximum suppression threshold.
86
+ normalize: Whether to percentile-normalize the input first.
87
+
88
+ Returns:
89
+ A label image, one integer per detected object.
90
+
91
+ Note: the model weights are downloaded on first use.
92
+ """
93
+ return _predict(image, _HE, prob_thresh, nms_thresh, normalize)
94
+
95
+
96
+ @op(env="stardist-tf")
97
+ def stardist2d_custom(
98
+ image: Annotated[ImageOf[np.ndarray], Axes("y", "x", "c?")],
99
+ model_dir: Annotated[str, {"widget_type": "FileEdit", "mode": "d"}] = "",
100
+ prob_thresh: Annotated[
101
+ float,
102
+ {"widget_type": "FloatSlider", "min": 0.0, "max": 1.0, "step": 0.05},
103
+ ] = 0.5,
104
+ nms_thresh: Annotated[
105
+ float,
106
+ {"widget_type": "FloatSlider", "min": 0.0, "max": 1.0, "step": 0.05},
107
+ ] = 0.4,
108
+ normalize: bool = True,
109
+ ) -> LabelsOf[np.ndarray]:
110
+ """Detect objects with a StarDist 2D model loaded from disk.
111
+
112
+ The counterpart to the two pretrained ops: same prediction, but the model
113
+ comes from a directory rather than StarDist's download cache. That is what
114
+ a model trained by ``skop.ops.train`` -- or by anything else that writes a
115
+ StarDist model -- is loaded with.
116
+
117
+ Args:
118
+ image: Plane to segment. The trailing channel axis is collapsed when
119
+ the model expects a single channel, and kept when it does not;
120
+ the model's own config decides, not the caller.
121
+ model_dir: Directory holding ``config.json`` and the weights. The
122
+ directory name is the model name, as StarDist stores it.
123
+ prob_thresh: Object probability threshold.
124
+ nms_thresh: Non-maximum suppression threshold.
125
+ normalize: Whether to percentile-normalize the input first.
126
+
127
+ Returns:
128
+ A label image, one integer per detected object.
129
+ """
130
+ import json
131
+ import os
132
+
133
+ from stardist.models import StarDist2D
134
+
135
+ if not model_dir:
136
+ raise ValueError(
137
+ "model_dir is required -- this op loads a model from disk. Use "
138
+ "stardist2d_fluo or stardist2d_he for the pretrained models."
139
+ )
140
+
141
+ model_dir = os.path.normpath(model_dir)
142
+ basedir, name = os.path.split(model_dir)
143
+ config_path = os.path.join(model_dir, "config.json")
144
+ if not os.path.isfile(config_path):
145
+ raise ValueError(f"No StarDist model in {model_dir}: no config.json")
146
+
147
+ with open(config_path) as f:
148
+ n_channel_in = json.load(f).get("n_channel_in", 1)
149
+
150
+ x = image if n_channel_in > 1 else to_gray(image)
151
+
152
+ progress(f"Loading model {name}")
153
+ net = StarDist2D(None, name=name, basedir=basedir)
154
+
155
+ return _run(net, x, prob_thresh, nms_thresh, normalize)
156
+
157
+
158
+ def _predict(
159
+ x: np.ndarray,
160
+ model: str,
161
+ prob_thresh: float,
162
+ nms_thresh: float,
163
+ normalize: bool,
164
+ ) -> np.ndarray:
165
+ """Run one pretrained StarDist 2D model over an already-shaped array."""
166
+ from stardist.models import StarDist2D
167
+
168
+ progress(f"Loading pretrained model {model}")
169
+ net = StarDist2D.from_pretrained(model)
170
+ return _run(net, x, prob_thresh, nms_thresh, normalize)
171
+
172
+
173
+ def _run(
174
+ net,
175
+ x: np.ndarray,
176
+ prob_thresh: float,
177
+ nms_thresh: float,
178
+ normalize: bool,
179
+ ) -> np.ndarray:
180
+ """Normalize if asked, then predict instances with an already-loaded net."""
181
+ from csbdeep.utils import normalize as normalize_percentile
182
+
183
+ if normalize:
184
+ x = normalize_percentile(x, 1, 99.8)
185
+
186
+ progress("Predicting instances")
187
+ labels, _ = net.predict_instances(
188
+ x,
189
+ prob_thresh=prob_thresh,
190
+ nms_thresh=nms_thresh,
191
+ )
192
+ return np.asarray(labels).astype(np.uint16)
@@ -0,0 +1,104 @@
1
+ """3D nuclei segmentation with pretrained StarDist models.
2
+
3
+ Ported from https://github.com/ctrueden/starfun3d. Shares the 'stardist-tf'
4
+ environment with stardist2d.py.
5
+
6
+ The models come from Galindo et al. (2023), "3D Nuclei Segmentation By
7
+ Combining GAN Based Image Synthesis and Existing 3D Manual Annotations",
8
+ https://doi.org/10.1101/2023.12.06.570366, published at
9
+ https://zenodo.org/records/10518151. They total 36 MB, so they are fetched
10
+ into skop's asset cache on first use rather than carried in the repository.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from enum import Enum
16
+ from typing import Annotated, NamedTuple
17
+
18
+ import numpy as np
19
+
20
+ from opspec.op import op
21
+ from opspec.runtime import progress
22
+ from opspec.types import ImageOf, LabelsOf, PointsOf
23
+
24
+ _ZENODO = "https://zenodo.org/records/10518151/files/model_{name}.zip?download=1"
25
+
26
+
27
+ class Model(Enum):
28
+ """Pretrained StarDist3D models, one per acquisition modality.
29
+
30
+ Average nucleus size in the training data, in pixels:
31
+ confocal [x=39, y=39, z=7], sospim [x=27, y=28, z=10],
32
+ spinning [x=39, y=39, z=7].
33
+ """
34
+
35
+ confocal = "confocal"
36
+ sospim = "sospim"
37
+ spinning = "spinning"
38
+
39
+
40
+ class Nuclei(NamedTuple):
41
+ labels: LabelsOf[np.ndarray]
42
+ points: PointsOf[np.ndarray]
43
+
44
+
45
+ @op(env="stardist-tf")
46
+ def segment_nuclei(
47
+ image: ImageOf[np.ndarray],
48
+ model: Model = Model.confocal,
49
+ prob_thresh: Annotated[
50
+ float | None,
51
+ {"widget_type": "FloatSlider", "min": 0.0, "max": 1.0, "step": 0.05},
52
+ ] = None,
53
+ nms_thresh: Annotated[
54
+ float | None,
55
+ {"widget_type": "FloatSlider", "min": 0.0, "max": 1.0, "step": 0.05},
56
+ ] = None,
57
+ normalize: bool = True,
58
+ ) -> Nuclei:
59
+ """Segment nuclei in a 3D volume.
60
+
61
+ Args:
62
+ image: Volume to segment, in (Z, Y, X) order.
63
+ model: Which pretrained model to use; pick the one matching how the
64
+ data was acquired.
65
+ prob_thresh: Object probability threshold. None uses the model's own.
66
+ nms_thresh: Non-maximum suppression threshold. None uses the model's.
67
+ normalize: Whether to percentile-normalize the input first.
68
+
69
+ Returns:
70
+ A label volume, and the detected nucleus centers as (N, 3) points.
71
+ """
72
+ from csbdeep.utils import normalize as normalize_percentile
73
+ from stardist.models import StarDist3D
74
+
75
+ from ophelper import assets
76
+
77
+ model_dir = assets.unzip_from_url(
78
+ _ZENODO.format(name=model.value),
79
+ f"starfun3d/{model.value}",
80
+ marker="config.json",
81
+ )
82
+
83
+ progress(f"Loading model {model.value}")
84
+ # NB: StarDist3D locates a model as basedir/name, so the model's own
85
+ # directory name must be used -- the original starfun3d passed a constant
86
+ # here, and so loaded the confocal model whichever model was asked for.
87
+ net = StarDist3D(None, name=model_dir.name, basedir=str(model_dir.parent))
88
+
89
+ x = normalize_percentile(image, 1, 99.8) if normalize else image
90
+
91
+ predict_kwargs = {}
92
+ if prob_thresh is not None:
93
+ predict_kwargs["prob_thresh"] = prob_thresh
94
+ if nms_thresh is not None:
95
+ predict_kwargs["nms_thresh"] = nms_thresh
96
+
97
+ progress("Predicting instances")
98
+ labels, details = net.predict_instances(x, **predict_kwargs)
99
+
100
+ points = np.asarray(details.get("points", np.zeros((0, 3))))
101
+ return Nuclei(
102
+ labels=np.asarray(labels).astype(np.uint16),
103
+ points=points.astype(np.int32),
104
+ )
@@ -0,0 +1,138 @@
1
+ """UNSEG: unsupervised segmentation of nuclei and cells.
2
+
3
+ Segments tissue images carrying both a nucleus marker and a cell membrane
4
+ marker, without training data.
5
+
6
+ Ported from https://github.com/ctrueden/unseg-fiji. The algorithm lives in
7
+ _algorithm.py, which is imported inside the op body because its own imports
8
+ (OpenCV, scikit-learn, scikit-image) are far too heavy for discovery.
9
+
10
+ Uttam Lab. UNSEG: unsupervised segmentation of cells and their nuclei in
11
+ complex tissue samples.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from enum import Enum
17
+ from typing import Annotated, NamedTuple
18
+
19
+ import numpy as np
20
+
21
+ from opspec.op import op
22
+ from opspec.runtime import progress
23
+ from opspec.types import ImageOf, LabelsOf
24
+
25
+
26
+ class DistanceTransform(Enum):
27
+ """How to compute distances when splitting touching objects."""
28
+
29
+ geodesic = "GDT"
30
+ euclidean = "DT"
31
+
32
+
33
+ class TernaryMethod(Enum):
34
+ """How to resolve the three-way foreground/background/boundary decision."""
35
+
36
+ argmax = "Argmax"
37
+ posterior = "Posterior"
38
+
39
+
40
+ class Segmentation(NamedTuple):
41
+ nuclei: LabelsOf[np.ndarray]
42
+ cells: LabelsOf[np.ndarray]
43
+ n_nuclei: int
44
+ n_cells: int
45
+
46
+
47
+ @op(env="unseg-cv")
48
+ def unseg(
49
+ image: ImageOf[np.ndarray],
50
+ channel_axis: int = 0,
51
+ nuclei_channel: int = 2,
52
+ membrane_channel: int = 0,
53
+ area_threshold: Annotated[int, {"min": 1, "max": 10000}] = 20,
54
+ convexity_threshold: Annotated[int, {"min": 0, "max": 100}] = 4,
55
+ cell_marker_threshold: Annotated[int, {"min": 0, "max": 255}] = 25,
56
+ dist_tr: DistanceTransform = DistanceTransform.geodesic,
57
+ sigma0: Annotated[int, {"min": 0, "max": 20}] = 3,
58
+ k0: Annotated[int, {"min": 0, "max": 20}] = 1,
59
+ r0: Annotated[int, {"min": 1, "max": 50}] = 5,
60
+ pct: tuple[float, float] = (0.01, 0.01),
61
+ nk: tuple[int, ...] = (5, 10, 20, 40),
62
+ t0: Annotated[float, {"min": 0.0, "max": 1.0, "step": 0.05}] = 0.5,
63
+ ternary_met: TernaryMethod = TernaryMethod.argmax,
64
+ area_ratio_threshold: Annotated[
65
+ float, {"min": 0.0, "max": 1.0, "step": 0.05}
66
+ ] = 0.65,
67
+ dilation_radius: Annotated[int, {"min": 1, "max": 50}] = 5,
68
+ ) -> Segmentation:
69
+ """Segment nuclei and cells in a two-marker tissue image.
70
+
71
+ Args:
72
+ image: Multichannel 2D image carrying both markers.
73
+ channel_axis: Which axis of ``image`` holds the channels. Fiji and
74
+ napari usually hand over (C, Y, X), hence the default of 0.
75
+ nuclei_channel: Index of the nucleus marker channel, e.g. DAPI.
76
+ membrane_channel: Index of the cell membrane marker channel, e.g.
77
+ Na+/K+ ATPase.
78
+ area_threshold: Smallest object area to keep, in pixels.
79
+ convexity_threshold: How non-convex an object may be before UNSEG
80
+ tries to split it.
81
+ cell_marker_threshold: Membrane marker intensity below which a pixel
82
+ is not considered part of a cell.
83
+ dist_tr: Distance transform used when splitting touching objects.
84
+ sigma0: Smoothing width for the empirical probability estimate.
85
+ k0: Gradient-adaptive smoothing strength.
86
+ r0: Radius of the local binary mask neighborhood.
87
+ pct: Background removal percentiles, one per marker.
88
+ nk: Cluster counts to try when building the global binary mask.
89
+ t0: Threshold on the global binary mask.
90
+ ternary_met: How to resolve the three-way mask.
91
+ area_ratio_threshold: Nucleus-to-cell area ratio above which a cell is
92
+ rejected.
93
+ dilation_radius: How far to grow nuclei when seeding cells.
94
+
95
+ Returns:
96
+ Nucleus and cell label images, and the count of each.
97
+ """
98
+ from ._algorithm import nuclei_cell_segmentation
99
+
100
+ nuclei_marker = np.take(image, nuclei_channel, axis=channel_axis)
101
+ membrane_marker = np.take(image, membrane_channel, axis=channel_axis)
102
+ if nuclei_marker.ndim != 2:
103
+ raise ValueError(
104
+ f"UNSEG segments 2D images, but selecting a channel of a "
105
+ f"{image.ndim}D image left {nuclei_marker.ndim} dimensions. "
106
+ f"Check channel_axis (currently {channel_axis})."
107
+ )
108
+
109
+ intensity = np.zeros((*nuclei_marker.shape, 2), dtype="float64")
110
+ intensity[:, :, 0] = nuclei_marker
111
+ intensity[:, :, 1] = membrane_marker
112
+
113
+ progress(f"Segmenting {nuclei_marker.shape} image")
114
+ nuc_seg, cel_seg, n_nuclei, n_cells = nuclei_cell_segmentation(
115
+ intensity,
116
+ area_threshold=area_threshold,
117
+ convexity_threshold=convexity_threshold,
118
+ cell_marker_threshold=cell_marker_threshold,
119
+ dist_tr=dist_tr.value,
120
+ sigma0=sigma0,
121
+ k0=k0,
122
+ r0=r0,
123
+ pct=list(pct),
124
+ nk=list(nk),
125
+ t0=t0,
126
+ ternary_met=ternary_met.value,
127
+ # NB: no display in a worker process.
128
+ visualization=False,
129
+ area_ratio_threshold=area_ratio_threshold,
130
+ dilation_radius=dilation_radius,
131
+ )
132
+
133
+ return Segmentation(
134
+ nuclei=np.asarray(nuc_seg).astype(np.int32),
135
+ cells=np.asarray(cel_seg).astype(np.int32),
136
+ n_nuclei=int(n_nuclei),
137
+ n_cells=int(n_cells),
138
+ )