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,189 @@
1
+ """Finetune one of Cellpose 3's models on your own patches.
2
+
3
+ Runs in the 'cellpose3' environment beside ``skop.ops.segment.cellpose3``.
4
+ Separate from ``train_cellpose`` for the same reason the inference ops are
5
+ separate: version 4 replaced the model zoo and moved the API, so no pin makes
6
+ the two coexist.
7
+
8
+ The channel settings are the reason to read this rather than assume it
9
+ matches CPSAM. Cellpose 3 takes two input planes, cytoplasm and optional
10
+ nuclei, and it takes them at training time as well as at inference. The two
11
+ must agree -- see ``cytoplasm_channel`` below.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import Annotated
17
+
18
+ from opspec.op import op
19
+ from opspec.runtime import progress
20
+ from skop.ops.segment.cellpose3 import (
21
+ _CHANNEL_NUMBER,
22
+ CytoplasmChannel,
23
+ NucleusChannel,
24
+ PretrainedModel,
25
+ )
26
+
27
+ from ._util import read_pairs, relay_epochs, write_history
28
+
29
+
30
+ @op(env="cellpose3")
31
+ def train_cellpose3(
32
+ images: list[str],
33
+ labels: list[str],
34
+ model_dir: str,
35
+ name: str = "cellpose3_model",
36
+ model: PretrainedModel = PretrainedModel.cyto3,
37
+ initial_model: str = "",
38
+ cytoplasm_channel: CytoplasmChannel = CytoplasmChannel.grayscale,
39
+ nucleus_channel: NucleusChannel = NucleusChannel.none,
40
+ epochs: Annotated[int, {"widget_type": "SpinBox", "min": 1, "max": 5000}] = 100,
41
+ learning_rate: float = 0.005,
42
+ weight_decay: float = 1e-5,
43
+ batch_size: Annotated[int, {"widget_type": "SpinBox", "min": 1, "max": 64}] = 8,
44
+ patch_size: Annotated[
45
+ int, {"widget_type": "SpinBox", "min": 64, "max": 1024, "step": 32}
46
+ ] = 224,
47
+ min_train_masks: Annotated[int, {"widget_type": "SpinBox", "min": 0, "max": 100}] = 5,
48
+ normalize: bool = True,
49
+ rescale: bool = True,
50
+ nimg_per_epoch: Annotated[int, {"widget_type": "SpinBox", "min": 0, "max": 100000}] = 0,
51
+ val_size: Annotated[int, {"widget_type": "SpinBox", "min": 1, "max": 100}] = 2,
52
+ dataset_id: str = "",
53
+ ) -> str:
54
+ """Finetune a Cellpose 3 model and write it to disk.
55
+
56
+ Args:
57
+ images: Paths to the input patches, in order.
58
+ labels: Paths to the label patches. ``labels[k]`` is the truth for
59
+ ``images[k]`` -- the pairing is positional, and the caller owns it.
60
+ model_dir: Directory to train into. Cellpose puts the model in a
61
+ ``models/`` subdirectory of it, which is its convention and is
62
+ left alone here.
63
+ name: Filename for the trained model.
64
+ model: Which built-in to start from. cyto3 is the generalist; a
65
+ specialist trained on your imaging domain starts closer and needs
66
+ fewer patches. Ignored when ``initial_model`` is set.
67
+ initial_model: A model of your own to continue from, which takes
68
+ precedence over ``model``, and what decides whether the history
69
+ continues or starts over: pass the path this op returned to carry
70
+ on, leave it empty to start again. It must be a Cellpose 1-3 model, not a
71
+ CPSAM one; ``skop.models.cellpose_flavor`` tells them apart from
72
+ the file.
73
+ cytoplasm_channel: Which colour the cells are in. Left at
74
+ ``grayscale``, the colours are averaged into one plane before
75
+ Cellpose sees them. **Whatever you choose here you must pass
76
+ again to** ``skop.ops.segment.cellpose3``: the two input planes
77
+ are what the network learned, so a model trained on green
78
+ cytoplasm and run on a grey average is being shown something it
79
+ never saw.
80
+ nucleus_channel: Which colour the nuclei are in. Ignored unless
81
+ ``cytoplasm_channel`` names a colour too -- Cellpose reads the
82
+ pair, and a grey first slot drops the second.
83
+ epochs: Passes over the training set. Cellpose's own default is
84
+ 2000, which is a from-scratch number; finetuning a zoo model
85
+ wants far fewer.
86
+ learning_rate: Cellpose 3's default. Three orders larger than
87
+ CPSAM's, because this is a far smaller network.
88
+ weight_decay: L2 penalty on the weights.
89
+ batch_size: Patches per batch.
90
+ patch_size: Size of the crops training samples from the patches.
91
+ min_train_masks: Skip patches with fewer masks than this. 0 keeps
92
+ everything -- which is what you want when empty patches are
93
+ themselves the lesson.
94
+ normalize: Whether to percentile-normalize each patch first.
95
+ rescale: Resize each patch so its objects match the diameter the
96
+ network expects. On by default, and worth keeping: it is what
97
+ lets a model trained at one magnification work at another. Turn
98
+ it off when your objects are all one size and you would rather
99
+ the network learn that size.
100
+ nimg_per_epoch: How many patches make up one epoch. 0 means all of
101
+ them, which is Cellpose's own behaviour. Set it lower when the
102
+ training set is large enough that a pass over all of it is too
103
+ coarse a unit to watch -- 10000 patches is one very long epoch,
104
+ where 50 an epoch gives a loss curve that moves.
105
+ val_size: How many pairs, taken from the end, to hold out for
106
+ validation.
107
+ dataset_id: Opaque label for the training data, recorded in the
108
+ history file so a plot can mark where it changed.
109
+
110
+ Returns:
111
+ The path of the model file. A ``<model file>_history.csv`` is written
112
+ beside it -- epoch, loss, val_loss, run, model, dataset -- appended to
113
+ when ``initial_model`` continues this same model, so the curve spans
114
+ every run the weights have had. Any other run starts the file over,
115
+ because Cellpose has just overwritten the model the old curve
116
+ described. To keep both, train the second under a different ``name``.
117
+
118
+ The mean diameter of the training labels is saved into the model as
119
+ ``diam_labels``. That is what ``skop.ops.segment.cellpose3`` falls
120
+ back to when it is given ``diameter=0`` and a model of yours: a
121
+ finetuned checkpoint carries no size model, so the size it was
122
+ trained at is the best answer available.
123
+ """
124
+ from pathlib import Path
125
+
126
+ import numpy as np
127
+ from cellpose import models, train
128
+
129
+ # Cellpose makes `<model_dir>/models` without parents.
130
+ Path(model_dir).mkdir(parents=True, exist_ok=True)
131
+
132
+ X_train, Y_train, X_val, Y_val = read_pairs(images, labels, val_size)
133
+
134
+ # Always passed: without `channels` the training reshape is skipped and
135
+ # a 2-D image reaches normalize_img, which wants a plane axis.
136
+ select = {
137
+ "channels": [
138
+ _CHANNEL_NUMBER[cytoplasm_channel.value],
139
+ _CHANNEL_NUMBER[nucleus_channel.value],
140
+ ]
141
+ }
142
+
143
+ # int32 arrives from label arithmetic upstream; normalization chokes on it.
144
+ X_train = [np.asarray(x).astype(np.float32) for x in X_train]
145
+ X_val = [np.asarray(x).astype(np.float32) for x in X_val]
146
+ Y_train = [np.asarray(y).astype(np.uint16) for y in Y_train]
147
+ Y_val = [np.asarray(y).astype(np.uint16) for y in Y_val]
148
+
149
+ # CellposeModel, not Cellpose: the SizeModel it bundles trains separately.
150
+ if initial_model:
151
+ source = initial_model
152
+ build = dict(pretrained_model=initial_model)
153
+ else:
154
+ source = model.value
155
+ build = dict(model_type=model.value)
156
+
157
+ progress(f"Loading Cellpose 3 model {source}")
158
+ net = models.CellposeModel(gpu=True, **build).net
159
+
160
+ progress(
161
+ f"Training {name}: {len(X_train)} train, {len(X_val)} val, "
162
+ f"{epochs} epochs",
163
+ 0,
164
+ epochs,
165
+ )
166
+ with relay_epochs(epochs) as relay:
167
+ model_path, _, _ = train.train_seg(
168
+ net,
169
+ train_data=X_train,
170
+ train_labels=Y_train,
171
+ test_data=X_val,
172
+ test_labels=Y_val,
173
+ n_epochs=epochs,
174
+ learning_rate=learning_rate,
175
+ weight_decay=weight_decay,
176
+ batch_size=batch_size,
177
+ bsize=patch_size,
178
+ min_train_masks=min_train_masks,
179
+ normalize=normalize,
180
+ rescale=rescale,
181
+ # None, not 0: Cellpose reads None as "every image".
182
+ nimg_per_epoch=nimg_per_epoch or None,
183
+ save_path=model_dir,
184
+ model_name=name,
185
+ **select,
186
+ )
187
+
188
+ write_history(model_path, relay.history, name, dataset_id, initial_model)
189
+ return str(model_path)
@@ -0,0 +1,164 @@
1
+ """Finetune CellposeSAM on your own patches.
2
+
3
+ Runs in the shared 'pytorch' environment, beside
4
+ ``skop.ops.segment.cellpose4`` -- so a caller that trains and then segments
5
+ builds one environment, not two.
6
+
7
+ Cellpose 3's trainer is the exception, in ``cellpose3.py`` and its own
8
+ environment, for the same reason the inference ops are split.
9
+
10
+ Takes paths and returns a path, like every training op (design 0011). What
11
+ comes back is a **file**, not a directory: that is how Cellpose saves a
12
+ model, and it is why a caller listing trained models lists files here where
13
+ StarDist would have it list directories.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import Annotated
19
+
20
+ from opspec.op import op
21
+ from opspec.runtime import progress
22
+ from skop.ops.segment.cellpose4 import PretrainedModel
23
+
24
+ from .._util import channel_axis, to_gray
25
+ from ._util import read_pairs, relay_epochs, write_history
26
+
27
+
28
+ @op(env="pytorch")
29
+ def train_cellpose4(
30
+ images: list[str],
31
+ labels: list[str],
32
+ model_dir: str,
33
+ name: str = "cellpose4_model",
34
+ model: PretrainedModel = PretrainedModel.cpsam_v2,
35
+ initial_model: str = "",
36
+ epochs: Annotated[int, {"widget_type": "SpinBox", "min": 1, "max": 5000}] = 100,
37
+ learning_rate: float = 1e-5,
38
+ weight_decay: float = 0.1,
39
+ batch_size: Annotated[int, {"widget_type": "SpinBox", "min": 1, "max": 64}] = 1,
40
+ patch_size: Annotated[
41
+ int, {"widget_type": "SpinBox", "min": 64, "max": 1024, "step": 32}
42
+ ] = 256,
43
+ min_train_masks: Annotated[int, {"widget_type": "SpinBox", "min": 0, "max": 100}] = 5,
44
+ normalize: bool = True,
45
+ rescale: bool = False,
46
+ nimg_per_epoch: Annotated[int, {"widget_type": "SpinBox", "min": 0, "max": 100000}] = 0,
47
+ collapse_channels: bool = False,
48
+ val_size: Annotated[int, {"widget_type": "SpinBox", "min": 1, "max": 100}] = 2,
49
+ dataset_id: str = "",
50
+ ) -> str:
51
+ """Finetune CPSAM and write the model to disk.
52
+
53
+ Args:
54
+ images: Paths to the input patches, in order.
55
+ labels: Paths to the label patches. ``labels[k]`` is the truth for
56
+ ``images[k]`` -- the pairing is positional, and the caller owns it.
57
+ model_dir: Directory to train into. Cellpose puts the model in a
58
+ ``models/`` subdirectory of it, which is its convention and is
59
+ left alone here.
60
+ name: Filename for the trained model.
61
+ model: Which built-in to finetune. ``cpdino_vitb`` is the small
62
+ backbone and the one to reach for when the others run the GPU
63
+ out of memory. Ignored when ``initial_model`` is set.
64
+ initial_model: A model of your own to continue from, and what
65
+ decides whether the history continues or starts over: pass the
66
+ path this op returned to carry on, leave it empty to start again.
67
+ Empty starts
68
+ from the built-in named by ``model``, which is the usual thing
69
+ to do -- these are large models and a handful of patches
70
+ finetunes one, where they would not train it from nothing. It
71
+ must be a Cellpose 4 model, not a Cellpose 3 one, and it brings
72
+ its own backbone.
73
+ epochs: Passes over the training set.
74
+ learning_rate: Cellpose's default for finetuning CPSAM, and low on
75
+ purpose: the weights being adjusted are already good.
76
+ weight_decay: L2 penalty on the weights.
77
+ batch_size: Patches per batch. 1 by default because CPSAM is large
78
+ and the memory is the constraint; raise it if the card allows.
79
+ patch_size: Size of the crops training samples from the patches.
80
+ min_train_masks: Skip patches with fewer masks than this. 0 keeps
81
+ everything -- which is what you want when empty patches are
82
+ themselves the lesson.
83
+ normalize: Whether to percentile-normalize each patch first.
84
+ rescale: Resize each patch so its objects match the diameter the
85
+ network expects. Off by default here, unlike Cellpose 3: CPSAM
86
+ was trained across scales and does not need it.
87
+ nimg_per_epoch: How many patches make up one epoch. 0 means all of
88
+ them, which is Cellpose's own behaviour. Set it lower when the
89
+ training set is large enough that a pass over all of it is too
90
+ coarse a unit to watch.
91
+ collapse_channels: Average a trailing RGB(A) axis to grey before
92
+ training. Must match what you pass ``skop.ops.segment.cellpose4``
93
+ afterwards: a model finetuned on grey and then run on colour is
94
+ being shown something it never saw.
95
+ val_size: How many pairs, taken from the end, to hold out for
96
+ validation.
97
+ dataset_id: Opaque label for the training data, recorded in the
98
+ history file so a plot can mark where it changed.
99
+
100
+ Returns:
101
+ The path of the model file. A ``<model file>_history.csv`` is written
102
+ beside it -- epoch, loss, val_loss, run, model, dataset -- appended to
103
+ when ``initial_model`` continues this same model, so the curve spans
104
+ every run the weights have had. Any other run starts the file over,
105
+ because Cellpose has just overwritten the model the old curve
106
+ described. To keep both, train the second under a different ``name``.
107
+ """
108
+ from pathlib import Path
109
+
110
+ import numpy as np
111
+ from cellpose import models, train
112
+
113
+ # Cellpose makes `<model_dir>/models` without parents.
114
+ Path(model_dir).mkdir(parents=True, exist_ok=True)
115
+
116
+ X_train, Y_train, X_val, Y_val = read_pairs(images, labels, val_size)
117
+
118
+ if collapse_channels:
119
+ X_train = [to_gray(x) for x in X_train]
120
+ X_val = [to_gray(x) for x in X_val]
121
+ axis = None
122
+ else:
123
+ axis = channel_axis(X_train[0])
124
+
125
+ # Labels reach here as whatever the patch writer used; the flow
126
+ # computation wants a plain integer image.
127
+ Y_train = [np.asarray(y).astype(np.uint16) for y in Y_train]
128
+ Y_val = [np.asarray(y).astype(np.uint16) for y in Y_val]
129
+
130
+ # A finetuned checkpoint carries its own backbone.
131
+ source = initial_model or model.value
132
+ progress(f"Loading Cellpose model {source}")
133
+ net = models.CellposeModel(gpu=True, pretrained_model=source).net
134
+
135
+ progress(
136
+ f"Training {name}: {len(X_train)} train, {len(X_val)} val, "
137
+ f"{epochs} epochs",
138
+ 0,
139
+ epochs,
140
+ )
141
+ with relay_epochs(epochs) as relay:
142
+ model_path, _, _ = train.train_seg(
143
+ net,
144
+ train_data=X_train,
145
+ train_labels=Y_train,
146
+ test_data=X_val,
147
+ test_labels=Y_val,
148
+ channel_axis=axis,
149
+ n_epochs=epochs,
150
+ learning_rate=learning_rate,
151
+ weight_decay=weight_decay,
152
+ batch_size=batch_size,
153
+ bsize=patch_size,
154
+ min_train_masks=min_train_masks,
155
+ normalize=normalize,
156
+ rescale=rescale,
157
+ # None, not 0: Cellpose reads None as "every image".
158
+ nimg_per_epoch=nimg_per_epoch or None,
159
+ save_path=model_dir,
160
+ model_name=name,
161
+ )
162
+
163
+ write_history(model_path, relay.history, name, dataset_id, initial_model)
164
+ return str(model_path)