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,97 @@
1
+ """Find the objects, then draw each one: a detector paired with a masker.
2
+
3
+ The two-stage segmenter from docs/spec/workflow-ops.md, and the reason the
4
+ detectors and mask detectors were built to share signatures in the first
5
+ place. Any of the two detectors pairs with either of the two mask detectors,
6
+ which is four segmenters from four ops -- and the point of the pairing is that
7
+ they are genuinely different: the object-aware YOLO finds objects a
8
+ photograph-trained SAM misses, and micro_sam draws cells that MobileSAM
9
+ smears.
10
+
11
+ Both stages take the image. Only one asks for it: ``binds`` on each stage's
12
+ arguments names what this workflow supplies, so a front end renders the rest
13
+ and nothing gets typed twice. The masker's ``boxes`` are bound for the same
14
+ reason, though they come from the detector rather than from the panel.
15
+
16
+ Note that a pairing may cross environments -- ``fastsam`` in 'pytorch' with
17
+ ``mobilesam_masks`` in 'segment-everything' means two workers, and the image
18
+ crosses shared memory twice. That is the cost of composing on the host, and
19
+ it is why this is a workflow rather than an op.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from collections.abc import Callable
25
+ from typing import Annotated, NamedTuple
26
+
27
+ import numpy as np
28
+
29
+ from opspec.op import Choices, ParamsFor, op
30
+ from opspec.runtime import progress, run
31
+ from opspec.types import BoxesOf, ImageOf, MasksOf
32
+ from skop.ops.detect.fastsam import fastsam
33
+ from skop.ops.detect.object_aware_yolo import object_aware_yolo
34
+ from skop.ops.mask.microsam import microsam_masks
35
+ from skop.ops.mask.mobilesam import mobilesam_masks
36
+
37
+
38
+ class Detected(NamedTuple):
39
+ """What each stage produced, in the order the stages ran.
40
+
41
+ Stage order rather than importance: a front end adds these to a viewer in
42
+ declaration order, so the pipeline reads down the layer list, and the last
43
+ stage -- the answer -- ends up on top where a new layer belongs. This is
44
+ why the fields sit the other way round from ``skop.ops.mask.Masks``, which
45
+ every mask detector returns. There, the masks are the point and the boxes
46
+ record which prompts survived; here they are stage one and stage two.
47
+ """
48
+
49
+ #: The prompts that produced the masks: the detector's boxes, minus any
50
+ #: the mask detector answered with nothing.
51
+ boxes: BoxesOf[np.ndarray]
52
+ #: (N, Y, X) uint8, one object per plane, possibly overlapping.
53
+ masks: MasksOf[np.ndarray]
54
+
55
+
56
+ @op()
57
+ def detect_then_mask(
58
+ image: ImageOf[np.ndarray],
59
+ detector: Annotated[
60
+ Callable,
61
+ Choices(object_aware_yolo=object_aware_yolo, fastsam=fastsam),
62
+ ] = object_aware_yolo,
63
+ detector_args: Annotated[dict | None, ParamsFor("detector", binds="image")] = None,
64
+ masker: Annotated[
65
+ Callable,
66
+ Choices(mobilesam=mobilesam_masks, microsam=microsam_masks),
67
+ ] = mobilesam_masks,
68
+ masker_args: Annotated[
69
+ dict | None, ParamsFor("masker", binds=("image", "boxes"))
70
+ ] = None,
71
+ ) -> Detected:
72
+ """Detect objects and segment each one.
73
+
74
+ Args:
75
+ image: The image to segment.
76
+ detector: Which op finds the bounding boxes. ``object_aware_yolo`` was
77
+ trained on microscopy; ``fastsam`` is a general detector.
78
+ detector_args: Settings for the chosen detector, minus the image.
79
+ masker: Which op draws a mask inside each box. ``microsam`` is
80
+ finetuned for microscopy and slower; ``mobilesam`` is the one the
81
+ object-aware detector was trained to pair with.
82
+ masker_args: Settings for the chosen mask detector, minus the image
83
+ and the boxes, which this workflow supplies.
84
+
85
+ Returns:
86
+ boxes: the box each mask answers.
87
+ masks: (N, Y, X) uint8, one object per plane, possibly overlapping.
88
+
89
+ A detector that finds nothing is an ordinary outcome, not a failure: the
90
+ mask detector is skipped and the result is empty.
91
+ """
92
+ progress("Detecting objects")
93
+ boxes = run(detector, image=image, **(detector_args or {})).boxes
94
+
95
+ progress(f"Segmenting {len(boxes)} objects")
96
+ found = run(masker, image=image, boxes=boxes, **(masker_args or {}))
97
+ return Detected(found.boxes, found.masks)
@@ -0,0 +1,7 @@
1
+ """Workflows that produce a label image."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .connect_2d_in_3d import Connected, connect_2d_in_3d
6
+
7
+ __all__ = ["Connected", "connect_2d_in_3d"]
@@ -0,0 +1,145 @@
1
+ """Segment a volume plane by plane, then join the planes back into objects.
2
+
3
+ The workflow ``skop.ops.normalize`` was written for. A 2-D segmenter handed a
4
+ volume is looped over its planes, and every one of these segmenters normalizes
5
+ whatever array it is given -- so each plane gets stretched to its own range,
6
+ and a top or bottom plane holding nothing but out-of-focus blur is stretched
7
+ until that blur fills the range and reads as nuclei. The segmenter then finds
8
+ objects there that are not there.
9
+
10
+ Three ops in a row fix it, and the order is the whole point:
11
+
12
+ 1. normalize over the **whole volume**, so an empty plane stays dim relative
13
+ to the planes with signal in them.
14
+ 2. the segmenter, **plane by plane and with its own normalization off**, so
15
+ nothing puts the stretch back.
16
+ 3. connect, which links objects that overlap between adjacent planes, so one
17
+ cell stops being a stack of unrelated labels.
18
+
19
+ Step 2 is why ``normalize`` is bound rather than offered. Leaving a checkbox
20
+ there would let someone switch back on precisely the behaviour this exists to
21
+ avoid, and get a result that looks like a failure of the idea rather than of
22
+ the setting. Running a segmenter with its own normalization on is still a
23
+ perfectly good thing to do -- from the Ops panel, where that is what you asked
24
+ for.
25
+
26
+ **All three stages are choosers, including the two with one choice each.**
27
+ There is only one normalizer and one connector today and there will be more,
28
+ and a stage that starts as a chooser gains its second option by having a name
29
+ added to a list rather than by being rewritten. It also keeps the shape
30
+ uniform: every stage's settings come from the chosen op's own signature, so
31
+ nothing here restates a parameter that lives somewhere else and can drift from
32
+ it.
33
+
34
+ **Three dimensions, and the dimensions are held fixed.** The connector needs
35
+ an axis to walk along, so there is no 2-D version of this. The workflow
36
+ declares ``Axes("z", "y", "x")`` on its own input, which is what lets a front
37
+ end ask which of *your* axes is z -- but it does not pass that question down.
38
+ Each stage's axis handling is pinned here, because getting it wrong is the bug
39
+ the workflow is for.
40
+ """
41
+
42
+ from __future__ import annotations
43
+
44
+ from collections.abc import Callable
45
+ from typing import Annotated, NamedTuple
46
+
47
+ import numpy as np
48
+
49
+ from opspec.op import Axes, Choices, ParamsFor, op
50
+ from opspec.runtime import progress, run
51
+ from opspec.types import ImageOf, LabelsOf
52
+ from skop.ops.labels import connect
53
+ from skop.ops.normalize import percentile
54
+ from skop.ops.segment.cellpose3 import cellpose3
55
+ from skop.ops.segment.cellpose4 import cellpose4
56
+ from skop.ops.segment.stardist2d import stardist2d_fluo
57
+
58
+ #: What every stage is handed, and what the workflow refuses to make adjustable.
59
+ _ZYX = list("zyx")
60
+
61
+
62
+ class Connected(NamedTuple):
63
+ """What each stage produced, in the order the stages ran.
64
+
65
+ Stage order rather than importance: a front end adds these to a viewer in
66
+ declaration order, so the pipeline reads down the layer list, and the last
67
+ stage -- the answer -- ends up on top where a new layer belongs.
68
+ """
69
+
70
+ #: The normalized volume the segmenter actually saw. Returned because it
71
+ #: is the evidence: if the top plane is still dim here, the normalization
72
+ #: did its job, and a spurious object there is the segmenter's doing.
73
+ normalized: ImageOf[np.ndarray]
74
+ #: One label per object, consistent through the stack.
75
+ labels: LabelsOf[np.ndarray]
76
+
77
+
78
+ @op()
79
+ def connect_2d_in_3d(
80
+ image: Annotated[ImageOf[np.ndarray], Axes("z", "y", "x")],
81
+ normalizer: Annotated[Callable, Choices(percentile=percentile)] = percentile,
82
+ normalizer_args: Annotated[
83
+ dict | None, ParamsFor("normalizer", binds="image")
84
+ ] = None,
85
+ segmenter: Annotated[
86
+ Callable,
87
+ Choices(stardist=stardist2d_fluo, cellpose4=cellpose4, cellpose3=cellpose3),
88
+ ] = stardist2d_fluo,
89
+ segmenter_args: Annotated[
90
+ dict | None, ParamsFor("segmenter", binds=("image", "normalize"))
91
+ ] = None,
92
+ connector: Annotated[Callable, Choices(connect=connect)] = connect,
93
+ connector_args: Annotated[
94
+ dict | None, ParamsFor("connector", binds="labels")
95
+ ] = None,
96
+ ) -> Connected:
97
+ """Segment a volume plane by plane and join the planes into objects.
98
+
99
+ Args:
100
+ image: The volume, as z, y, x. Normalized once, whole, before any
101
+ plane is segmented.
102
+ normalizer: Which op scales the volume. Whichever it is, it is run on
103
+ the whole volume rather than plane by plane -- that is the step
104
+ this workflow exists to get right.
105
+ normalizer_args: Settings for the chosen normalizer, minus the volume.
106
+ segmenter: Which 2-D segmenter to run on each plane. Each brings its
107
+ own environment, and picking one for the first time builds it.
108
+ segmenter_args: Settings for the chosen segmenter, minus the plane and
109
+ its normalization, both of which this workflow supplies.
110
+ connector: Which op links objects between adjacent planes.
111
+ connector_args: Settings for the chosen connector, minus the labels.
112
+
113
+ Returns:
114
+ normalized: The volume as the segmenter saw it.
115
+ labels: The connected label volume, numbered from 1.
116
+
117
+ Note: the three stages live in three environments, so a run crosses shared
118
+ memory three times. That is the cost of composing on the host, and it is
119
+ paid per call rather than per plane -- skop loops the segmenter over z
120
+ inside its own worker.
121
+ """
122
+ progress("Normalizing the whole volume")
123
+ # Naming the axes hands a variadic op the volume entire rather than a
124
+ # plane at a time. This is the step the whole workflow exists for.
125
+ normalized = run(
126
+ normalizer, image=image, axes={"image": _ZYX}, **(normalizer_args or {})
127
+ )
128
+
129
+ progress("Segmenting plane by plane")
130
+ # The segmenter declares two axes and is given three, so skop loops it over
131
+ # z. normalize=False is what keeps it from undoing the step above.
132
+ labels = run(
133
+ segmenter,
134
+ image=normalized,
135
+ normalize=False,
136
+ axes={"image": _ZYX},
137
+ **(segmenter_args or {}),
138
+ )
139
+
140
+ progress("Connecting objects between planes")
141
+ joined = run(
142
+ connector, labels=labels, axes={"labels": _ZYX}, **(connector_args or {})
143
+ )
144
+
145
+ return Connected(normalized, joined)