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,372 @@
1
+ Metadata-Version: 2.5
2
+ Name: scikit-ops
3
+ Version: 0.1.0.dev0
4
+ Summary: Appose-powered ops, and the machinery for running them.
5
+ Project-URL: homepage, https://github.com/apposed/scikit-ops
6
+ Project-URL: documentation, https://github.com/apposed/scikit-ops/blob/main/README.md
7
+ Project-URL: source, https://github.com/apposed/scikit-ops
8
+ Project-URL: download, https://pypi.org/project/scikit-ops
9
+ Project-URL: tracker, https://github.com/apposed/appose/issues
10
+ Author: scikit-ops developers
11
+ License-Expression: BSD-2-Clause
12
+ Classifier: Development Status :: 2 - Pre-Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Education
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: Operating System :: MacOS
17
+ Classifier: Operating System :: Microsoft :: Windows
18
+ Classifier: Operating System :: Unix
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
25
+ Classifier: Topic :: Scientific/Engineering
26
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
27
+ Classifier: Topic :: Scientific/Engineering :: Image Recognition
28
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
29
+ Classifier: Topic :: Utilities
30
+ Requires-Python: >=3.10
31
+ Requires-Dist: numpy
32
+ Description-Content-Type: text/markdown
33
+
34
+ # scikit-ops
35
+
36
+ A collection of image-processing **ops**, and `skop` — the machinery that runs
37
+ them.
38
+
39
+ An op is an ordinary Python function. The same function can be called directly,
40
+ run in its own isolated environment through
41
+ [Appose](https://github.com/apposed/appose), or wrapped in a GUI. Nothing about
42
+ the op changes between those modes.
43
+
44
+ Design notes — why the machinery is shaped this way, and what else was
45
+ considered — are in [`docs/`](docs/README.md). How an op describes itself is
46
+ [opspec](opspec/README.md), a separate package kept in this repository.
47
+
48
+ ## Install
49
+
50
+ Not on PyPI yet, so from GitHub:
51
+
52
+ ```sh
53
+ pip install "git+https://github.com/apposed/scikit-ops"
54
+ ```
55
+
56
+ That brings opspec with it. opspec on its own, standard library only, for
57
+ a host or plugin that describes ops without running them:
58
+
59
+ ```sh
60
+ pip install "opspec @ git+https://github.com/apposed/scikit-ops#subdirectory=opspec"
61
+ ```
62
+
63
+ Working on the code instead, from a checkout: `uv sync`, which installs both,
64
+ editable. See [Development](#development).
65
+
66
+ ## Layout
67
+
68
+ ```
69
+ envs/<env-id>/pixi.toml environment definitions, named and shared between ops
70
+ envs/<env-id>/init.py optional; runs in each worker before its I/O loop
71
+ src/skop/ op-independent machinery
72
+ src/skop/ops/<ns>.py an op namespace, when its ops share one environment
73
+ src/skop/ops/<ns>/<op>.py an op namespace, when its ops do not
74
+ opspec/ how an op describes itself: @op, OpSpec, roles, plans
75
+ src/imgops/ napari/magicgui front ends (not published)
76
+ ```
77
+
78
+ Environments are keyed by ID, and several ops may declare the same one. Ops
79
+ sharing an environment also share a warm worker process, unless one asks for
80
+ `exclusive=True`.
81
+
82
+ ## What's here
83
+
84
+ | Op | Environment | Does |
85
+ | --- | --- | --- |
86
+ | `skop.ops.threshold:*` | `skimage` | Global thresholding: Otsu, isodata, Li, mean, minimum, triangle, Yen, multi-Otsu |
87
+ | `skop.ops.smooth:*` | `skimage` | Smoothing and denoising: Gaussian, median, mean, Kuwahara, bilateral, TV, wavelet, NL-means, Butterworth |
88
+ | `skop.ops.edges:*` | `skimage` | Derivative filters: Sobel, Scharr, Prewitt, Farid, Roberts, Laplace, DoG, unsharp mask, Frangi, Sato, Meijering |
89
+ | `skop.ops.morphology:*` | `skimage` | Grayscale morphology: erosion, dilation, opening, closing, white/black tophat |
90
+ | `skop.ops.kernels.psf:gaussian_psf` | `skimage` | Gaussian PSF, 2D or 3D |
91
+ | `skop.ops.kernels.paraxial:paraxial_psf` | `skimage` | Paraxial PSF, from wavelength and NA |
92
+ | `skop.ops.kernels.paraxial:paraxial_otf` | `skimage` | Paraxial OTF, the transform of the above |
93
+ | `skop.ops.kernels.gibson_lanni:gibson_lanni` | `sdeconv` | Gibson-Lanni 3D PSF, with spherical aberration |
94
+ | `skop.ops.deconvolve.richardson_lucy:richardson_lucy` | `skimage` | Richardson-Lucy, on the CPU |
95
+ | `skop.ops.deconvolve.richardson_lucy_cupy:richardson_lucy_cupy` | `cupy` | Richardson-Lucy, on an NVIDIA GPU |
96
+ | `skop.ops.segment.cellpose:cellpose` | `cellpose` | Cellpose segmentation |
97
+ | `skop.ops.segment.stardist2d:stardist2d_fluo` | `stardist-tf` | StarDist 2D, pretrained fluorescence |
98
+ | `skop.ops.segment.stardist2d:stardist2d_he` | `stardist-tf` | StarDist 2D, pretrained H&E |
99
+ | `skop.ops.segment.stardist2d:stardist2d_custom` | `stardist-tf` | StarDist 2D, model loaded from a directory |
100
+ | `skop.ops.train.stardist2d:train_stardist2d` | `stardist-tf` | StarDist 2D training, file pairs in, model path out |
101
+ | `skop.ops.segment.starfun3d:segment_nuclei` | `stardist-tf` | StarDist 3D nuclei |
102
+ | `skop.ops.labels:connect` | `skimage` | One label per object along an axis, by linking overlaps |
103
+ | `skop.ops.generate:synthetic_nuclei` | `skimage` | Synthetic 3D test volume |
104
+ | `skop.ops.segment.unseg:unseg` | `unseg-cv` | Unsupervised nuclei + cells |
105
+ | `skop.ops.detect.fastsam:fastsam` | `pytorch` | Class-agnostic boxes, via FastSAM |
106
+ | `skop.ops.detect.object_aware_yolo:object_aware_yolo` | `segment-everything` | Class-agnostic boxes, via MobileSAMv2 |
107
+ | `skop.ops.detect.yolo:yolo` | `pytorch` | Tiled YOLO detection, with automatic GPU batching, confidences and classes |
108
+ | `skop.ops.toy:*` | `minimal` | Exercises for skop itself |
109
+
110
+ Those are the fully qualified op IDs, as `discover()` reports them. Callers
111
+ import from the namespace instead, and never need to know whether it is one
112
+ module or a package of them:
113
+
114
+ ```python
115
+ from skop.ops.threshold import otsu
116
+ from skop.ops.segment import cellpose, stardist2d_fluo
117
+ ```
118
+
119
+ `stardist2d_fluo` and `segment_nuclei` share one TensorFlow build and one warm
120
+ worker — the point of naming environments rather than tying them to ops.
121
+ `unseg-cv` shares nothing with anything: it pins Python 3.9, numpy 1.24 and an
122
+ old scikit-image, which is exactly why it needs an environment of its own.
123
+
124
+ The two Richardson-Lucy ops are the same algorithm against different hardware,
125
+ and there are two of them because `@op(env=...)` is fixed per function — a
126
+ backend cannot be chosen at call time. Their signatures are identical and a
127
+ test holds them numerically equivalent, so picking one is picking on speed.
128
+
129
+ To list what the collection currently offers:
130
+
131
+ ```python
132
+ import oprunner
133
+
134
+ specs, failures = oprunner.discover()
135
+ ```
136
+
137
+ For a large image and a YOLO `.pt` model:
138
+
139
+ ```python
140
+ from pathlib import Path
141
+ from skop.ops.detect import yolo
142
+
143
+ with oprunner.Runner() as runner:
144
+ found = runner.run(
145
+ yolo, image=image, weights=Path("best.pt"),
146
+ object_size=100, overlap=0.15, gpu_fraction=0.9,
147
+ )
148
+ # found.boxes: [min_y, min_x, max_y, max_x]
149
+ # found.confidences and found.classes: one value per box
150
+ ```
151
+
152
+ `object_size` is area in pixels²; `None` runs the whole image once. Areas
153
+ outside 0.1%-50% of the image select square tiles with side
154
+ `ceil(sqrt(1.5 * object_size / 0.001))`. Input size comes from the checkpoint.
155
+ Tiles align with the image edges. Class-aware GreedyNMM matches SAHI 0.12.8:
156
+ same-class boxes with IoS at least `merge_threshold` become an enclosing box
157
+ with the maximum confidence. This includes matches within a tile. The default
158
+ merge threshold is 0.5, independent of the per-tile NMS `iou` parameter.
159
+ Outputs follow class order, then SAHI's keeper order within each class.
160
+ A multiscale grid finds nearby candidates without an all-pairs matrix; the
161
+ runtime needs no SAHI dependency. To run the optional reference comparisons:
162
+
163
+ ```sh
164
+ uv run --with sahi==0.12.8 pytest tests/test_yolo.py -k sahi
165
+ ```
166
+
167
+ GPU batches are calibrated after warm-up, with intermediate sizes before
168
+ larger batches. Measured peaks adjust the size up or down; an out-of-memory
169
+ retry establishes an upper bound rather than permanently halving throughput.
170
+ CUDA measures the reserved allocator footprint; MPS samples driver memory.
171
+ One CPU batch is prepared ahead of inference, using pinned buffers for CUDA.
172
+ Inference transfers only detection data back to the host. The image must fit
173
+ in host RAM; only the current resized batch enters the GPU.
174
+
175
+ ## Writing an op
176
+
177
+ ```python
178
+ from typing import Annotated, NamedTuple
179
+
180
+ import numpy as np
181
+ from opspec.op import op
182
+ from opspec.types import ImageOf, LabelsOf
183
+
184
+
185
+ class Result(NamedTuple):
186
+ labels: LabelsOf[np.ndarray]
187
+ count: int
188
+
189
+
190
+ @op(env="stardist-tf")
191
+ def segment(
192
+ image: ImageOf[np.ndarray],
193
+ prob_thresh: Annotated[
194
+ float, {"widget_type": "FloatSlider", "min": 0.0, "max": 1.0}
195
+ ] = 0.5,
196
+ ) -> Result:
197
+ """Segment nuclei."""
198
+ from stardist.models import StarDist3D # NB: heavy imports go in the body.
199
+
200
+ ...
201
+ ```
202
+
203
+ Two rules, and both exist for the same reason — ops are discovered by importing
204
+ them into a minimal environment that has little more than numpy:
205
+
206
+ 1. **Heavy imports go inside the function body.** A module-scope
207
+ `import tensorflow` makes the op undiscoverable.
208
+ 2. **Type your inputs and outputs with plain types.** Annotations *are*
209
+ evaluated at discovery time, so `-> tf.Tensor` fails even if the import is
210
+ in the body.
211
+
212
+ Multiple outputs come from a `NamedTuple`: its field names become the names of
213
+ the op's outputs. `progress(...)` and `cancel_requested()`, from
214
+ `opspec.runtime`, report to whoever is running the op, and do nothing when it
215
+ is called directly.
216
+
217
+ ### Saying what an array is
218
+
219
+ Every op here passes `np.ndarray` around, which tells a front end nothing
220
+ about how to display it. opspec's types add that missing half:
221
+
222
+ | Write | Role | napari shows it as |
223
+ | --- | --- | --- |
224
+ | `ImageOf[np.ndarray]` | `Role.image` | an Image layer |
225
+ | `LabelsOf[np.ndarray]` | `Role.labels` | a Labels layer |
226
+ | `PointsOf[np.ndarray]` | `Role.points` | a Points layer |
227
+ | `VectorsOf[np.ndarray]` | `Role.vectors` | a Vectors layer |
228
+ | `TracksOf[np.ndarray]` | `Role.tracks` | a Tracks layer |
229
+ | `BoxesOf[np.ndarray]` | `Role.boxes` | a Shapes layer |
230
+ | `MasksOf[np.ndarray]` | `Role.masks` | a Labels layer, once projected |
231
+
232
+ `MasksOf` is the odd one: an `(N, Y, X)` stack of masks that may overlap, so
233
+ it is not a label image and no viewer shows it directly. A front end projects
234
+ it with `ophelper.masks` — flattened to 2-D, or kept as a rotatable 3-D stack —
235
+ and gets to offer that as a choice, since re-projecting costs a numpy call
236
+ rather than another run of the op.
237
+
238
+ They are `Annotated[np.ndarray, ...]`, so nothing else changes: the codec
239
+ still sees an array, direct callers still get an array, and `ParamSpec.type`
240
+ is still `np.ndarray`. They compose with the rest —
241
+ `Out[LabelsOf[np.ndarray]]` and
242
+ `Annotated[LabelsOf[np.ndarray], {"label": "Nuclei"}]` both work.
243
+
244
+ Roles are advisory and never guessed. An unannotated array reports
245
+ `role is None`, and deciding what to do with that is the front end's business,
246
+ not opspec's. Nothing in `opspec.types` imports napari; the roles simply
247
+ mirror `napari.types` so that mapping is a lookup rather than a judgement call,
248
+ and `Role` is equally the hook a Fiji or plain-magicgui front end reads.
249
+
250
+ ### Where it goes
251
+
252
+ Ops live in a namespace under `skop/ops/`, named for what they do. How the
253
+ namespace is laid out inside depends on its ops, and the split point is the
254
+ environment:
255
+
256
+ - **One module** when the ops are variations on one call sharing one
257
+ environment — every `skop.ops.threshold` op is a few lines against
258
+ `skimage.filters`, so they share `threshold.py`.
259
+ - **A package, one module per op**, when they do not — `skop.ops.segment`
260
+ spans three environments and each op carries its own models and types, so
261
+ each gets a file and `segment/__init__.py` re-exports them.
262
+
263
+ An op needing several files of its own becomes a package
264
+ (`segment/unseg/__init__.py` plus siblings). Underscore-prefixed modules are
265
+ private and never imported by discovery, which is where shared helpers and
266
+ vendored code belong.
267
+
268
+ Because `from skop.ops.<ns> import <op>` reads the same either way, a
269
+ namespace can grow from a module into a package without breaking callers.
270
+
271
+ ### Computation forms
272
+
273
+ Following [SciJava Ops](https://ops.scijava.org/en/latest/Concepts.html), an op
274
+ is one of three forms, declared through its signature:
275
+
276
+ | Form | Signature | Meaning |
277
+ | --- | --- | --- |
278
+ | function | `def f(image) -> Result` | allocates and returns its output |
279
+ | computer | `def f(image, out: Out[np.ndarray])` | fills a buffer the caller owns |
280
+ | inplace | `def f(image: Mut[np.ndarray])` | mutates its input |
281
+
282
+ `Out` parameters are hidden from generated GUIs — a user is never asked for an
283
+ output buffer. Adapting between forms is not implemented yet: a computer or
284
+ inplace op must be handed its buffers.
285
+
286
+ ## Running an op
287
+
288
+ ```python
289
+ import oprunner
290
+ from skop.ops import toy
291
+
292
+ # Directly, in this process, using this process's dependencies.
293
+ toy.add(2, 3)
294
+
295
+ # Or in the op's own environment, in a worker process.
296
+ with oprunner.Runner() as runner:
297
+ runner.run(toy.add, a=2, b=3)
298
+ ```
299
+
300
+ The first run of an environment builds it, which takes a while. Later runs
301
+ reuse it, since Appose keys environments by name.
302
+
303
+ Because that build happens inside `run()`, a caller with a progress bar can
304
+ subscribe to it:
305
+
306
+ ```python
307
+ runner.subscribe_build_progress(lambda title, current, maximum: ...)
308
+ runner.subscribe_build_output(lambda text: ...)
309
+ runner.subscribe_build_error(lambda text: ...)
310
+ ```
311
+
312
+ These pass straight through to Appose's builder. Progress titles name the
313
+ phase — `Solving`, `Installing conda packages`, `Downloading PyPI packages`,
314
+ `Installing PyPI packages`, `Done` — and come with a real denominator.
315
+
316
+ Two things worth knowing. Subscribing to progress is what *enables* it:
317
+ Appose's `PixiInstallMonitor` runs pixi under `-vv` to read its phase
318
+ transitions, and only wires up when a progress subscriber exists. And
319
+ `build_error` is the stderr *stream* rather than a failure report — pixi
320
+ writes its ordinary status there, and under `-vv` its whole debug log as
321
+ well. A build that actually fails raises out of `run()`.
322
+
323
+ This needs Appose newer than 0.11, which is why `pyproject.toml` currently
324
+ sources it from the main branch.
325
+
326
+ ## Development
327
+
328
+ ```sh
329
+ uv sync --all-groups
330
+ uv run pytest
331
+ uv run ruff check --fix && uv run ruff format
332
+ ```
333
+
334
+ ### Testing the ops for real
335
+
336
+ Ops are exercised end to end in their own environments, in `test_ops_e2e.py`.
337
+ Each such test declares what it needs with `@pytest.mark.env("<env-id>")`, and
338
+ is skipped when that environment is not built — so a plain `pytest` run stays
339
+ fast and offline, testing against whatever happens to be installed already.
340
+
341
+ To run them all, building whatever is missing:
342
+
343
+ ```sh
344
+ uv run pytest --build-envs # everything; slow the first time
345
+ uv run pytest --build-envs -m env # only the op tests
346
+ uv run pytest -m "not env" # only the fast ones
347
+ ```
348
+
349
+ Running the notebooks and the interactive tests is a separate setup, since they
350
+ need a host environment and the ops do not run in it:
351
+ [notebooks/README.md](notebooks/README.md).
352
+
353
+ ### Checking that the environments still solve
354
+
355
+ Building is slow; solving is not. `scripts/check_envs.py` resolves every
356
+ `envs/*/pixi.toml` without downloading anything, and exits non-zero if one is
357
+ unsatisfiable:
358
+
359
+ ```sh
360
+ python scripts/check_envs.py
361
+ ```
362
+
363
+ This catches what the tests structurally cannot: they run *inside* an
364
+ environment, and so cannot tell you whether that environment could still be
365
+ created today. One already built keeps working from its lock file however
366
+ broken its `pixi.toml` becomes — which is how an unsolvable `envs/pytorch`
367
+ survived a week. Run it after editing anything under `envs/`.
368
+
369
+ CI runs `--build-envs`, since these ops are the whole point of the project and
370
+ nothing else validates them against the stacks they target. The flag is also
371
+ settable as `PYTEST_ADDOPTS=--build-envs`, which is handy when the pytest
372
+ invocation is not yours to edit.
@@ -0,0 +1,66 @@
1
+ skop/__init__.py,sha256=q3aBnPDP8M-mFYHVcXGB3nIwAJ9Woq6i5TKJp-T6_3o,518
2
+ skop/models.py,sha256=fWPjTVLoqISXYBsf2HllaRhCx0c_QoOInA5cxp6mgH4,5311
3
+ skop/ops/__init__.py,sha256=DYALgX7N8j17grfDurLsAEYe90MY6xnAvWBH1ctJWik,808
4
+ skop/ops/_util.py,sha256=4aTMa_NVv6IqmRXmVnQB6NZY_qHa7yvjduMYme-rGd4,4076
5
+ skop/ops/edges.py,sha256=oom-p_Qgvdp64dG2k5clqvzNNMeSTC7u_LsC1pem3DA,13777
6
+ skop/ops/generate.py,sha256=0cKxIdDyJXIRloFfAeypx7DxIYhvvxdux1WwrkgBMBw,2722
7
+ skop/ops/labels.py,sha256=-fE2apWk4KcQQ5u0BHigUwMKNTb2amtCE7bSTlg6svo,6382
8
+ skop/ops/morphology.py,sha256=9vUMsuo5ZiC-hCYnw-5tgBWA3bMPmdcYwb3H_V1DTWI,7600
9
+ skop/ops/normalize.py,sha256=f_pKdDDXBjRS83rcsxZZF3pBuB-BEHyCLwsTrTksqTY,3652
10
+ skop/ops/smooth.py,sha256=bz0wYYQ7UzruNXp6V4-7pKolgxmbANkLy7_YMzkFrNk,17615
11
+ skop/ops/threshold.py,sha256=CZ2ETPRt8kcFqc2sC50fxiLWrRlCCAmGdtylJVM7Pig,9988
12
+ skop/ops/toy.py,sha256=7VCUBpkINYc6wsHteYvNypN02CsMb_4jXiyw8qGA_fg,3330
13
+ skop/ops/deconvolve/__init__.py,sha256=Xx_xR_3qXdLPfkB5f_onTXLuOQwY2p5Kxn5cgj-D-G0,728
14
+ skop/ops/deconvolve/_edges.py,sha256=nlavBoTw1J8D_uHIMusWgXxBmkuD_OtQyEc7g0ehEBM,3345
15
+ skop/ops/deconvolve/_pad.py,sha256=0k3bnKJOLC9ihVXpS_Rhy-ti2CIeBeqZZmt1-0viaVA,3535
16
+ skop/ops/deconvolve/richardson_lucy.py,sha256=tHNWcgQVpDBBEfzN8DQOy7E39qK1qjufqfXtktu-TGU,3767
17
+ skop/ops/deconvolve/richardson_lucy_cupy.py,sha256=yhlpdgyaj-BpR6zE2oQ8oe8pJF_oEEZ-lgijs-GpEgo,4838
18
+ skop/ops/detect/__init__.py,sha256=mSBR9M8xmfKBjGsN6ZBHqhDZsfkSeC9OJUdKJTq618Y,1145
19
+ skop/ops/detect/_result.py,sha256=hQqN7TgLkK8ZcuydbGogbh1ydMgWqIZNVWLI1br6nq4,1578
20
+ skop/ops/detect/fastsam.py,sha256=g1L4B29H4JtosA1gfuJzWsFlcTaQjbziI3Vf_W99j_w,3517
21
+ skop/ops/detect/jdll_yolo.py,sha256=G55NAmLj2FN_to8Dt6nV5I56JijSv2JcBaoXy50P24A,4172
22
+ skop/ops/detect/object_aware_yolo.py,sha256=ybY5fjTHR5zLLsBqJ_m1MJP4X7iwFdKlkOxbR2l1y9Q,4226
23
+ skop/ops/detect/yolo.py,sha256=E43l_-j-WLeax2dTjedKNPBy5y1m1_o3RaKLEW3uTRE,18559
24
+ skop/ops/kernels/__init__.py,sha256=GCCv7rT8Uu46oSh7emDwn-C1xm54zEYPGulTr1J7HFo,1422
25
+ skop/ops/kernels/_fluorophore.py,sha256=Dh2apJl1Dgfl4LfY6cgHH_YjwB3L_atyyerV2uDpzaw,1654
26
+ skop/ops/kernels/_recenter.py,sha256=jHQYKKK-l84HTQgCZgH3UQQzxekazeviG2sNzRAc0tk,2370
27
+ skop/ops/kernels/gibson_lanni.py,sha256=NngT_BAZnnwViHcCbLt0abK5SWYdGzJfr6K4kfGXov4,6745
28
+ skop/ops/kernels/paraxial.py,sha256=CEqv4Fzq0ZXlIFelqWTOsbVuHEcgEWQLxz87zISTxbc,5554
29
+ skop/ops/kernels/psf.py,sha256=wntCqZ6wsq9HeVA9wtA6tSotns03BrpVOLwFNZGOH-Q,2087
30
+ skop/ops/mask/__init__.py,sha256=hk0mQqFiypPG9ZucNuKfU1tjQkab5H5jRsG6kTuZx6s,1387
31
+ skop/ops/mask/_result.py,sha256=OtEe58jlYC88g-uTujypyUaMoucMDO_OT9VIhPGBkJk,1717
32
+ skop/ops/mask/microsam.py,sha256=Rq3CxPKPu2gn-7w8PN1v3ow0yp41H_lc6U2rlV073yg,4670
33
+ skop/ops/mask/mobilesam.py,sha256=PqiXvF0CrH4n7PnJPItXg_24PpMMw0JzRoqnzBu8FXk,7436
34
+ skop/ops/segment/__init__.py,sha256=GblPOpbYOPKcTUZMP2X3VU8m3EMAuFMttEhicnlIw8E,667
35
+ skop/ops/segment/cellpose3.py,sha256=GxEAVOpAMYurYMcbTf5hMlzQDt0KgJq1qMPTxRFFF60,10375
36
+ skop/ops/segment/cellpose4.py,sha256=y7g7ZXK0M8l11IJAPKJ-OtIZBrxWK5-mmNdJRUUMlqc,5149
37
+ skop/ops/segment/instanseg.py,sha256=TvQbmwqDm4_pqBbQX6H1kYvKuNb5fdppxoIIJOK8G9M,6296
38
+ skop/ops/segment/stardist2d.py,sha256=XSG4kabcfeY5fsQr1e3kYmGbkWOqyoTTyEp98MB679M,6605
39
+ skop/ops/segment/starfun3d.py,sha256=03RB3QL59NkkbgVABXTf9_VKZ-1Li_wUeJFPFjTqeAQ,3417
40
+ skop/ops/segment/unseg/__init__.py,sha256=nxfXLG3xmnL5zwAlUB7YPdFeLNQk5R6L8JZ8BQ3c1nk,5088
41
+ skop/ops/segment/unseg/_algorithm.py,sha256=M1l3g8g7hBr3af9aEwLBvnUbo_PvikEoZHRNiED7mfA,68689
42
+ skop/ops/train/__init__.py,sha256=dIxjseVRpKvmW9nPvfQCWmZUsBfs4t31CeJagj9Ym0s,3000
43
+ skop/ops/train/_util.py,sha256=lZi2PjsJ4SQtir2pu7qAeZkZa53HenTbpl7aA1cijvk,5749
44
+ skop/ops/train/cellpose3.py,sha256=uh1SI72eYwnmSQjo0jmKhlkTW3FE5MKTd_H9_6onwac,8482
45
+ skop/ops/train/cellpose4.py,sha256=akcfij7wsR4kdqlw9AmBBZ5Jh3UCF6dMLSyUnxFhVYA,7212
46
+ skop/ops/train/stardist2d.py,sha256=dioZYyyw6coHoPLGWIBJxVIYmAHtOp6GjxsDkGDBCSQ,18876
47
+ skop/ops/workflows/__init__.py,sha256=7GyWvVd9hIuDhup-ElcIeinjoI6xAe_JLwS67UDsIZM,1934
48
+ skop/ops/workflows/deconvolve/__init__.py,sha256=7f6Sd4cJCfPPfEVwlXo3TIyN00P974zjXA-s3xv5YZ8,189
49
+ skop/ops/workflows/deconvolve/with_psf.py,sha256=yATu3eqPW37haDJPEFxtI3DEMuWqaMxtrgFK0glZadM,4260
50
+ skop/ops/workflows/mask/__init__.py,sha256=nKt0l8GrX44sCQ9jTtExBARnNrPZu6Y2AWVsMBPzdto,190
51
+ skop/ops/workflows/mask/detect_then_mask.py,sha256=-6DtBC0rdrs1XZKM5rN-ruFO7kRJatIpZGDp4cb0ZA0,4166
52
+ skop/ops/workflows/segment/__init__.py,sha256=RBdgluVVsBwuyPgYdkXbfYnb6sAWUrMQ7vcMV8rxOB4,184
53
+ skop/ops/workflows/segment/connect_2d_in_3d.py,sha256=gV9M5AJQbV0Q4weeP-BF_LHiOg6TSoBUsZhndc5jzhU,6377
54
+ skop/envs/cellpose3/pixi.toml,sha256=hvpkH9eTrUa1xbbJkWM-LwlVXBDJjpRJclCOQvbqUr4,1785
55
+ skop/envs/cupy/pixi.toml,sha256=cQ4bFbGOYMolGnO8IeSO_6JEf806YeXLgtxuybWn09c,1546
56
+ skop/envs/minimal/pixi.toml,sha256=WjHWc35qybmlHaP1KeUD2E-518Rue1fjmNOpIVR1AxE,1200
57
+ skop/envs/pytorch/pixi.toml,sha256=3BU2nmhZMmt6x-MGGu4lQqsnk_1TqUwZwQpi2I7GkY0,3813
58
+ skop/envs/sdeconv/pixi.toml,sha256=o5GhRrDG-ypUj155RriWR0W77CDPBunru-2xriFrLRs,3991
59
+ skop/envs/segment-everything/pixi.toml,sha256=KMq2GUazmzN_Sw3dd5FuuAKMdiMCKtJTcXmhcWoc76Y,3943
60
+ skop/envs/skimage/pixi.toml,sha256=X7W1E79E4UBHYSFiybeCgAYXvKumGCBT00reQnfNOHo,1305
61
+ skop/envs/stardist-tf/pixi.toml,sha256=WyiniXNs6w2_-vz2XX1l_jr62vIVIG2mUOiLeyXV9dg,3656
62
+ skop/envs/unseg-cv/pixi.toml,sha256=TwcYzpAnfBd5taAB0b-kRXBhbw5aXHoLzoKRaebvyDw,1065
63
+ scikit_ops-0.1.0.dev0.dist-info/METADATA,sha256=-rNlEQzsKaScbKYxSG6vMSOVhhjFmy9z7D9FLx70ehQ,16089
64
+ scikit_ops-0.1.0.dev0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
65
+ scikit_ops-0.1.0.dev0.dist-info/entry_points.txt,sha256=0gDhmxYQNjvBwJO4L0Q2fB-5fIEBbRVUS5u5heCYulQ,43
66
+ scikit_ops-0.1.0.dev0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [opspec.collections]
2
+ scikit-ops = skop.ops
skop/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ """skop: scikit-ops' collection of image-processing ops.
2
+
3
+ Each op is an ordinary Python function declared with opspec's ``@op``, in
4
+ ``skop.ops.<namespace>``; its environment is in ``envs/<id>/pixi.toml``. Call
5
+ one directly, or run it in its own environment with oprunner::
6
+
7
+ from oprunner import Runner
8
+ from skop.ops.smooth import gaussian
9
+
10
+ with Runner() as runner:
11
+ blurred = runner.run(gaussian, image=image, sigma=2.0)
12
+
13
+ skop is the working name; the collection will be renamed before release.
14
+ """
@@ -0,0 +1,46 @@
1
+ [workspace]
2
+ name = "skop-cellpose3"
3
+ description = "Cellpose 3, whose cyto3 and nuclei models predate CellposeSAM."
4
+ channels = ["conda-forge"]
5
+ platforms = ["osx-arm64", "linux-64", "win-64"]
6
+ version = "0.1.0"
7
+
8
+ [system-requirements]
9
+ cuda = "12.0"
10
+
11
+ # NB: an environment of its own rather than a version knob on envs/cellpose.
12
+ # Cellpose 4 replaced the model zoo with a single CellposeSAM model and moved
13
+ # the API with it -- v3's models.Cellpose became v4's models.CellposeModel,
14
+ # and eval lost a return value. One environment cannot hold both, and an op
15
+ # that branched on cellpose.version would be back to the runtime version
16
+ # sniffing this project exists to avoid.
17
+ #
18
+ # Deliberately shaped like envs/pytorch and envs/cellpose otherwise: same
19
+ # python, same per-platform torch split. pixi caches conda packages globally,
20
+ # so the torch and CUDA payload is shared with those environments rather than
21
+ # downloaded again -- provided cellpose 3 does not drag the solve onto older
22
+ # builds of them. It is the one thing worth watching in the lock file.
23
+ [dependencies]
24
+ python = "3.11.*"
25
+ numpy = "*"
26
+ cellpose = ">=3,<4"
27
+ # appose comes with the pinned scikit-ops below, as in envs/pytorch.
28
+
29
+ [pypi-dependencies]
30
+ # Pinned, as in every environment: a Fiji with no scikit-ops checkout runs
31
+ # this copy. See envs/pytorch for why the pin is deliberate.
32
+ scikit-ops = { git = "https://github.com/apposed/scikit-ops.git", rev = "00925fea3d92f3c974b7b1ec67d6633307d27bcc" }
33
+
34
+ # PyTorch varies by platform: CUDA builds where there are GPUs to use,
35
+ # and the plain package on Apple Silicon, where MPS comes built in.
36
+ [target.linux-64.dependencies]
37
+ pytorch-gpu = "*"
38
+ cudnn = "*"
39
+ cuda-version = "12.2"
40
+
41
+ [target.win-64.dependencies]
42
+ pytorch-gpu = "*"
43
+ cudnn = "*"
44
+
45
+ [target.osx-arm64.dependencies]
46
+ pytorch = "*"
@@ -0,0 +1,35 @@
1
+ [workspace]
2
+ name = "skop-cupy"
3
+ description = "GPU array computing on CUDA, with cupy."
4
+ channels = ["conda-forge"]
5
+ # No macOS: CUDA is not available there. Listing a platform this env cannot
6
+ # resolve on would turn a clean "no such platform" into a solver failure.
7
+ platforms = ["linux-64", "win-64"]
8
+ version = "0.1.0"
9
+
10
+ [system-requirements]
11
+ cuda = "12.0"
12
+
13
+ [dependencies]
14
+ python = "3.11.*"
15
+ numpy = "*"
16
+ cupy = "*"
17
+ # NB: pinned as envs/cellpose pins it. cuda-version selects which CUDA runtime
18
+ # the packages are built against, and it must not exceed what the installed
19
+ # driver supports.
20
+ cuda-version = "12.2"
21
+ # NB: appose is not listed as a conda dependency here, unlike in
22
+ # envs/unseg-cv, and this is temporary. A conda appose does satisfy the
23
+ # pinned scikit-ops' PyPI dependency on it -- but only once the pin is new
24
+ # enough to have dropped scikit-ops' [tool.uv.sources] git source for
25
+ # appose, which uv honors transitively and which makes the solve
26
+ # unsatisfiable. So this goes back to `appose = ">=0.12"` above as soon as
27
+ # the rev below is bumped past that change and conda-forge carries 0.12.
28
+
29
+ [pypi-dependencies]
30
+ # Note: pinned deliberately -- this is part of the environment's identity,
31
+ # so bumping it rebuilds every environment. A checkout still shadows it,
32
+ # because skop.host.INIT puts src at sys.path[0]. See
33
+ # docs/spec/fiji-front-end.md; it becomes an ordinary version constraint
34
+ # once scikit-ops is released.
35
+ scikit-ops = { git = "https://github.com/apposed/scikit-ops.git", rev = "00925fea3d92f3c974b7b1ec67d6633307d27bcc" }
@@ -0,0 +1,25 @@
1
+ [workspace]
2
+ name = "skop-minimal"
3
+ description = "Smallest environment an op can run in: python, numpy, appose."
4
+ channels = ["conda-forge"]
5
+ platforms = ["osx-arm64", "osx-64", "linux-64", "win-64"]
6
+ version = "0.1.0"
7
+
8
+ [dependencies]
9
+ python = "3.11.*"
10
+ numpy = "*"
11
+ # NB: appose is not listed as a conda dependency here, unlike in
12
+ # envs/unseg-cv, and this is temporary. A conda appose does satisfy the
13
+ # pinned scikit-ops' PyPI dependency on it -- but only once the pin is new
14
+ # enough to have dropped scikit-ops' [tool.uv.sources] git source for
15
+ # appose, which uv honors transitively and which makes the solve
16
+ # unsatisfiable. So this goes back to `appose = ">=0.12"` above as soon as
17
+ # the rev below is bumped past that change and conda-forge carries 0.12.
18
+
19
+ [pypi-dependencies]
20
+ # Note: pinned deliberately -- this is part of the environment's identity,
21
+ # so bumping it rebuilds every environment. A checkout still shadows it,
22
+ # because skop.host.INIT puts src at sys.path[0]. See
23
+ # docs/spec/fiji-front-end.md; it becomes an ordinary version constraint
24
+ # once scikit-ops is released.
25
+ scikit-ops = { git = "https://github.com/apposed/scikit-ops.git", rev = "00925fea3d92f3c974b7b1ec67d6633307d27bcc" }
@@ -0,0 +1,88 @@
1
+ [workspace]
2
+ name = "skop-pytorch"
3
+ description = "The shared PyTorch environment: well-maintained torch libraries that can coexist."
4
+ channels = ["conda-forge"]
5
+ platforms = ["osx-arm64", "linux-64", "win-64"]
6
+ version = "0.1.0"
7
+
8
+ # This environment is meant to hold several ops, not one. Torch installs are
9
+ # large and slow, so anything torch-based that solves cleanly alongside what is
10
+ # already here belongs here rather than in an environment of its own.
11
+ #
12
+ # Three occupants now: ultralytics for the FastSAM detector, micro_sam for the
13
+ # mask detector of docs/design/0008, and Cellpose, whose own environment this
14
+ # replaced -- which is why envs/cellpose is gone.
15
+ #
16
+ # Conda-forge alone until Cellpose 4.2, which it does not carry; see
17
+ # [pypi-dependencies].
18
+ #
19
+ # envs/cellpose3 stays separate, and is the exception that shows the rule:
20
+ # Cellpose 4 replaced the v3 model zoo and moved the API with it, so the two
21
+ # cannot share an environment at any version pin.
22
+
23
+ [system-requirements]
24
+ cuda = "12.0"
25
+
26
+ [dependencies]
27
+ python = "3.11.*"
28
+ numpy = "*"
29
+ ultralytics = ">=8.3"
30
+ # NB: appose is not listed as a conda dependency here, unlike in
31
+ # envs/unseg-cv, and this is temporary. A conda appose does satisfy the
32
+ # pinned scikit-ops' PyPI dependency on it -- but only once the pin is new
33
+ # enough to have dropped scikit-ops' [tool.uv.sources] git source for
34
+ # appose, which uv honors transitively and which makes the solve
35
+ # unsatisfiable. So this goes back to `appose = ">=0.12"` above as soon as
36
+ # the rev below is bumped past that change and conda-forge carries 0.12.
37
+
38
+ # NB: micro_sam depends on `napari >=0.5,<0.6` and `pyqt` outright -- it ships
39
+ # a napari plugin and does not split it out. So this environment installs a GUI
40
+ # stack that nothing in it imports; the ops use micro_sam.util and
41
+ # micro_sam.prompt_based_segmentation, neither of which touches napari. Heavy
42
+ # but harmless: a worker process never opens a window, and 0002's whole point
43
+ # is that this napari pin cannot reach the front end's.
44
+ #
45
+ # It also carries `zarr <3` and `xarray <2025.3` -- worth knowing when the next
46
+ # occupant fails to solve.
47
+ micro_sam = ">=1.6"
48
+
49
+ # NB: torchvision is required explicitly even though nothing here imports it
50
+ # directly. conda's ultralytics does not depend on it, but reads
51
+ # `importlib.metadata.version("torchvision")` at import time and raises
52
+ # PackageNotFoundError when it is absent -- so leaving it out fails at the
53
+ # first `from ultralytics import ...`, not at some later feature.
54
+ torchvision = "*"
55
+
56
+ # PyTorch varies by platform: CUDA builds where there are GPUs to use,
57
+ # and the plain package on Apple Silicon, where MPS comes built in.
58
+
59
+ [pypi-dependencies]
60
+ # Conda-forge stops at cellpose 4.1.1, and 4.2 is where the DINO models
61
+ # arrived; dinov3 is only on GitHub. Neither pulls a second torch -- pixi maps
62
+ # the conda pytorch onto the `torch` requirement.
63
+ cellpose = ">=4.2"
64
+ dinov3 = { git = "https://github.com/facebookresearch/dinov3" }
65
+
66
+ # InstanSeg is plain torch, PyPI only. Since 0.1 its file readers (bioio,
67
+ # rasterio, slideio) are an optional `io` extra, left out: the op is handed
68
+ # an array. The base install is torch, numpy, scikit-image and small helpers.
69
+ instanseg-torch = "*"
70
+
71
+ # Note: pinned deliberately -- this is part of the environment's identity,
72
+ # so bumping it rebuilds every environment. A checkout still shadows it,
73
+ # because skop.host.INIT puts src at sys.path[0]. See
74
+ # docs/spec/fiji-front-end.md; it becomes an ordinary version constraint
75
+ # once scikit-ops is released.
76
+ scikit-ops = { git = "https://github.com/apposed/scikit-ops.git", rev = "00925fea3d92f3c974b7b1ec67d6633307d27bcc" }
77
+
78
+ [target.linux-64.dependencies]
79
+ pytorch-gpu = "*"
80
+ cudnn = "*"
81
+ cuda-version = "12.2"
82
+
83
+ [target.win-64.dependencies]
84
+ pytorch-gpu = "*"
85
+ cudnn = "*"
86
+
87
+ [target.osx-arm64.dependencies]
88
+ pytorch = "*"