senoquant 1.0.0b2__py3-none-any.whl → 1.0.0b4__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 (57) hide show
  1. senoquant/__init__.py +6 -2
  2. senoquant/_reader.py +1 -1
  3. senoquant/_widget.py +9 -1
  4. senoquant/reader/core.py +201 -18
  5. senoquant/tabs/__init__.py +2 -0
  6. senoquant/tabs/batch/backend.py +76 -27
  7. senoquant/tabs/batch/frontend.py +127 -25
  8. senoquant/tabs/quantification/features/marker/dialog.py +26 -6
  9. senoquant/tabs/quantification/features/marker/export.py +97 -24
  10. senoquant/tabs/quantification/features/marker/rows.py +2 -2
  11. senoquant/tabs/quantification/features/spots/dialog.py +41 -11
  12. senoquant/tabs/quantification/features/spots/export.py +163 -10
  13. senoquant/tabs/quantification/frontend.py +2 -2
  14. senoquant/tabs/segmentation/frontend.py +46 -9
  15. senoquant/tabs/segmentation/models/cpsam/model.py +1 -1
  16. senoquant/tabs/segmentation/models/default_2d/model.py +22 -77
  17. senoquant/tabs/segmentation/models/default_3d/model.py +8 -74
  18. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/tools/create_zip_contents.py +0 -0
  19. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/probe.py +13 -13
  20. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/stardist_libs.py +171 -0
  21. senoquant/tabs/spots/frontend.py +96 -5
  22. senoquant/tabs/spots/models/rmp/details.json +3 -9
  23. senoquant/tabs/spots/models/rmp/model.py +341 -266
  24. senoquant/tabs/spots/models/ufish/details.json +32 -0
  25. senoquant/tabs/spots/models/ufish/model.py +327 -0
  26. senoquant/tabs/spots/ufish_utils/__init__.py +13 -0
  27. senoquant/tabs/spots/ufish_utils/core.py +387 -0
  28. senoquant/tabs/visualization/__init__.py +1 -0
  29. senoquant/tabs/visualization/backend.py +306 -0
  30. senoquant/tabs/visualization/frontend.py +1113 -0
  31. senoquant/tabs/visualization/plots/__init__.py +80 -0
  32. senoquant/tabs/visualization/plots/base.py +152 -0
  33. senoquant/tabs/visualization/plots/double_expression.py +187 -0
  34. senoquant/tabs/visualization/plots/spatialplot.py +156 -0
  35. senoquant/tabs/visualization/plots/umap.py +140 -0
  36. senoquant/utils.py +1 -1
  37. senoquant-1.0.0b4.dist-info/METADATA +162 -0
  38. {senoquant-1.0.0b2.dist-info → senoquant-1.0.0b4.dist-info}/RECORD +53 -30
  39. {senoquant-1.0.0b2.dist-info → senoquant-1.0.0b4.dist-info}/top_level.txt +1 -0
  40. ufish/__init__.py +1 -0
  41. ufish/api.py +778 -0
  42. ufish/model/__init__.py +0 -0
  43. ufish/model/loss.py +62 -0
  44. ufish/model/network/__init__.py +0 -0
  45. ufish/model/network/spot_learn.py +50 -0
  46. ufish/model/network/ufish_net.py +204 -0
  47. ufish/model/train.py +175 -0
  48. ufish/utils/__init__.py +0 -0
  49. ufish/utils/img.py +418 -0
  50. ufish/utils/log.py +8 -0
  51. ufish/utils/spot_calling.py +115 -0
  52. senoquant/tabs/spots/models/udwt/details.json +0 -103
  53. senoquant/tabs/spots/models/udwt/model.py +0 -482
  54. senoquant-1.0.0b2.dist-info/METADATA +0 -193
  55. {senoquant-1.0.0b2.dist-info → senoquant-1.0.0b4.dist-info}/WHEEL +0 -0
  56. {senoquant-1.0.0b2.dist-info → senoquant-1.0.0b4.dist-info}/entry_points.txt +0 -0
  57. {senoquant-1.0.0b2.dist-info → senoquant-1.0.0b4.dist-info}/licenses/LICENSE +0 -0
@@ -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
- elif div_by:
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
@@ -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
- Napari viewer used to populate layer choices.
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=lambda: detector.run(layer=layer, settings=settings),
502
+ run_callable=run_detector,
499
503
  on_success=lambda result: self._handle_run_result(
500
504
  layer, detector_name, result
501
505
  ),
@@ -647,16 +651,103 @@ class SpotsTab(QWidget):
647
651
  if mask is not None:
648
652
  filtered_mask = self._apply_size_filter(mask)
649
653
  self._add_labels_layer(layer, filtered_mask, detector_name)
654
+ debug_images = result.get("debug_images")
655
+ if isinstance(debug_images, dict):
656
+ self._add_debug_image_layers(layer, detector_name, debug_images)
650
657
 
651
658
  def _add_labels_layer(self, source_layer, mask, detector_name: str) -> None:
