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,85 @@
1
+ [workspace]
2
+ name = "skop-sdeconv"
3
+ description = "sdeconv, for the Gibson-Lanni PSF model."
4
+ channels = ["conda-forge"]
5
+ platforms = ["osx-arm64", "linux-64", "win-64"]
6
+ version = "0.1.0"
7
+
8
+ # This environment exists only because sdeconv is not on conda-forge at a
9
+ # usable version: the channel carries 0.1.0, a py38 build, and the API this op
10
+ # needs (`sdeconv.psfs.SPSFGibsonLanni`) arrived in 1.x. So sdeconv comes from
11
+ # PyPI, and everything it depends on comes from conda -- see the note on
12
+ # [pypi-dependencies] below.
13
+ #
14
+ # It is deliberately NOT its own torch stack. Every conda dependency here is
15
+ # stated exactly as envs/pytorch states it -- same channel, same python, same
16
+ # torch packages, same CUDA -- so that the two environments solve to the same
17
+ # builds and pixi hard-links one copy out of the shared package cache instead
18
+ # of unpacking a second multi-gigabyte torch.
19
+ #
20
+ # That is a deliberate reversal of what design 0010 first proposed. The spec
21
+ # suggested pinning a CPU-only torch, reasoning that generating a PSF is small
22
+ # work and needs no GPU. True, but conda package identity is
23
+ # name-version-buildstring, and `pytorch-cpu ... cpu_py311_*` and
24
+ # `pytorch-gpu ... cuda129_mkl_py311_*` share no bytes at all. A CPU pin saves
25
+ # space only on a machine that has no other torch environment, and costs a
26
+ # full extra copy on any machine that does -- which, since fastsam lives in
27
+ # envs/pytorch, is the machine a developer here actually has.
28
+ #
29
+ # Keeping it aligned is also what makes the merge cheap. If sdeconv earns its
30
+ # way into the canonical environment later, folding it in is deleting this
31
+ # file and adding one line to envs/pytorch's [pypi-dependencies]; nothing
32
+ # about the conda half has to be reconciled first.
33
+ #
34
+ # The one thing to keep in step by hand: envs/pytorch leaves torch unpinned
35
+ # (`pytorch-gpu = "*"`), so two environments solved months apart can land on
36
+ # different versions and share nothing. If that starts to bite, pin both to
37
+ # the same version rather than pinning only this one.
38
+
39
+ [system-requirements]
40
+ cuda = "12.0"
41
+
42
+ [dependencies]
43
+ python = "3.11.*"
44
+ numpy = "*"
45
+ # NB: appose is not listed as a conda dependency here, unlike in
46
+ # envs/unseg-cv, and this is temporary. A conda appose does satisfy the
47
+ # pinned scikit-ops' PyPI dependency on it -- but only once the pin is new
48
+ # enough to have dropped scikit-ops' [tool.uv.sources] git source for
49
+ # appose, which uv honors transitively and which makes the solve
50
+ # unsatisfiable. So this goes back to `appose = ">=0.12"` above as soon as
51
+ # the rev below is bumped past that change and conda-forge carries 0.12.
52
+
53
+ # sdeconv's own declared dependencies, satisfied from conda rather than left
54
+ # to PyPI. This is the same lesson envs/segment-everything records: left to
55
+ # resolve them itself, uv matches `torchvision` against the torch the conda
56
+ # solve already pinned, finds no published wheel that fits, and the solve
57
+ # fails outright. Stating them here leaves uv nothing to resolve but sdeconv.
58
+ scipy = "*"
59
+ scikit-image = "*"
60
+ torchvision = "*"
61
+
62
+ [pypi-dependencies]
63
+ # Note: pinned deliberately -- this is part of the environment's identity,
64
+ # so bumping it rebuilds every environment. A checkout still shadows it,
65
+ # because skop.host.INIT puts src at sys.path[0]. See
66
+ # docs/spec/fiji-front-end.md; it becomes an ordinary version constraint
67
+ # once scikit-ops is released.
68
+ scikit-ops = { git = "https://github.com/apposed/scikit-ops.git", rev = "00925fea3d92f3c974b7b1ec67d6633307d27bcc" }
69
+ sdeconv = ">=1,<2"
70
+
71
+ # Torch, stated exactly as envs/pytorch states it: CUDA builds where there are
72
+ # GPUs, and the plain package on Apple Silicon where MPS is built in. Nothing
73
+ # here uses a GPU -- a Gibson-Lanni PSF is a small FFT -- but matching the
74
+ # canonical environment is worth more than the GPU build being unnecessary.
75
+ [target.linux-64.dependencies]
76
+ pytorch-gpu = "*"
77
+ cudnn = "*"
78
+ cuda-version = "12.2"
79
+
80
+ [target.win-64.dependencies]
81
+ pytorch-gpu = "*"
82
+ cudnn = "*"
83
+
84
+ [target.osx-arm64.dependencies]
85
+ pytorch = "*"
@@ -0,0 +1,96 @@
1
+ [workspace]
2
+ name = "skop-segment-everything"
3
+ description = "segment-everything, for MobileSAMv2's object-aware YOLO."
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: segment-everything's own dependencies are listed here as conda packages,
12
+ # even though it declares them itself on PyPI. That is what makes this
13
+ # environment solve at all.
14
+ #
15
+ # Left to PyPI, uv resolves `torchvision` against whatever `torch` the conda
16
+ # solve pinned, and no published torchvision matches a current one -- the solve
17
+ # fails outright. Satisfying them from conda first leaves uv only the leaves it
18
+ # is the only source for (onnxsim, torchpack). The recipe is lifted from a
19
+ # working environment: ../napari-ai-lab/pixi/microsam_cellposesam_czi, where
20
+ # micro_sam and cellpose happened to drag the same packages in from conda.
21
+ [dependencies]
22
+ python = "3.11.*"
23
+ numpy = "*"
24
+ # NB: appose is not listed as a conda dependency here, unlike in
25
+ # envs/unseg-cv, and this is temporary. A conda appose does satisfy the
26
+ # pinned scikit-ops' PyPI dependency on it -- but only once the pin is new
27
+ # enough to have dropped scikit-ops' [tool.uv.sources] git source for
28
+ # appose, which uv honors transitively and which makes the solve
29
+ # unsatisfiable. So this goes back to `appose = ">=0.12"` above as soon as
30
+ # the rev below is bumped past that change and conda-forge carries 0.12.
31
+ scikit-image = "*"
32
+ py-opencv = "*"
33
+ timm = "*"
34
+ onnx = "*"
35
+ segment-anything = "*"
36
+ gdown = "*"
37
+ magicgui = "*"
38
+
39
+ # The vendored ultralytics 8.0.120's own runtime dependencies, which
40
+ # segment-everything does not declare either -- it only lists what its own
41
+ # modules import. `pandas` is the one that bites first: AutoBackend calls
42
+ # export_formats() to sniff the weights file, and that imports pandas at the
43
+ # top of exporter.py before any model loads.
44
+ pandas = "*"
45
+ matplotlib-base = "*"
46
+ seaborn-base = "*"
47
+ psutil = "*"
48
+ py-cpuinfo = "*"
49
+ requests = "*"
50
+ pyyaml = "*"
51
+ tqdm = "*"
52
+ scipy = "*"
53
+ pillow = "*"
54
+
55
+ # NB: setuptools is pinned below 81 because that is where `pkg_resources` was
56
+ # removed, and the vendored ultralytics 8.0.120 imports it
57
+ # (yolo/utils/checks.py) while building its settings at import time. Without
58
+ # the pin pixi installs setuptools 83 and the op dies on
59
+ # `ModuleNotFoundError: No module named 'pkg_resources'` before it loads a
60
+ # model. The working environment this recipe came from has 80.9.0, but by
61
+ # lock date rather than by constraint.
62
+ setuptools = "<81"
63
+
64
+ # NB: ultralytics is deliberately absent, and must stay absent.
65
+ #
66
+ # segment-everything vendors its own ultralytics 8.0.120 fork and puts it on
67
+ # sys.path when loading the object-aware model. A second ultralytics in this
68
+ # environment would shadow, or be shadowed by, that one depending on import
69
+ # order. The maintained ultralytics lives in envs/pytorch instead, which is
70
+ # why these are two environments and not one. See docs/design/0007.
71
+ [pypi-dependencies]
72
+ # Note: pinned deliberately -- this is part of the environment's identity,
73
+ # so bumping it rebuilds every environment. A checkout still shadows it,
74
+ # because skop.host.INIT puts src at sys.path[0]. See
75
+ # docs/spec/fiji-front-end.md; it becomes an ordinary version constraint
76
+ # once scikit-ops is released.
77
+ scikit-ops = { git = "https://github.com/apposed/scikit-ops.git", rev = "00925fea3d92f3c974b7b1ec67d6633307d27bcc" }
78
+ segment-everything = "==0.3"
79
+
80
+ # Torch is pinned to the 2.9 series rather than left free. The vendored fork is
81
+ # an ultralytics 8.0.120 from 2023 that already needed patching for a torch
82
+ # API change (weights_only), so tracking the newest torch is a liability here.
83
+ # 2.9.1 is what the working environment above runs.
84
+ [target.linux-64.dependencies]
85
+ pytorch-gpu = "2.9.*"
86
+ torchvision = "*"
87
+ cudnn = "*"
88
+
89
+ [target.win-64.dependencies]
90
+ pytorch-gpu = "2.9.*"
91
+ torchvision = "*"
92
+ cudnn = "*"
93
+
94
+ [target.osx-arm64.dependencies]
95
+ pytorch = "2.9.*"
96
+ torchvision = "*"
@@ -0,0 +1,28 @@
1
+ [workspace]
2
+ name = "skop-skimage"
3
+ description = "Classical image processing: scikit-image and friends."
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
+ scipy = "*"
12
+ scikit-image = "*"
13
+ pywavelets = "*" # Note: an optional scikit-image dep, needed by smooth:wavelet.
14
+ # NB: appose is not listed as a conda dependency here, unlike in
15
+ # envs/unseg-cv, and this is temporary. A conda appose does satisfy the
16
+ # pinned scikit-ops' PyPI dependency on it -- but only once the pin is new
17
+ # enough to have dropped scikit-ops' [tool.uv.sources] git source for
18
+ # appose, which uv honors transitively and which makes the solve
19
+ # unsatisfiable. So this goes back to `appose = ">=0.12"` above as soon as
20
+ # the rev below is bumped past that change and conda-forge carries 0.12.
21
+
22
+ [pypi-dependencies]
23
+ # Note: pinned deliberately -- this is part of the environment's identity,
24
+ # so bumping it rebuilds every environment. A checkout still shadows it,
25
+ # because skop.host.INIT puts src at sys.path[0]. See
26
+ # docs/spec/fiji-front-end.md; it becomes an ordinary version constraint
27
+ # once scikit-ops is released.
28
+ scikit-ops = { git = "https://github.com/apposed/scikit-ops.git", rev = "00925fea3d92f3c974b7b1ec67d6633307d27bcc" }
@@ -0,0 +1,91 @@
1
+ [workspace]
2
+ name = "skop-stardist-tf"
3
+ description = "StarDist on TensorFlow, in 2D and 3D."
4
+ channels = ["conda-forge"]
5
+ platforms = ["osx-arm64", "osx-64", "linux-64", "win-64"]
6
+ version = "0.1.0"
7
+
8
+ # Shared by ops/stardist2d.py and ops/starfun3d.py: same stack, one build.
9
+ #
10
+ # The per-platform TensorFlow split is inherited from starfun3d, where it is
11
+ # known to work. TF 2.10 is the last release with GPU support on Windows, and
12
+ # Apple Silicon needs the tensorflow-macos/tensorflow-metal pair rather than
13
+ # plain tensorflow.
14
+
15
+ [dependencies]
16
+ python = "==3.10"
17
+ numpy = "<2"
18
+ scipy = "*"
19
+ pip = "*"
20
+ # NB: appose is not listed as a conda dependency here, unlike in
21
+ # envs/unseg-cv, and this is temporary. A conda appose does satisfy the
22
+ # pinned scikit-ops' PyPI dependency on it -- but only once the pin is new
23
+ # enough to have dropped scikit-ops' [tool.uv.sources] git source for
24
+ # appose, which uv honors transitively and which makes the solve
25
+ # unsatisfiable. So this goes back to `appose = ">=0.12"` above as soon as
26
+ # the rev below is bumped past that change and conda-forge carries 0.12.
27
+ # NB: stardist 0.9.1 imports pkg_resources, which setuptools dropped in 81.
28
+ setuptools = "<81"
29
+
30
+ [pypi-dependencies]
31
+ # Note: pinned deliberately -- this is part of the environment's identity,
32
+ # so bumping it rebuilds every environment. A checkout still shadows it,
33
+ # because skop.host.INIT puts src at sys.path[0]. See
34
+ # docs/spec/fiji-front-end.md; it becomes an ordinary version constraint
35
+ # once scikit-ops is released.
36
+ scikit-ops = { git = "https://github.com/apposed/scikit-ops.git", rev = "00925fea3d92f3c974b7b1ec67d6633307d27bcc" }
37
+ stardist = "==0.9.1"
38
+ csbdeep = "*"
39
+
40
+ [target.osx-64.dependencies]
41
+ python = "==3.10"
42
+
43
+ [target.osx-arm64.dependencies]
44
+ # NB: inherited from starfun3d, which leaves Python unpinned here so the
45
+ # resolver can pick whatever tensorflow-macos currently ships wheels for.
46
+ # In practice that means Apple Silicon runs a newer Python than the other
47
+ # platforms -- 3.12 with TF 2.16, rather than 3.10 with TF 2.10.
48
+ python = "*"
49
+
50
+ # CUDA on Windows only. TF 2.10 predates the CUDA-carrying wheels and ships no
51
+ # CUDA of its own, so on win-64 it has to come from conda. Nothing else is
52
+ # needed: pixi's activation already puts %CONDA_PREFIX%\Library\bin on PATH,
53
+ # which is how Windows finds DLLs.
54
+ #
55
+ # Linux needs none of this: tensorflow[and-cuda] below brings its own CUDA in
56
+ # site-packages, where TensorFlow finds it without help. macOS has no CUDA at
57
+ # all; Apple Silicon uses tensorflow-metal.
58
+ [target.win-64.dependencies]
59
+ cudatoolkit = "11.2.*"
60
+ cudnn = "8.1.*"
61
+
62
+ [target.win-64.pypi-dependencies]
63
+ tensorflow = "==2.10.1"
64
+
65
+ # 2.15 is the ceiling, not a preference: 2.16 defaults to Keras 3, which
66
+ # csbdeep and stardist 0.9.1 do not work with. The [and-cuda] extra pulls
67
+ # NVIDIA's own pip wheels, so this is the whole CUDA story on Linux.
68
+ [target.linux-64.pypi-dependencies]
69
+ tensorflow = { version = "==2.15.*", extras = ["and-cuda"] }
70
+
71
+ [target.osx-64.pypi-dependencies]
72
+ tensorflow = "*"
73
+
74
+ [target.osx-arm64.pypi-dependencies]
75
+ tensorflow-macos = "*"
76
+ tensorflow-metal = "*"
77
+
78
+ # Variant: Apple Silicon without the Metal plugin, which some models trip over.
79
+ [feature.nometal]
80
+ channels = ["conda-forge"]
81
+
82
+ [feature.nometal.target.osx-arm64.pypi-dependencies]
83
+ tensorflow = "*"
84
+
85
+ # NB: Only 'default' is installed on build; 'nometal' is materialized on
86
+ # demand via Runner.run(..., variant="nometal"). There is no longer a 'cuda'
87
+ # variant -- CUDA rides with the wheel on Linux and comes from conda on
88
+ # Windows, both in the default.
89
+ [environments]
90
+ default = { solve-group = "default" }
91
+ nometal = { features = ["nometal"], solve-group = "default" }
@@ -0,0 +1,27 @@
1
+ [workspace]
2
+ name = "skop-unseg-cv"
3
+ description = "UNSEG's stack: an older scikit-image, scikit-learn and OpenCV."
4
+ channels = ["conda-forge"]
5
+ platforms = ["osx-arm64", "osx-64", "linux-64", "linux-aarch64", "win-64"]
6
+ version = "0.1.0"
7
+
8
+ # Pins are inherited from unseg-fiji, where the algorithm is known to produce
9
+ # correct results. They are old enough that this env can share nothing with
10
+ # the others -- which is precisely why environments are per-stack.
11
+
12
+ [dependencies]
13
+ python = "3.9.*"
14
+ numpy = "==1.24.3"
15
+ scipy = "==1.9.1"
16
+ scikit-image = "==0.20.0"
17
+ scikit-learn = "==1.2.2"
18
+ # NB: unseg-fiji pinned appose 0.7.2, but skop's worker adapter needs the
19
+ # service init script support added later.
20
+ appose = ">=0.11"
21
+
22
+ [pypi-dependencies]
23
+ # NB: the one environment with no pinned scikit-ops. Every other one installs
24
+ # it, so that a worker needs no checkout; this one cannot, because scikit-ops
25
+ # requires Python 3.10 and UNSEG's pins 3.9. It runs off the sys.path
26
+ # injection alone, which is what got it working in the first place.
27
+ opencv-python-headless = "==4.7.0.72"
skop/models.py ADDED
@@ -0,0 +1,145 @@
1
+ """Which Cellpose a model is for, answered without loading it.
2
+
3
+ A trained Cellpose model is a bare ``torch.save`` of a state dict under a name
4
+ its author chose, so the file says nothing about which Cellpose it belongs to.
5
+ Handing a Cellpose 3 model to the ``cellpose4`` op, or the reverse, fails deep
6
+ inside torch with an error about unexpected keys.
7
+
8
+ The architecture is legible in the key names, though, and a torch checkpoint
9
+ is a zip whose pickled header lists them. Reading that header is a ~50 KB read
10
+ even when the weights beside it are 600 MB, and it needs only the standard
11
+ library -- so the *host* can route a model to the right op before dispatching
12
+ it, which is the point. ``cellpose4`` and ``cellpose3`` live in environments
13
+ that cannot both exist, and the host has neither.
14
+
15
+ Built-in models are answered from a name instead, so that a caller with a
16
+ mixed list -- some built-ins, some files of its own -- can ask the same
17
+ question of every entry.
18
+
19
+ Standard library only, like ``ophelper.boxes`` and ``ophelper.masks``, so the host, a
20
+ worker and a front end can all import it.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import zipfile
26
+ from enum import Enum
27
+ from pathlib import Path
28
+
29
+ __all__ = ["BUILTIN_MODELS", "CellposeFlavor", "cellpose_flavor"]
30
+
31
+
32
+ class CellposeFlavor(Enum):
33
+ """Which Cellpose can load a file, and so which op runs it. Not which
34
+ backbone -- Cellpose 4 picks between its own two, and cannot load a
35
+ Cellpose 3 model at all."""
36
+
37
+ cellpose4 = "cellpose4"
38
+ cellpose3 = "cellpose3"
39
+
40
+
41
+ #: A key prefix in one architecture and not the others. ``diam_mean`` and
42
+ #: ``diam_labels`` are in all of them and tell you nothing.
43
+ _SIGNATURES = (
44
+ (b"encoder.blocks.", CellposeFlavor.cellpose4),
45
+ (b"encoder.cls_token", CellposeFlavor.cellpose4),
46
+ (b"downsample.down.res_down_", CellposeFlavor.cellpose3),
47
+ )
48
+
49
+
50
+ #: The models each Cellpose ships with, by the name you pass it. Cellpose 4
51
+ #: replaced the whole zoo with one model, so no name is in both versions and a
52
+ #: name alone settles the architecture. The Cellpose 3 list is its full model
53
+ #: zoo, older ``cyto``/``cyto2`` included -- they still load there, and a
54
+ #: published result may name one.
55
+ BUILTIN_MODELS: dict[str, CellposeFlavor] = {
56
+ "cpsam": CellposeFlavor.cellpose4,
57
+ "cpsam_v2": CellposeFlavor.cellpose4,
58
+ "cpdino": CellposeFlavor.cellpose4,
59
+ "cpdino-vitb": CellposeFlavor.cellpose4,
60
+ **dict.fromkeys(
61
+ (
62
+ "cyto",
63
+ "cyto2",
64
+ "cyto3",
65
+ "nuclei",
66
+ "cyto2_cp3",
67
+ "tissuenet_cp3",
68
+ "livecell_cp3",
69
+ "yeast_PhC_cp3",
70
+ "yeast_BF_cp3",
71
+ "bact_phase_cp3",
72
+ "bact_fluor_cp3",
73
+ "deepbacs_cp3",
74
+ "transformer_cp3",
75
+ "neurips_cellpose_default",
76
+ "neurips_cellpose_transformer",
77
+ "neurips_grayscale_cyto2",
78
+ "CP",
79
+ "CPx",
80
+ "TN1",
81
+ "TN2",
82
+ "TN3",
83
+ "LC1",
84
+ "LC2",
85
+ "LC3",
86
+ "LC4",
87
+ ),
88
+ CellposeFlavor.cellpose3,
89
+ ),
90
+ }
91
+
92
+
93
+ def cellpose_flavor(model: str | Path) -> CellposeFlavor:
94
+ """Identify which Cellpose a model is for.
95
+
96
+ Args:
97
+ model: Either the name of a built-in model, such as ``"cyto3"`` or
98
+ ``"cpsam"``, or the path of one saved by ``torch.save``. A name
99
+ is looked up; a path is read.
100
+
101
+ Returns:
102
+ Which architecture it is, and so which op can run it.
103
+
104
+ Raises:
105
+ ValueError: If the name is not a built-in, or the file is not a torch
106
+ checkpoint, or is one whose keys match neither architecture.
107
+ Guessing here would send the model to an environment that dies on
108
+ it, so this reports rather than picks.
109
+ """
110
+ # A plain string is a name first and a path second, so that "cyto3" means
111
+ # the built-in even when run beside a file of that name. Spell it as a
112
+ # Path to mean the file.
113
+ if isinstance(model, str) and model in BUILTIN_MODELS:
114
+ return BUILTIN_MODELS[model]
115
+
116
+ path = Path(model)
117
+ if not path.exists():
118
+ raise ValueError(
119
+ f"{model} is neither a built-in Cellpose model nor an existing file"
120
+ )
121
+ if not zipfile.is_zipfile(path):
122
+ # Either not a checkpoint at all, or one from torch before 1.6. No
123
+ # Cellpose that skop can run wrote the latter.
124
+ raise ValueError(f"{path} is not a torch checkpoint")
125
+
126
+ header = _header(path)
127
+ for signature, flavor in _SIGNATURES:
128
+ if signature in header:
129
+ return flavor
130
+ raise ValueError(f"{path} is not a recognized Cellpose model")
131
+
132
+
133
+ def _header(path: Path) -> bytes:
134
+ """The pickled state dict header, without the tensors it points at.
135
+
136
+ A torch checkpoint is a zip holding ``<name>/data.pkl`` -- the state dict
137
+ with its storages replaced by references -- beside one entry per tensor.
138
+ The root name is whatever the model was called when it was saved, so the
139
+ entry is found by suffix.
140
+ """
141
+ with zipfile.ZipFile(path) as archive:
142
+ names = [n for n in archive.namelist() if n.endswith("data.pkl")]
143
+ if not names:
144
+ raise ValueError(f"{path} is a zip, but not a torch checkpoint")
145
+ return archive.read(names[0])
skop/ops/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ """The bundled op collection.
2
+
3
+ Ops are grouped into namespaces by what they do -- ``skop.ops.threshold``,
4
+ ``skop.ops.segment`` -- and are imported from the namespace regardless of how
5
+ it is laid out inside::
6
+
7
+ from skop.ops.threshold import otsu
8
+ from skop.ops.segment import cellpose4
9
+
10
+ A namespace whose ops are variations on one call sharing one environment is a
11
+ single module (``threshold.py``). A namespace whose ops span environments and
12
+ carry their own models and types is a package with a module per op
13
+ (``segment/``), re-exporting them in its ``__init__.py``. Both read the same
14
+ from the outside, so a namespace can grow from one into the other without
15
+ breaking a caller's import.
16
+
17
+ Discovery walks this package recursively, skipping underscore-prefixed
18
+ modules; see ``oprunner.discovery``.
19
+ """
skop/ops/_util.py ADDED
@@ -0,0 +1,115 @@
1
+ """Helpers shared between ops.
2
+
3
+ Kept as light as an op module, since it is imported alongside them during
4
+ discovery.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Callable
10
+ from enum import Enum
11
+
12
+ import numpy as np
13
+
14
+
15
+ class Footprint(Enum):
16
+ """Shape of a structuring element, as the metric its radius measures in.
17
+
18
+ ``ball`` is Euclidean and the usual choice: it is isotropic, so an
19
+ operation does not depend on how the object happens to be oriented.
20
+ ``box`` is Chebyshev and the fastest, at the cost of favouring the
21
+ diagonals. ``diamond`` is Manhattan, the tightest of the three.
22
+ """
23
+
24
+ ball = "ball"
25
+ box = "box"
26
+ diamond = "diamond"
27
+
28
+
29
+ def footprint(ndim: int, radius: int, shape: Footprint = Footprint.ball) -> np.ndarray:
30
+ """A structuring element of the given radius, in the given dimension.
31
+
32
+ Generated rather than taken from ``skimage.morphology``, whose generators
33
+ are one function per dimension -- ``disk`` for 2-D, ``ball`` for 3-D and
34
+ nothing beyond. The three shapes are the three usual metrics, so each is a
35
+ distance from the centre thresholded at ``radius``.
36
+ """
37
+ grid = np.indices((2 * radius + 1,) * ndim) - radius
38
+ if shape is Footprint.ball:
39
+ distance = np.sqrt((grid.astype(np.float64) ** 2).sum(axis=0))
40
+ elif shape is Footprint.box:
41
+ distance = np.abs(grid).max(axis=0)
42
+ else:
43
+ distance = np.abs(grid).sum(axis=0)
44
+ return (distance <= radius).astype(np.uint8)
45
+
46
+
47
+ def channel_axis(image: np.ndarray) -> int | None:
48
+ """``-1`` when a trailing axis looks like RGB(A), else ``None``.
49
+
50
+ The same guess :func:`to_gray` makes, for ops that keep the channels
51
+ rather than collapsing them.
52
+ """
53
+ if image.ndim >= 3 and image.shape[-1] in (3, 4):
54
+ return -1
55
+ return None
56
+
57
+
58
+ def per_channel(
59
+ image: np.ndarray, fn: Callable[[np.ndarray], np.ndarray]
60
+ ) -> np.ndarray:
61
+ """Apply a spatial filter to each RGB(A) channel, or to the whole array.
62
+
63
+ A neighborhood is a spatial notion, and mixing red into green is never
64
+ what a filter meant. Arrays without a trailing channel axis go through
65
+ untouched, so an op needs no branch of its own.
66
+ """
67
+ if channel_axis(image) is None:
68
+ return fn(image)
69
+ return np.stack(
70
+ [fn(image[..., c]) for c in range(image.shape[-1])],
71
+ axis=-1,
72
+ )
73
+
74
+
75
+ def to_gray(image: np.ndarray) -> np.ndarray:
76
+ """Collapse a small trailing channel axis, leaving other axes alone.
77
+
78
+ A trailing extent of 3 or 4 is taken to be RGB(A); anything else is
79
+ assumed to be spatial and returned untouched.
80
+ """
81
+ if image.ndim >= 3 and image.shape[-1] in (3, 4):
82
+ return image[..., :3].mean(axis=-1)
83
+ return image
84
+
85
+
86
+ def to_rgb(image: np.ndarray) -> np.ndarray:
87
+ """Promote an image to the ``(H, W, 3)`` uint8 an RGB model expects.
88
+
89
+ The other direction from :func:`to_gray`, and the thing every detector
90
+ ported from natural-image work needs: a 16-bit microscopy plane means
91
+ nothing to a model trained on photographs until it is contrast-stretched
92
+ into 8-bit and given three channels.
93
+
94
+ Percentile-stretched rather than min-max scaled, so a single hot pixel
95
+ cannot flatten everything else to black. Already-uint8 RGB is returned
96
+ untouched, since that is exactly what the model wants.
97
+ """
98
+ if image.ndim == 3 and image.shape[-1] == 4:
99
+ image = image[..., :3]
100
+ if image.ndim == 3 and image.shape[-1] == 3 and image.dtype == np.uint8:
101
+ return image
102
+ if image.ndim > 2 and not (image.ndim == 3 and image.shape[-1] == 3):
103
+ raise ValueError(f"expected a 2-D image or (H, W, 3), got shape {image.shape}")
104
+
105
+ x = image.astype(np.float32)
106
+ low, high = np.percentile(x, (1.0, 99.8))
107
+ if high <= low:
108
+ # A blank or single-valued image: stretching it would divide by zero.
109
+ high = low + 1.0
110
+ x = np.clip((x - low) / (high - low), 0.0, 1.0)
111
+ x = (x * 255).astype(np.uint8)
112
+
113
+ if x.ndim == 2:
114
+ x = np.repeat(x[:, :, None], 3, axis=2)
115
+ return np.ascontiguousarray(x)
@@ -0,0 +1,18 @@
1
+ """Deconvolution: undo the blur a microscope's PSF imposed.
2
+
3
+ One op per backend, because ``@op(env=...)`` is fixed per function and the
4
+ backends need different environments -- ``richardson_lucy`` runs anywhere,
5
+ ``richardson_lucy_cupy`` needs an NVIDIA GPU. They implement the same
6
+ iteration and are held numerically equivalent by a test, so a caller that can
7
+ choose is choosing on speed alone.
8
+
9
+ PSFs to feed them live in ``skop.ops.kernels``, and the workflow that pairs
10
+ one with a deconvolver is ``skop.ops.workflows.deconvolve``.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from .richardson_lucy import richardson_lucy
16
+ from .richardson_lucy_cupy import richardson_lucy_cupy
17
+
18
+ __all__ = ["richardson_lucy", "richardson_lucy_cupy"]