senoquant 1.0.0b2__py3-none-any.whl → 1.0.0b3__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.
- senoquant/__init__.py +6 -2
- senoquant/_reader.py +1 -1
- senoquant/reader/core.py +201 -18
- senoquant/tabs/batch/backend.py +18 -3
- senoquant/tabs/batch/frontend.py +8 -4
- senoquant/tabs/quantification/features/marker/dialog.py +26 -6
- senoquant/tabs/quantification/features/marker/export.py +97 -24
- senoquant/tabs/quantification/features/marker/rows.py +2 -2
- senoquant/tabs/quantification/features/spots/dialog.py +41 -11
- senoquant/tabs/quantification/features/spots/export.py +163 -10
- senoquant/tabs/quantification/frontend.py +2 -2
- senoquant/tabs/segmentation/frontend.py +46 -9
- senoquant/tabs/segmentation/models/cpsam/model.py +1 -1
- senoquant/tabs/segmentation/models/default_2d/model.py +22 -77
- senoquant/tabs/segmentation/models/default_3d/model.py +8 -74
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/tools/create_zip_contents.py +0 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/probe.py +13 -13
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/stardist_libs.py +171 -0
- senoquant/tabs/spots/frontend.py +42 -5
- senoquant/tabs/spots/models/ufish/details.json +17 -0
- senoquant/tabs/spots/models/ufish/model.py +129 -0
- senoquant/tabs/spots/ufish_utils/__init__.py +13 -0
- senoquant/tabs/spots/ufish_utils/core.py +357 -0
- senoquant/utils.py +1 -1
- senoquant-1.0.0b3.dist-info/METADATA +161 -0
- {senoquant-1.0.0b2.dist-info → senoquant-1.0.0b3.dist-info}/RECORD +41 -28
- {senoquant-1.0.0b2.dist-info → senoquant-1.0.0b3.dist-info}/top_level.txt +1 -0
- ufish/__init__.py +1 -0
- ufish/api.py +778 -0
- ufish/model/__init__.py +0 -0
- ufish/model/loss.py +62 -0
- ufish/model/network/__init__.py +0 -0
- ufish/model/network/spot_learn.py +50 -0
- ufish/model/network/ufish_net.py +204 -0
- ufish/model/train.py +175 -0
- ufish/utils/__init__.py +0 -0
- ufish/utils/img.py +418 -0
- ufish/utils/log.py +8 -0
- ufish/utils/spot_calling.py +115 -0
- senoquant/tabs/spots/models/rmp/details.json +0 -61
- senoquant/tabs/spots/models/rmp/model.py +0 -499
- senoquant/tabs/spots/models/udwt/details.json +0 -103
- senoquant/tabs/spots/models/udwt/model.py +0 -482
- senoquant-1.0.0b2.dist-info/METADATA +0 -193
- {senoquant-1.0.0b2.dist-info → senoquant-1.0.0b3.dist-info}/WHEEL +0 -0
- {senoquant-1.0.0b2.dist-info → senoquant-1.0.0b3.dist-info}/entry_points.txt +0 -0
- {senoquant-1.0.0b2.dist-info → senoquant-1.0.0b3.dist-info}/licenses/LICENSE +0 -0
|
@@ -19,6 +19,9 @@ from senoquant.tabs.segmentation.stardist_onnx_utils.onnx_framework import (
|
|
|
19
19
|
normalize,
|
|
20
20
|
predict_tiled,
|
|
21
21
|
)
|
|
22
|
+
from senoquant.tabs.segmentation.stardist_onnx_utils.onnx_framework.stardist_libs import (
|
|
23
|
+
ensure_stardist_libs,
|
|
24
|
+
)
|
|
22
25
|
from senoquant.tabs.segmentation.stardist_onnx_utils.onnx_framework.inspect import (
|
|
23
26
|
make_probe_image,
|
|
24
27
|
)
|
|
@@ -240,7 +243,7 @@ class StarDistOnnxModel(SenoQuantSegmentationModel):
|
|
|
240
243
|
Parameters
|
|
241
244
|
----------
|
|
242
245
|
layer : object or None
|
|
243
|
-
|
|
246
|
+
napari layer to convert.
|
|
244
247
|
required : bool
|
|
245
248
|
Whether a missing layer should raise an error.
|
|
246
249
|
|
|
@@ -263,6 +266,9 @@ class StarDistOnnxModel(SenoQuantSegmentationModel):
|
|
|
263
266
|
session = self._sessions.get(model_path)
|
|
264
267
|
if session is None or providers_override is not None:
|
|
265
268
|
providers = providers_override or self._preferred_providers()
|
|
269
|
+
preload = getattr(ort, "preload_dlls", None)
|
|
270
|
+
if callable(preload):
|
|
271
|
+
preload()
|
|
266
272
|
session = ort.InferenceSession(
|
|
267
273
|
str(model_path),
|
|
268
274
|
providers=providers,
|
|
@@ -505,85 +511,13 @@ class StarDistOnnxModel(SenoQuantSegmentationModel):
|
|
|
505
511
|
libraries are absent, allowing Python utilities to import.
|
|
506
512
|
"""
|
|
507
513
|
utils_root = self._get_utils_root()
|
|
508
|
-
csbdeep_root = utils_root / "_csbdeep"
|
|
509
|
-
if csbdeep_root.exists():
|
|
510
|
-
csbdeep_path = str(csbdeep_root)
|
|
511
|
-
if csbdeep_path not in sys.path:
|
|
512
|
-
sys.path.insert(0, csbdeep_path)
|
|
513
|
-
|
|
514
514
|
stardist_pkg = (
|
|
515
515
|
"senoquant.tabs.segmentation.stardist_onnx_utils._stardist"
|
|
516
516
|
)
|
|
517
|
-
|
|
518
|
-
pkg = types.ModuleType(stardist_pkg)
|
|
519
|
-
pkg.__path__ = [str(utils_root / "_stardist")]
|
|
520
|
-
sys.modules[stardist_pkg] = pkg
|
|
521
|
-
|
|
522
|
-
base_pkg = f"{stardist_pkg}.lib"
|
|
523
|
-
lib_dirs = [utils_root / "_stardist" / "lib"]
|
|
524
|
-
for entry in list(sys.path):
|
|
525
|
-
if not entry:
|
|
526
|
-
continue
|
|
527
|
-
try:
|
|
528
|
-
candidate = (
|
|
529
|
-
Path(entry)
|
|
530
|
-
/ "senoquant"
|
|
531
|
-
/ "tabs"
|
|
532
|
-
/ "segmentation"
|
|
533
|
-
/ "stardist_onnx_utils"
|
|
534
|
-
/ "_stardist"
|
|
535
|
-
/ "lib"
|
|
536
|
-
)
|
|
537
|
-
except Exception:
|
|
538
|
-
continue
|
|
539
|
-
if candidate.exists():
|
|
540
|
-
lib_dirs.append(candidate)
|
|
541
|
-
|
|
542
|
-
if base_pkg in sys.modules:
|
|
543
|
-
pkg = sys.modules[base_pkg]
|
|
544
|
-
pkg.__path__ = [str(p) for p in lib_dirs]
|
|
545
|
-
else:
|
|
546
|
-
pkg = types.ModuleType(base_pkg)
|
|
547
|
-
pkg.__path__ = [str(p) for p in lib_dirs]
|
|
548
|
-
sys.modules[base_pkg] = pkg
|
|
549
|
-
|
|
550
|
-
def _stub(*_args, **_kwargs):
|
|
551
|
-
raise RuntimeError("StarDist compiled ops are unavailable.")
|
|
552
|
-
|
|
553
|
-
has_2d = False
|
|
554
|
-
has_3d = False
|
|
555
|
-
for lib_dir in lib_dirs:
|
|
556
|
-
has_2d = has_2d or any(lib_dir.glob("stardist2d*.so")) or any(
|
|
557
|
-
lib_dir.glob("stardist2d*.pyd")
|
|
558
|
-
)
|
|
559
|
-
has_3d = has_3d or any(lib_dir.glob("stardist3d*.so")) or any(
|
|
560
|
-
lib_dir.glob("stardist3d*.pyd")
|
|
561
|
-
)
|
|
517
|
+
has_2d, has_3d = ensure_stardist_libs(utils_root, stardist_pkg)
|
|
562
518
|
self._has_stardist_2d_lib = has_2d
|
|
563
519
|
self._has_stardist_3d_lib = has_3d
|
|
564
520
|
|
|
565
|
-
mod2d = f"{base_pkg}.stardist2d"
|
|
566
|
-
if has_2d and mod2d in sys.modules:
|
|
567
|
-
if getattr(sys.modules[mod2d], "__file__", None) is None:
|
|
568
|
-
del sys.modules[mod2d]
|
|
569
|
-
if not has_2d and mod2d not in sys.modules:
|
|
570
|
-
module = types.ModuleType(mod2d)
|
|
571
|
-
module.c_star_dist = _stub
|
|
572
|
-
module.c_non_max_suppression_inds_old = _stub
|
|
573
|
-
module.c_non_max_suppression_inds = _stub
|
|
574
|
-
sys.modules[mod2d] = module
|
|
575
|
-
|
|
576
|
-
mod3d = f"{base_pkg}.stardist3d"
|
|
577
|
-
if has_3d and mod3d in sys.modules:
|
|
578
|
-
if getattr(sys.modules[mod3d], "__file__", None) is None:
|
|
579
|
-
del sys.modules[mod3d]
|
|
580
|
-
if not has_3d and mod3d not in sys.modules:
|
|
581
|
-
module = types.ModuleType(mod3d)
|
|
582
|
-
module.c_star_dist3d = _stub
|
|
583
|
-
module.c_polyhedron_to_label = _stub
|
|
584
|
-
module.c_non_max_suppression_inds = _stub
|
|
585
|
-
sys.modules[mod3d] = module
|
|
586
|
-
|
|
587
521
|
def _get_rays_class(self):
|
|
588
522
|
"""Load and cache the StarDist Rays_GoldenSpiral class."""
|
|
589
523
|
if self._rays_class is not None:
|
|
File without changes
|
|
@@ -53,6 +53,17 @@ def make_probe_image(
|
|
|
53
53
|
if model_path is None or input_layout is None:
|
|
54
54
|
return probe
|
|
55
55
|
|
|
56
|
+
div_by = None
|
|
57
|
+
if div_by_cache is not None:
|
|
58
|
+
div_by = div_by_cache.get(model_path)
|
|
59
|
+
if div_by is None:
|
|
60
|
+
try:
|
|
61
|
+
div_by = infer_div_by(model_path, ndim=image.ndim)
|
|
62
|
+
except Exception:
|
|
63
|
+
div_by = None
|
|
64
|
+
if div_by_cache is not None and div_by is not None:
|
|
65
|
+
div_by_cache[model_path] = div_by
|
|
66
|
+
|
|
56
67
|
patterns = None
|
|
57
68
|
if valid_size_cache is not None:
|
|
58
69
|
patterns = valid_size_cache.get(model_path)
|
|
@@ -68,24 +79,13 @@ def make_probe_image(
|
|
|
68
79
|
if valid_size_cache is not None:
|
|
69
80
|
valid_size_cache[model_path] = patterns
|
|
70
81
|
|
|
71
|
-
div_by = None
|
|
72
|
-
if div_by_cache is not None:
|
|
73
|
-
div_by = div_by_cache.get(model_path)
|
|
74
|
-
if div_by is None:
|
|
75
|
-
try:
|
|
76
|
-
div_by = infer_div_by(model_path, ndim=image.ndim)
|
|
77
|
-
except Exception:
|
|
78
|
-
div_by = None
|
|
79
|
-
if div_by_cache is not None and div_by is not None:
|
|
80
|
-
div_by_cache[model_path] = div_by
|
|
81
|
-
|
|
82
82
|
desired = list(probe.shape)
|
|
83
83
|
if patterns:
|
|
84
84
|
desired = [
|
|
85
85
|
max(1, snap_size(int(size), patterns[axis]))
|
|
86
86
|
for axis, size in enumerate(desired)
|
|
87
87
|
]
|
|
88
|
-
|
|
88
|
+
if div_by:
|
|
89
89
|
desired = [
|
|
90
90
|
max(int(d), (int(size) // int(d)) * int(d)) if d else int(size)
|
|
91
91
|
for size, d in zip(desired, div_by)
|
|
@@ -97,4 +97,4 @@ def make_probe_image(
|
|
|
97
97
|
pads = [(0, max(0, d - s)) for s, d in zip(probe.shape, desired)]
|
|
98
98
|
if any(pad_after > 0 for _, pad_after in pads):
|
|
99
99
|
probe = np.pad(probe, pads, mode="reflect")
|
|
100
|
-
return probe
|
|
100
|
+
return probe
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Helpers for locating and loading StarDist compiled extensions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.machinery
|
|
6
|
+
import importlib.util
|
|
7
|
+
import sys
|
|
8
|
+
import types
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def ensure_stardist_libs( # noqa: C901, PLR0912, PLR0915
|
|
13
|
+
utils_root: Path,
|
|
14
|
+
stardist_pkg: str,
|
|
15
|
+
) -> tuple[bool, bool]:
|
|
16
|
+
"""Ensure StarDist compiled extensions can be imported.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
utils_root : pathlib.Path
|
|
21
|
+
Root path to the ``stardist_onnx_utils`` package.
|
|
22
|
+
stardist_pkg : str
|
|
23
|
+
Fully-qualified package name for the vendored StarDist package.
|
|
24
|
+
|
|
25
|
+
Returns
|
|
26
|
+
-------
|
|
27
|
+
tuple[bool, bool]
|
|
28
|
+
Tuple of ``(has_2d, has_3d)`` indicating which compiled
|
|
29
|
+
extensions are available.
|
|
30
|
+
|
|
31
|
+
"""
|
|
32
|
+
csbdeep_root = utils_root / "_csbdeep"
|
|
33
|
+
if csbdeep_root.exists():
|
|
34
|
+
csbdeep_path = str(csbdeep_root)
|
|
35
|
+
if csbdeep_path not in sys.path:
|
|
36
|
+
sys.path.insert(0, csbdeep_path)
|
|
37
|
+
|
|
38
|
+
if stardist_pkg not in sys.modules:
|
|
39
|
+
pkg = types.ModuleType(stardist_pkg)
|
|
40
|
+
pkg.__path__ = [str(utils_root / "_stardist")]
|
|
41
|
+
sys.modules[stardist_pkg] = pkg
|
|
42
|
+
|
|
43
|
+
base_pkg = f"{stardist_pkg}.lib"
|
|
44
|
+
lib_dirs: list[Path] = []
|
|
45
|
+
seen_dirs: set[Path] = set()
|
|
46
|
+
|
|
47
|
+
def _append_if_exists(path: Path) -> None:
|
|
48
|
+
if not path.exists():
|
|
49
|
+
return
|
|
50
|
+
resolved = path.resolve()
|
|
51
|
+
if resolved in seen_dirs:
|
|
52
|
+
return
|
|
53
|
+
seen_dirs.add(resolved)
|
|
54
|
+
lib_dirs.append(resolved)
|
|
55
|
+
|
|
56
|
+
_append_if_exists(utils_root / "_stardist" / "lib")
|
|
57
|
+
for entry in list(sys.path):
|
|
58
|
+
if not entry:
|
|
59
|
+
continue
|
|
60
|
+
try:
|
|
61
|
+
legacy_candidate = (
|
|
62
|
+
Path(entry)
|
|
63
|
+
/ "senoquant"
|
|
64
|
+
/ "tabs"
|
|
65
|
+
/ "segmentation"
|
|
66
|
+
/ "stardist_onnx_utils"
|
|
67
|
+
/ "_stardist"
|
|
68
|
+
/ "lib"
|
|
69
|
+
)
|
|
70
|
+
ext_candidate = Path(entry) / "senoquant_stardist_ext" / "lib"
|
|
71
|
+
except (TypeError, ValueError, OSError):
|
|
72
|
+
continue
|
|
73
|
+
_append_if_exists(legacy_candidate)
|
|
74
|
+
_append_if_exists(ext_candidate)
|
|
75
|
+
|
|
76
|
+
if base_pkg in sys.modules:
|
|
77
|
+
pkg = sys.modules[base_pkg]
|
|
78
|
+
pkg.__path__ = [str(p) for p in lib_dirs]
|
|
79
|
+
else:
|
|
80
|
+
pkg = types.ModuleType(base_pkg)
|
|
81
|
+
pkg.__path__ = [str(p) for p in lib_dirs]
|
|
82
|
+
sys.modules[base_pkg] = pkg
|
|
83
|
+
|
|
84
|
+
for module_name in (f"{base_pkg}.stardist2d", f"{base_pkg}.stardist3d"):
|
|
85
|
+
try:
|
|
86
|
+
spec = importlib.util.find_spec(module_name)
|
|
87
|
+
except (ImportError, AttributeError, ValueError):
|
|
88
|
+
spec = None
|
|
89
|
+
if spec and spec.origin:
|
|
90
|
+
try:
|
|
91
|
+
candidate = Path(spec.origin).parent
|
|
92
|
+
except (TypeError, OSError):
|
|
93
|
+
candidate = None
|
|
94
|
+
if candidate is not None and candidate.exists():
|
|
95
|
+
lib_dirs.append(candidate)
|
|
96
|
+
|
|
97
|
+
mod2d = f"{base_pkg}.stardist2d"
|
|
98
|
+
mod3d = f"{base_pkg}.stardist3d"
|
|
99
|
+
|
|
100
|
+
def _stub(*_args: object, **_kwargs: object) -> None:
|
|
101
|
+
msg = "StarDist compiled ops are unavailable."
|
|
102
|
+
raise RuntimeError(msg)
|
|
103
|
+
|
|
104
|
+
def _module_available(module_name: str) -> bool:
|
|
105
|
+
module = sys.modules.get(module_name)
|
|
106
|
+
if module is not None:
|
|
107
|
+
return getattr(module, "__file__", None) is not None
|
|
108
|
+
try:
|
|
109
|
+
spec = importlib.util.find_spec(module_name)
|
|
110
|
+
except (ImportError, AttributeError, ValueError):
|
|
111
|
+
return False
|
|
112
|
+
return bool(spec and spec.origin)
|
|
113
|
+
|
|
114
|
+
def _try_load_dll(module_name: str, dll_path: Path) -> bool:
|
|
115
|
+
try:
|
|
116
|
+
loader = importlib.machinery.ExtensionFileLoader(
|
|
117
|
+
module_name,
|
|
118
|
+
str(dll_path),
|
|
119
|
+
)
|
|
120
|
+
spec = importlib.util.spec_from_file_location(
|
|
121
|
+
module_name,
|
|
122
|
+
str(dll_path),
|
|
123
|
+
loader=loader,
|
|
124
|
+
)
|
|
125
|
+
if spec is None or spec.loader is None:
|
|
126
|
+
return False
|
|
127
|
+
module = importlib.util.module_from_spec(spec)
|
|
128
|
+
spec.loader.exec_module(module)
|
|
129
|
+
sys.modules[module_name] = module
|
|
130
|
+
except (ImportError, OSError, AttributeError, ValueError):
|
|
131
|
+
return False
|
|
132
|
+
else:
|
|
133
|
+
return True
|
|
134
|
+
|
|
135
|
+
if not _module_available(mod2d):
|
|
136
|
+
for lib_dir in lib_dirs:
|
|
137
|
+
candidates = sorted(lib_dir.glob("stardist2d*.dll"))
|
|
138
|
+
if candidates and _try_load_dll(mod2d, candidates[0]):
|
|
139
|
+
break
|
|
140
|
+
if not _module_available(mod3d):
|
|
141
|
+
for lib_dir in lib_dirs:
|
|
142
|
+
candidates = sorted(lib_dir.glob("stardist3d*.dll"))
|
|
143
|
+
if candidates and _try_load_dll(mod3d, candidates[0]):
|
|
144
|
+
break
|
|
145
|
+
|
|
146
|
+
has_2d = _module_available(mod2d)
|
|
147
|
+
has_3d = _module_available(mod3d)
|
|
148
|
+
|
|
149
|
+
if has_2d and mod2d in sys.modules and getattr(
|
|
150
|
+
sys.modules[mod2d], "__file__", None,
|
|
151
|
+
) is None:
|
|
152
|
+
del sys.modules[mod2d]
|
|
153
|
+
if not has_2d and mod2d not in sys.modules:
|
|
154
|
+
module = types.ModuleType(mod2d)
|
|
155
|
+
module.c_star_dist = _stub # type: ignore[attr-defined]
|
|
156
|
+
module.c_non_max_suppression_inds_old = _stub # type: ignore[attr-defined]
|
|
157
|
+
module.c_non_max_suppression_inds = _stub # type: ignore[attr-defined]
|
|
158
|
+
sys.modules[mod2d] = module
|
|
159
|
+
|
|
160
|
+
if has_3d and mod3d in sys.modules and getattr(
|
|
161
|
+
sys.modules[mod3d], "__file__", None,
|
|
162
|
+
) is None:
|
|
163
|
+
del sys.modules[mod3d]
|
|
164
|
+
if not has_3d and mod3d not in sys.modules:
|
|
165
|
+
module = types.ModuleType(mod3d)
|
|
166
|
+
module.c_star_dist3d = _stub # type: ignore[attr-defined]
|
|
167
|
+
module.c_polyhedron_to_label = _stub # type: ignore[attr-defined]
|
|
168
|
+
module.c_non_max_suppression_inds = _stub # type: ignore[attr-defined]
|
|
169
|
+
sys.modules[mod3d] = module
|
|
170
|
+
|
|
171
|
+
return has_2d, has_3d
|
senoquant/tabs/spots/frontend.py
CHANGED
|
@@ -118,7 +118,7 @@ class SpotsTab(QWidget):
|
|
|
118
118
|
backend : SpotsBackend or None
|
|
119
119
|
Backend instance used to discover and load detectors.
|
|
120
120
|
napari_viewer : object or None
|
|
121
|
-
|
|
121
|
+
napari viewer used to populate layer choices.
|
|
122
122
|
"""
|
|
123
123
|
|
|
124
124
|
def __init__(
|
|
@@ -491,11 +491,15 @@ class SpotsTab(QWidget):
|
|
|
491
491
|
detector = self._backend.get_detector(detector_name)
|
|
492
492
|
layer = self._get_layer_by_name(self._layer_combo.currentText())
|
|
493
493
|
settings = self._collect_settings()
|
|
494
|
+
|
|
495
|
+
def run_detector() -> dict:
|
|
496
|
+
return detector.run(layer=layer, settings=settings)
|
|
497
|
+
|
|
494
498
|
self._start_background_run(
|
|
495
499
|
run_button=self._run_button,
|
|
496
500
|
run_text="Run",
|
|
497
501
|
detector_name=detector_name,
|
|
498
|
-
run_callable=
|
|
502
|
+
run_callable=run_detector,
|
|
499
503
|
on_success=lambda result: self._handle_run_result(
|
|
500
504
|
layer, detector_name, result
|
|
501
505
|
),
|
|
@@ -653,8 +657,41 @@ class SpotsTab(QWidget):
|
|
|
653
657
|
if self._viewer is None or source_layer is None:
|
|
654
658
|
return
|
|
655
659
|
name = self._spot_label_name(source_layer, detector_name)
|
|
656
|
-
|
|
657
|
-
|
|
660
|
+
source_metadata = getattr(source_layer, "metadata", {})
|
|
661
|
+
merged_metadata: dict[str, object] = {}
|
|
662
|
+
if isinstance(source_metadata, dict):
|
|
663
|
+
merged_metadata.update(source_metadata)
|
|
664
|
+
merged_metadata["task"] = "spots"
|
|
665
|
+
|
|
666
|
+
labels_layer = None
|
|
667
|
+
if Labels is not None and hasattr(self._viewer, "add_layer"):
|
|
668
|
+
# Add a fully configured Labels layer object to avoid name-based lookup.
|
|
669
|
+
labels_layer = Labels(
|
|
670
|
+
mask,
|
|
671
|
+
name=name,
|
|
672
|
+
metadata=merged_metadata,
|
|
673
|
+
)
|
|
674
|
+
added_layer = self._viewer.add_layer(labels_layer)
|
|
675
|
+
if added_layer is not None:
|
|
676
|
+
labels_layer = added_layer
|
|
677
|
+
elif hasattr(self._viewer, "add_labels"):
|
|
678
|
+
try:
|
|
679
|
+
labels_layer = self._viewer.add_labels(
|
|
680
|
+
mask,
|
|
681
|
+
name=name,
|
|
682
|
+
metadata=merged_metadata,
|
|
683
|
+
)
|
|
684
|
+
except TypeError:
|
|
685
|
+
labels_layer = self._viewer.add_labels(mask, name=name)
|
|
686
|
+
|
|
687
|
+
if labels_layer is None:
|
|
688
|
+
return
|
|
689
|
+
|
|
690
|
+
layer_metadata = getattr(labels_layer, "metadata", {})
|
|
691
|
+
if isinstance(layer_metadata, dict):
|
|
692
|
+
merged_metadata.update(layer_metadata)
|
|
693
|
+
merged_metadata["task"] = "spots"
|
|
694
|
+
labels_layer.metadata = merged_metadata
|
|
658
695
|
labels_layer.contour = 1
|
|
659
696
|
|
|
660
697
|
def _apply_size_filter(self, mask: np.ndarray) -> np.ndarray:
|
|
@@ -722,7 +759,7 @@ class SpotsTab(QWidget):
|
|
|
722
759
|
Parameters
|
|
723
760
|
----------
|
|
724
761
|
layer : object or None
|
|
725
|
-
|
|
762
|
+
napari layer to validate.
|
|
726
763
|
label : str
|
|
727
764
|
User-facing label for notifications.
|
|
728
765
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ufish",
|
|
3
|
+
"description": "U-FISH local-maxima seeded watershed detector",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"order": 0,
|
|
6
|
+
"settings": [
|
|
7
|
+
{
|
|
8
|
+
"key": "threshold",
|
|
9
|
+
"label": "Threshold",
|
|
10
|
+
"type": "float",
|
|
11
|
+
"decimals": 2,
|
|
12
|
+
"min": 0.0,
|
|
13
|
+
"max": 1.0,
|
|
14
|
+
"default": 0.5
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""U-FISH local-maxima seeded watershed detector."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from scipy import ndimage as ndi
|
|
7
|
+
from skimage.filters import laplace
|
|
8
|
+
from skimage.morphology import local_maxima
|
|
9
|
+
from skimage.segmentation import watershed
|
|
10
|
+
|
|
11
|
+
from ..base import SenoQuantSpotDetector
|
|
12
|
+
from senoquant.utils import layer_data_asarray
|
|
13
|
+
from senoquant.tabs.spots.ufish_utils import UFishConfig, enhance_image
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
DEFAULT_THRESHOLD = 0.5
|
|
17
|
+
USE_LAPLACE_FOR_PEAKS = False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _clamp_threshold(value: float) -> float:
|
|
21
|
+
"""Clamp threshold to the inclusive [0.0, 1.0] range."""
|
|
22
|
+
return float(np.clip(value, 0.0, 1.0))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _normalize_unit(image: np.ndarray) -> np.ndarray:
|
|
26
|
+
"""Normalize to float32 in [0, 1] using 0.05/99.95 percentiles."""
|
|
27
|
+
data = np.asarray(image, dtype=np.float32)
|
|
28
|
+
low, high = np.nanpercentile(data, [0.05, 99.95])
|
|
29
|
+
low = float(low)
|
|
30
|
+
high = float(high)
|
|
31
|
+
if high <= low:
|
|
32
|
+
return np.zeros_like(data, dtype=np.float32)
|
|
33
|
+
normalized = (data - low) / (high - low)
|
|
34
|
+
return np.clip(normalized, 0.0, 1.0).astype(np.float32, copy=False)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _markers_from_local_maxima(
|
|
38
|
+
enhanced: np.ndarray,
|
|
39
|
+
threshold: float,
|
|
40
|
+
use_laplace: bool = True,
|
|
41
|
+
) -> np.ndarray:
|
|
42
|
+
"""Build marker labels from U-FISH local maxima calls."""
|
|
43
|
+
connectivity = max(1, min(2, enhanced.ndim))
|
|
44
|
+
response = (
|
|
45
|
+
laplace(enhanced.astype(np.float32, copy=False))
|
|
46
|
+
if use_laplace
|
|
47
|
+
else np.asarray(enhanced, dtype=np.float32)
|
|
48
|
+
)
|
|
49
|
+
mask = local_maxima(response, connectivity=connectivity)
|
|
50
|
+
mask = mask & (response > threshold)
|
|
51
|
+
|
|
52
|
+
markers = np.zeros(enhanced.shape, dtype=np.int32)
|
|
53
|
+
coords = np.argwhere(mask)
|
|
54
|
+
if coords.size == 0:
|
|
55
|
+
return markers
|
|
56
|
+
|
|
57
|
+
max_indices = np.asarray(enhanced.shape) - 1
|
|
58
|
+
coords = np.clip(coords, 0, max_indices)
|
|
59
|
+
markers[tuple(coords.T)] = 1
|
|
60
|
+
|
|
61
|
+
structure = ndi.generate_binary_structure(enhanced.ndim, 1)
|
|
62
|
+
marker_labels, _num = ndi.label(markers > 0, structure=structure)
|
|
63
|
+
return marker_labels.astype(np.int32, copy=False)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _segment_from_markers(
|
|
67
|
+
enhanced: np.ndarray,
|
|
68
|
+
markers: np.ndarray,
|
|
69
|
+
threshold: float,
|
|
70
|
+
) -> np.ndarray:
|
|
71
|
+
"""Run watershed from local-maxima markers inside threshold foreground."""
|
|
72
|
+
foreground = enhanced > threshold
|
|
73
|
+
if not np.any(foreground):
|
|
74
|
+
return np.zeros_like(enhanced, dtype=np.int32)
|
|
75
|
+
|
|
76
|
+
seeded_markers = markers * foreground.astype(np.int32, copy=False)
|
|
77
|
+
if not np.any(seeded_markers > 0):
|
|
78
|
+
return np.zeros_like(enhanced, dtype=np.int32)
|
|
79
|
+
|
|
80
|
+
labels = watershed(
|
|
81
|
+
-enhanced.astype(np.float32, copy=False),
|
|
82
|
+
markers=seeded_markers,
|
|
83
|
+
mask=foreground,
|
|
84
|
+
)
|
|
85
|
+
return labels.astype(np.int32, copy=False)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class UFishDetector(SenoQuantSpotDetector):
|
|
89
|
+
"""Spot detector using U-FISH local maxima and watershed expansion."""
|
|
90
|
+
|
|
91
|
+
def __init__(self, models_root=None) -> None:
|
|
92
|
+
super().__init__("ufish", models_root=models_root)
|
|
93
|
+
|
|
94
|
+
def run(self, **kwargs) -> dict:
|
|
95
|
+
"""Run U-FISH seeded watershed and return instance labels."""
|
|
96
|
+
layer = kwargs.get("layer")
|
|
97
|
+
if layer is None:
|
|
98
|
+
return {"mask": None, "points": None}
|
|
99
|
+
if getattr(layer, "rgb", False):
|
|
100
|
+
raise ValueError("U-FISH detector requires single-channel images.")
|
|
101
|
+
|
|
102
|
+
settings = kwargs.get("settings", {}) or {}
|
|
103
|
+
threshold = _clamp_threshold(float(settings.get("threshold", DEFAULT_THRESHOLD)))
|
|
104
|
+
use_laplace = USE_LAPLACE_FOR_PEAKS
|
|
105
|
+
|
|
106
|
+
data = layer_data_asarray(layer)
|
|
107
|
+
if data.ndim not in (2, 3):
|
|
108
|
+
raise ValueError("U-FISH detector expects 2D images or 3D stacks.")
|
|
109
|
+
|
|
110
|
+
# Percentile normalize the data
|
|
111
|
+
data = _normalize_unit(data)
|
|
112
|
+
|
|
113
|
+
enhanced = enhance_image(np.asarray(data, dtype=np.float32), config=UFishConfig())
|
|
114
|
+
enhanced = np.asarray(enhanced, dtype=np.float32)
|
|
115
|
+
|
|
116
|
+
# Re-normalize after enhancement
|
|
117
|
+
enhanced = _normalize_unit(enhanced)
|
|
118
|
+
|
|
119
|
+
markers = _markers_from_local_maxima(
|
|
120
|
+
enhanced,
|
|
121
|
+
threshold,
|
|
122
|
+
use_laplace=use_laplace,
|
|
123
|
+
)
|
|
124
|
+
labels = _segment_from_markers(
|
|
125
|
+
enhanced,
|
|
126
|
+
markers,
|
|
127
|
+
threshold,
|
|
128
|
+
)
|
|
129
|
+
return {"mask": labels}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Public UFish utility API for spot enhancement.
|
|
2
|
+
|
|
3
|
+
This package exposes a minimal stable surface used by the Spots tab:
|
|
4
|
+
|
|
5
|
+
``UFishConfig``
|
|
6
|
+
Configuration dataclass for model initialization and weight loading.
|
|
7
|
+
``enhance_image``
|
|
8
|
+
Convenience function that runs UFish enhancement on an input image.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .core import UFishConfig, enhance_image
|
|
12
|
+
|
|
13
|
+
__all__ = ["UFishConfig", "enhance_image"]
|