652
659
  """Add a labels layer for the detector mask."""
653
660
  if self._viewer is None or source_layer is None:
654
661
  return
655
662
  name = self._spot_label_name(source_layer, detector_name)
656
- self._viewer.add_labels(mask, name=name)
657
- labels_layer = self._viewer.layers[name]
663
+ source_metadata = getattr(source_layer, "metadata", {})
664
+ merged_metadata: dict[str, object] = {}
665
+ if isinstance(source_metadata, dict):
666
+ merged_metadata.update(source_metadata)
667
+ merged_metadata["task"] = "spots"
668
+
669
+ labels_layer = None
670
+ if Labels is not None and hasattr(self._viewer, "add_layer"):
671
+ # Add a fully configured Labels layer object to avoid name-based lookup.
672
+ labels_layer = Labels(
673
+ mask,
674
+ name=name,
675
+ metadata=merged_metadata,
676
+ )
677
+ added_layer = self._viewer.add_layer(labels_layer)
678
+ if added_layer is not None:
679
+ labels_layer = added_layer
680
+ elif hasattr(self._viewer, "add_labels"):
681
+ try:
682
+ labels_layer = self._viewer.add_labels(
683
+ mask,
684
+ name=name,
685
+ metadata=merged_metadata,
686
+ )
687
+ except TypeError:
688
+ labels_layer = self._viewer.add_labels(mask, name=name)
689
+
690
+ if labels_layer is None:
691
+ return
692
+
693
+ layer_metadata = getattr(labels_layer, "metadata", {})
694
+ if isinstance(layer_metadata, dict):
695
+ merged_metadata.update(layer_metadata)
696
+ merged_metadata["task"] = "spots"
697
+ labels_layer.metadata = merged_metadata
658
698
  labels_layer.contour = 1
659
699
 
700
+ def _add_debug_image_layers(
701
+ self,
702
+ source_layer,
703
+ detector_name: str,
704
+ debug_images: dict,
705
+ ) -> None:
706
+ """Add debug image layers emitted by a detector run."""
707
+ if self._viewer is None:
708
+ return
709
+ source_name = getattr(source_layer, "name", "") if source_layer is not None else ""
710
+ source_name = source_name.strip() if isinstance(source_name, str) else ""
711
+
712
+ for debug_key, debug_image in debug_images.items():
713
+ if debug_image is None:
714
+ continue
715
+ image_data = np.asarray(debug_image)
716
+ if image_data.size == 0:
717
+ continue
718
+ layer_name = f"{detector_name}_{debug_key}"
719
+ if source_name:
720
+ layer_name = f"{source_name}_{layer_name}"
721
+ if layer_name in self._viewer.layers:
722
+ self._viewer.layers.remove(layer_name)
723
+
724
+ metadata = {"task": "spots", "debug": True}
725
+ image_layer = None
726
+ if Image is not None and hasattr(self._viewer, "add_layer"):
727
+ image_layer = Image(
728
+ image_data,
729
+ name=layer_name,
730
+ metadata=metadata,
731
+ )
732
+ added_layer = self._viewer.add_layer(image_layer)
733
+ if added_layer is not None:
734
+ image_layer = added_layer
735
+ elif hasattr(self._viewer, "add_image"):
736
+ try:
737
+ image_layer = self._viewer.add_image(
738
+ image_data,
739
+ name=layer_name,
740
+ metadata=metadata,
741
+ )
742
+ except TypeError:
743
+ image_layer = self._viewer.add_image(
744
+ image_data,
745
+ name=layer_name,
746
+ )
747
+
748
+ if image_layer is not None:
749
+ image_layer.visible = True
750
+
660
751
  def _apply_size_filter(self, mask: np.ndarray) -> np.ndarray:
661
752
  """Filter spots by size based on min/max settings.
662
753
 
@@ -722,7 +813,7 @@ class SpotsTab(QWidget):
722
813
  Parameters
723
814
  ----------
724
815
  layer : object or None
725
- Napari layer to validate.
816
+ napari layer to validate.
726
817
  label : str
727
818
  User-facing label for notifications.
728
819
 
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "rmp",
3
- "description": "Placeholder details for the rmp spot detector",
3
+ "description": "RMP spot detector implementation",
4
4
  "version": "0.1.0",
5
- "order": 2,
5
+ "order": 0,
6
6
  "settings": [
7
7
  {
8
8
  "key": "denoising_kernel_length",
@@ -17,7 +17,7 @@
17
17
  "key": "extraction_kernel_length",
18
18
  "label": "Extraction kernel length",
19
19
  "type": "int",
20
- "min": 1,
20
+ "min": 3,
21
21
  "max": 9999,
22
22
  "default": 10
23
23
  },
@@ -50,12 +50,6 @@
50
50
  "label": "Enable denoising",
51
51
  "type": "bool",
52
52
  "default": true
53
- },
54
- {
55
- "key": "use_3d",
56
- "label": "3D",
57
- "type": "bool",
58
- "default": false
59
53
  }
60
54
  ]
61
55
  }