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.
- scikit_ops-0.1.0.dev0.dist-info/METADATA +372 -0
- scikit_ops-0.1.0.dev0.dist-info/RECORD +66 -0
- scikit_ops-0.1.0.dev0.dist-info/WHEEL +4 -0
- scikit_ops-0.1.0.dev0.dist-info/entry_points.txt +2 -0
- skop/__init__.py +14 -0
- skop/envs/cellpose3/pixi.toml +46 -0
- skop/envs/cupy/pixi.toml +35 -0
- skop/envs/minimal/pixi.toml +25 -0
- skop/envs/pytorch/pixi.toml +88 -0
- skop/envs/sdeconv/pixi.toml +85 -0
- skop/envs/segment-everything/pixi.toml +96 -0
- skop/envs/skimage/pixi.toml +28 -0
- skop/envs/stardist-tf/pixi.toml +91 -0
- skop/envs/unseg-cv/pixi.toml +27 -0
- skop/models.py +145 -0
- skop/ops/__init__.py +19 -0
- skop/ops/_util.py +115 -0
- skop/ops/deconvolve/__init__.py +18 -0
- skop/ops/deconvolve/_edges.py +92 -0
- skop/ops/deconvolve/_pad.py +117 -0
- skop/ops/deconvolve/richardson_lucy.py +100 -0
- skop/ops/deconvolve/richardson_lucy_cupy.py +116 -0
- skop/ops/detect/__init__.py +28 -0
- skop/ops/detect/_result.py +45 -0
- skop/ops/detect/fastsam.py +103 -0
- skop/ops/detect/jdll_yolo.py +115 -0
- skop/ops/detect/object_aware_yolo.py +112 -0
- skop/ops/detect/yolo.py +441 -0
- skop/ops/edges.py +382 -0
- skop/ops/generate.py +77 -0
- skop/ops/kernels/__init__.py +39 -0
- skop/ops/kernels/_fluorophore.py +42 -0
- skop/ops/kernels/_recenter.py +60 -0
- skop/ops/kernels/gibson_lanni.py +147 -0
- skop/ops/kernels/paraxial.py +128 -0
- skop/ops/kernels/psf.py +59 -0
- skop/ops/labels.py +144 -0
- skop/ops/mask/__init__.py +29 -0
- skop/ops/mask/_result.py +40 -0
- skop/ops/mask/microsam.py +114 -0
- skop/ops/mask/mobilesam.py +176 -0
- skop/ops/morphology.py +214 -0
- skop/ops/normalize.py +85 -0
- skop/ops/segment/__init__.py +24 -0
- skop/ops/segment/cellpose3.py +245 -0
- skop/ops/segment/cellpose4.py +127 -0
- skop/ops/segment/instanseg.py +162 -0
- skop/ops/segment/stardist2d.py +192 -0
- skop/ops/segment/starfun3d.py +104 -0
- skop/ops/segment/unseg/__init__.py +138 -0
- skop/ops/segment/unseg/_algorithm.py +1618 -0
- skop/ops/smooth.py +452 -0
- skop/ops/threshold.py +278 -0
- skop/ops/toy.py +114 -0
- skop/ops/train/__init__.py +81 -0
- skop/ops/train/_util.py +152 -0
- skop/ops/train/cellpose3.py +189 -0
- skop/ops/train/cellpose4.py +164 -0
- skop/ops/train/stardist2d.py +439 -0
- skop/ops/workflows/__init__.py +43 -0
- skop/ops/workflows/deconvolve/__init__.py +7 -0
- skop/ops/workflows/deconvolve/with_psf.py +103 -0
- skop/ops/workflows/mask/__init__.py +7 -0
- skop/ops/workflows/mask/detect_then_mask.py +97 -0
- skop/ops/workflows/segment/__init__.py +7 -0
- 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"]
|