itasc 0.2.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 (162) hide show
  1. itasc/__init__.py +16 -0
  2. itasc/cellpose/__init__.py +31 -0
  3. itasc/cellpose/cellpose_runner.py +334 -0
  4. itasc/cellpose/divergence_maps.py +380 -0
  5. itasc/cellpose/flow_following.py +283 -0
  6. itasc/cellpose/joint.py +98 -0
  7. itasc/cellpose/napari.yaml +10 -0
  8. itasc/cellpose/native_masks.py +313 -0
  9. itasc/cellpose/retrack.py +232 -0
  10. itasc/cellpose/shape.py +51 -0
  11. itasc/cellpose/track_laptrack.py +279 -0
  12. itasc/contact_analysis/__init__.py +61 -0
  13. itasc/contact_analysis/_provenance.py +29 -0
  14. itasc/contact_analysis/catalog.py +393 -0
  15. itasc/contact_analysis/config.py +155 -0
  16. itasc/contact_analysis/contacts/__init__.py +24 -0
  17. itasc/contact_analysis/contacts/batch.py +158 -0
  18. itasc/contact_analysis/contacts/build.py +837 -0
  19. itasc/contact_analysis/contacts/contact_labels.py +136 -0
  20. itasc/contact_analysis/contacts/neighborhood.py +146 -0
  21. itasc/contact_analysis/contacts/reader.py +58 -0
  22. itasc/contact_analysis/contacts/signed_contact_length.py +143 -0
  23. itasc/contact_analysis/curation.py +185 -0
  24. itasc/contact_analysis/dynamics/__init__.py +43 -0
  25. itasc/contact_analysis/dynamics/collective.py +206 -0
  26. itasc/contact_analysis/dynamics/kinematics.py +278 -0
  27. itasc/contact_analysis/dynamics/msd.py +165 -0
  28. itasc/contact_analysis/dynamics/store.py +251 -0
  29. itasc/contact_analysis/dynamics/trajectories.py +100 -0
  30. itasc/contact_analysis/frame_interval.py +85 -0
  31. itasc/contact_analysis/napari.yaml +10 -0
  32. itasc/contact_analysis/pipeline.py +313 -0
  33. itasc/contact_analysis/pixel_size.py +166 -0
  34. itasc/contact_analysis/quantifier.py +204 -0
  35. itasc/contact_analysis/quantifiers/__init__.py +7 -0
  36. itasc/contact_analysis/quantifiers/_contacts_derived.py +42 -0
  37. itasc/contact_analysis/quantifiers/cell_density.py +65 -0
  38. itasc/contact_analysis/quantifiers/cell_dynamics.py +81 -0
  39. itasc/contact_analysis/quantifiers/cell_shape.py +32 -0
  40. itasc/contact_analysis/quantifiers/contacts.py +72 -0
  41. itasc/contact_analysis/quantifiers/neighbor_count.py +26 -0
  42. itasc/contact_analysis/quantifiers/nucleus_dynamics.py +77 -0
  43. itasc/contact_analysis/quantifiers/nucleus_shape.py +35 -0
  44. itasc/contact_analysis/quantifiers/shape_relational.py +34 -0
  45. itasc/contact_analysis/quantifiers/signed_contact_length.py +44 -0
  46. itasc/contact_analysis/records.py +129 -0
  47. itasc/contact_analysis/shape/__init__.py +19 -0
  48. itasc/contact_analysis/shape/core.py +141 -0
  49. itasc/contact_analysis/shape/relational.py +134 -0
  50. itasc/contact_analysis/shape_tables.py +387 -0
  51. itasc/core/__init__.py +0 -0
  52. itasc/core/cancellation.py +15 -0
  53. itasc/core/commit.py +61 -0
  54. itasc/core/imageops.py +34 -0
  55. itasc/core/label_store.py +53 -0
  56. itasc/core/lineage.py +113 -0
  57. itasc/core/logging.py +51 -0
  58. itasc/core/paths.py +34 -0
  59. itasc/core/tiff.py +13 -0
  60. itasc/correction/__init__.py +25 -0
  61. itasc/correction/labels.py +983 -0
  62. itasc/napari/__init__.py +7 -0
  63. itasc/napari/_experiments_panel.py +1091 -0
  64. itasc/napari/_flow_layout.py +98 -0
  65. itasc/napari/_icons.py +126 -0
  66. itasc/napari/_napari_compat.py +118 -0
  67. itasc/napari/_paths.py +231 -0
  68. itasc/napari/_preview_cache.py +60 -0
  69. itasc/napari/_spotlight.py +46 -0
  70. itasc/napari/_stage_loader.py +146 -0
  71. itasc/napari/_stage_status.py +110 -0
  72. itasc/napari/_standalone_paths.py +89 -0
  73. itasc/napari/_state.py +146 -0
  74. itasc/napari/_status_rail.py +128 -0
  75. itasc/napari/_track_render.py +114 -0
  76. itasc/napari/_widget_helpers.py +236 -0
  77. itasc/napari/aggregate_widget.py +339 -0
  78. itasc/napari/candidate_gallery_controller.py +285 -0
  79. itasc/napari/cell_workflow_widget.py +1405 -0
  80. itasc/napari/cellpose_segment_track_widget.py +1253 -0
  81. itasc/napari/cellpose_widget.py +1034 -0
  82. itasc/napari/contact_analysis/__init__.py +6 -0
  83. itasc/napari/contact_analysis/curation_controller.py +94 -0
  84. itasc/napari/contact_analysis/plugins/__init__.py +102 -0
  85. itasc/napari/contact_analysis/plugins/catalog_summary.py +56 -0
  86. itasc/napari/contact_analysis/plugins/curation.py +247 -0
  87. itasc/napari/contact_analysis/plugins/visualize_contacts.py +64 -0
  88. itasc/napari/contact_analysis_widget.py +951 -0
  89. itasc/napari/contact_visualization.py +895 -0
  90. itasc/napari/correction/__init__.py +8 -0
  91. itasc/napari/correction/_correction_anchor.py +54 -0
  92. itasc/napari/correction/_correction_candidate_panel.py +259 -0
  93. itasc/napari/correction/_correction_candidates.py +169 -0
  94. itasc/napari/correction/_correction_centroids.py +132 -0
  95. itasc/napari/correction/_correction_commit.py +71 -0
  96. itasc/napari/correction/_correction_events.py +39 -0
  97. itasc/napari/correction/_correction_keymap.py +80 -0
  98. itasc/napari/correction/_correction_layer_lifecycle.py +137 -0
  99. itasc/napari/correction/_correction_layer_loader.py +86 -0
  100. itasc/napari/correction/_correction_navigation.py +97 -0
  101. itasc/napari/correction/_correction_paint.py +46 -0
  102. itasc/napari/correction/_correction_playback.py +68 -0
  103. itasc/napari/correction/_correction_protection.py +44 -0
  104. itasc/napari/correction/_correction_takeover.py +64 -0
  105. itasc/napari/correction/_correction_track_accordion.py +566 -0
  106. itasc/napari/correction/_correction_track_path.py +388 -0
  107. itasc/napari/correction/_correction_ui.py +367 -0
  108. itasc/napari/correction/_correction_ui_nucleus.py +393 -0
  109. itasc/napari/correction/_correction_utils.py +204 -0
  110. itasc/napari/correction/_correction_validation.py +83 -0
  111. itasc/napari/correction/cell_correction_widget.py +1190 -0
  112. itasc/napari/correction/correction_widget.py +1363 -0
  113. itasc/napari/correction/nucleus_correction_widget.py +2185 -0
  114. itasc/napari/divergence_maps_widget.py +795 -0
  115. itasc/napari/lineage_canvas_controller.py +351 -0
  116. itasc/napari/main_widget.py +1184 -0
  117. itasc/napari/nucleus_atom_extraction_widget.py +648 -0
  118. itasc/napari/nucleus_db_browser_widget.py +1442 -0
  119. itasc/napari/nucleus_pipeline_widget.py +579 -0
  120. itasc/napari/nucleus_segmentation_inputs_widget.py +21 -0
  121. itasc/napari/nucleus_tracking_inputs_widget.py +248 -0
  122. itasc/napari/nucleus_workflow_widget.py +963 -0
  123. itasc/napari/track_path_controller.py +387 -0
  124. itasc/napari/ui_gate.py +266 -0
  125. itasc/napari/ui_style.py +503 -0
  126. itasc/napari/validated_overlay_controller.py +254 -0
  127. itasc/napari/widgets.py +616 -0
  128. itasc/napari.yaml +45 -0
  129. itasc/segmentation/__init__.py +60 -0
  130. itasc/segmentation/cell_divergence_segmentation.py +433 -0
  131. itasc/segmentation/cell_label_icm.py +637 -0
  132. itasc/segmentation/contour_filtering.py +128 -0
  133. itasc/segmentation/lineage.py +18 -0
  134. itasc/segmentation/nucleus_segmentation.py +35 -0
  135. itasc/tracking_ultrack/__init__.py +12 -0
  136. itasc/tracking_ultrack/_node_geometry.py +191 -0
  137. itasc/tracking_ultrack/atoms.py +470 -0
  138. itasc/tracking_ultrack/config.py +76 -0
  139. itasc/tracking_ultrack/corrections.py +1213 -0
  140. itasc/tracking_ultrack/db_build.py +492 -0
  141. itasc/tracking_ultrack/db_query.py +952 -0
  142. itasc/tracking_ultrack/export.py +133 -0
  143. itasc/tracking_ultrack/extend.py +243 -0
  144. itasc/tracking_ultrack/ingest.py +83 -0
  145. itasc/tracking_ultrack/linking.py +286 -0
  146. itasc/tracking_ultrack/multi_threshold.py +46 -0
  147. itasc/tracking_ultrack/napari.yaml +10 -0
  148. itasc/tracking_ultrack/reseed.py +456 -0
  149. itasc/tracking_ultrack/retracker.py +153 -0
  150. itasc/tracking_ultrack/scoring.py +74 -0
  151. itasc/tracking_ultrack/seed_prior.py +175 -0
  152. itasc/tracking_ultrack/solve.py +52 -0
  153. itasc/tracking_ultrack/swap_candidate.py +171 -0
  154. itasc/tracking_ultrack/track_quality.py +142 -0
  155. itasc/tracking_ultrack/validation_nodes.py +315 -0
  156. itasc/tracking_ultrack/validation_state.py +430 -0
  157. itasc-0.2.0.dev0.data/data/itasc/napari.yaml +45 -0
  158. itasc-0.2.0.dev0.dist-info/METADATA +186 -0
  159. itasc-0.2.0.dev0.dist-info/RECORD +162 -0
  160. itasc-0.2.0.dev0.dist-info/WHEEL +4 -0
  161. itasc-0.2.0.dev0.dist-info/entry_points.txt +2 -0
  162. itasc-0.2.0.dev0.dist-info/licenses/LICENSE +661 -0
@@ -0,0 +1,579 @@
1
+ """Pipeline action / worker-coordination widget for the nucleus workflow."""
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ import threading
6
+ from collections.abc import Callable
7
+ from pathlib import Path
8
+
9
+ import napari
10
+ import numpy as np
11
+ import tifffile
12
+ from napari.qt.threading import thread_worker
13
+ from qtpy.QtWidgets import (
14
+ QHBoxLayout,
15
+ QLabel,
16
+ QVBoxLayout,
17
+ QWidget,
18
+ )
19
+
20
+ from itasc.core.tiff import imwrite_grayscale
21
+ from itasc.napari.correction._correction_utils import reorder_stack_by_quality
22
+ from itasc.napari._paths import NucleusWorkspace
23
+ from itasc.napari._widget_helpers import (
24
+ make_progress as _make_progress,
25
+ make_status as _make_status,
26
+ tool_btn as _tool_btn,
27
+ )
28
+ from itasc.napari.ui_style import (
29
+ stage_header_action_button as _stage_header_action_button,
30
+ stage_header_label as _stage_header_label,
31
+ )
32
+ from itasc.tracking_ultrack.validation_state import read_corrections, read_validated_tracks
33
+ from itasc.core.cancellation import CancelledError
34
+ from itasc.tracking_ultrack.db_build import (
35
+ apply_annotations_and_score,
36
+ build_atom_union_database,
37
+ )
38
+ from itasc.tracking_ultrack.corrections import corrections_from_validated_tracks
39
+ from itasc.tracking_ultrack.export import export_tracked_labels
40
+ from itasc.tracking_ultrack.solve import run_solve
41
+ from itasc.tracking_ultrack.track_quality import track_quality_scores
42
+
43
+ logger = logging.getLogger(__name__)
44
+
45
+
46
+ def _ultrack_available() -> bool:
47
+ """Return True if the ultrack package is importable.
48
+
49
+ Kept as a function so the (slow) ultrack import only happens when an
50
+ action actually needs it, not at widget construction time.
51
+ """
52
+ try:
53
+ import ultrack.core.segmentation.processing # noqa: F401
54
+ except ImportError:
55
+ return False
56
+ return True
57
+
58
+
59
+ # ── Layer name constants ──────────────────────────────────────────────────────
60
+ _TRACKED_LAYER = "Tracked: Nucleus"
61
+
62
+
63
+ class NucleusPipelineWidget(QWidget):
64
+ """Action buttons, workers, and coordination handlers for the nucleus pipeline."""
65
+
66
+ def __init__(
67
+ self,
68
+ viewer: napari.Viewer,
69
+ *,
70
+ workspace_provider: Callable[[], NucleusWorkspace | None],
71
+ seg_inputs_provider: Callable,
72
+ tracking_inputs_provider: Callable,
73
+ refresh_files_callback: Callable[[], None],
74
+ refresh_db_browser_callback: Callable[[], None],
75
+ sync_viewer_activity_callback: Callable[[], None] | None = None,
76
+ parent: QWidget | None = None,
77
+ ) -> None:
78
+ super().__init__(parent)
79
+ self.viewer = viewer
80
+ self._workspace_provider = workspace_provider
81
+ self._seg_inputs_provider = seg_inputs_provider
82
+ self._tracking_inputs_provider = tracking_inputs_provider
83
+ self._refresh_files_callback = refresh_files_callback
84
+ self._refresh_db_browser_callback = refresh_db_browser_callback
85
+ self._sync_viewer_activity_callback = sync_viewer_activity_callback
86
+
87
+ self._db_gen_worker = None
88
+ self._ultrack_worker = None
89
+ self._db_gen_cancel: threading.Event | None = None
90
+ self._running_stage: str | None = None
91
+
92
+ # ── Per-stage buttons ──────────────────────────────────────────
93
+ self.seg_params_btn = _tool_btn("⚙", "Show parameters for this stage.", checkable=True)
94
+ self.seg_run_btn = _tool_btn("▶", "Run segmentation inputs.")
95
+
96
+ self.db_params_btn = _tool_btn("⚙", "Show parameters for this stage.", checkable=True)
97
+ self.db_run_btn = _tool_btn("▶", "Run Ultrack database build.")
98
+
99
+ self.solve_params_btn = _tool_btn("⚙", "Show parameters for this stage.", checkable=True)
100
+ self.solve_run_btn = _tool_btn("▶", "Run Ultrack solve.")
101
+
102
+ for button in (
103
+ self.seg_params_btn,
104
+ self.seg_run_btn,
105
+ self.db_params_btn,
106
+ self.db_run_btn,
107
+ self.solve_params_btn,
108
+ self.solve_run_btn,
109
+ ):
110
+ _stage_header_action_button(button, "nucleus")
111
+
112
+ self.pipeline_status_lbl = _make_status()
113
+ self.pipeline_progress_bar = _make_progress()
114
+
115
+ self.seg_run_btn.clicked.connect(self._on_seg_run_btn_clicked)
116
+ self.db_run_btn.clicked.connect(self._on_db_run_btn_clicked)
117
+ self.solve_run_btn.clicked.connect(self._on_solve_run_btn_clicked)
118
+
119
+ # ── Layout helpers ────────────────────────────────────────────────────────
120
+
121
+ # ── Per-row run/cancel dispatchers ───────────────────────────────────────
122
+
123
+ def _on_seg_run_btn_clicked(self) -> None:
124
+ if self._running_stage is not None:
125
+ self._on_cancel()
126
+ else:
127
+ self._on_build_segmentation_inputs()
128
+
129
+ def _on_db_run_btn_clicked(self) -> None:
130
+ if self._running_stage is not None:
131
+ self._on_cancel()
132
+ else:
133
+ self._on_run_db_generation()
134
+
135
+ def _on_solve_run_btn_clicked(self) -> None:
136
+ if self._running_stage is not None:
137
+ self._on_cancel()
138
+ else:
139
+ self._on_run_ultrack()
140
+
141
+ def build_pipeline_block(
142
+ self,
143
+ seg_section=None,
144
+ db_section=None,
145
+ solve_section=None,
146
+ ) -> QWidget:
147
+ """Build the three per-stage rows with inline params blocks."""
148
+ block = QWidget(self)
149
+ lay = QVBoxLayout(block)
150
+ lay.setContentsMargins(0, 0, 0, 0)
151
+ lay.setSpacing(6)
152
+
153
+ def _stage_label(text: str) -> QLabel:
154
+ lbl = QLabel(text)
155
+ return _stage_header_label(lbl, "nucleus")
156
+
157
+ def _stage_row(label: QLabel, *trailing: QWidget) -> QHBoxLayout:
158
+ row = QHBoxLayout()
159
+ row.setContentsMargins(0, 0, 0, 0)
160
+ row.setSpacing(4)
161
+ row.addWidget(label)
162
+ for w in trailing:
163
+ row.addWidget(w)
164
+ row.addStretch(1)
165
+ return row
166
+
167
+ # ── Ultrack database ─────────────────────────────────────────
168
+ lay.addLayout(_stage_row(
169
+ _stage_label("Ultrack database"),
170
+ self.db_params_btn,
171
+ self.db_run_btn,
172
+ ))
173
+ if db_section is not None:
174
+ self.db_params_btn.toggled.connect(
175
+ lambda checked: db_section._toggle.setChecked(checked)
176
+ )
177
+ lay.addWidget(db_section)
178
+
179
+ # ── Ultrack solve ────────────────────────────────────────────
180
+ lay.addLayout(_stage_row(
181
+ _stage_label("Ultrack solve"),
182
+ self.solve_params_btn,
183
+ self.solve_run_btn,
184
+ ))
185
+ if solve_section is not None:
186
+ self.solve_params_btn.toggled.connect(
187
+ lambda checked: solve_section._toggle.setChecked(checked)
188
+ )
189
+ lay.addWidget(solve_section)
190
+
191
+ return block
192
+
193
+ # ── Path helpers ──────────────────────────────────────────────────────────
194
+
195
+ @property
196
+ def _workspace(self) -> NucleusWorkspace | None:
197
+ return self._workspace_provider()
198
+
199
+ @property
200
+ def _pos_dir(self) -> Path | None:
201
+ """The nucleus annotation/store directory (validation JSONs live here)."""
202
+ ws = self._workspace
203
+ return ws.nucleus_dir if ws is not None else None
204
+
205
+ def _contours_path(self) -> Path | None:
206
+ ws = self._workspace
207
+ return ws.contours if ws is not None else None
208
+
209
+ def _foreground_path(self) -> Path | None:
210
+ ws = self._workspace
211
+ return ws.foreground if ws is not None else None
212
+
213
+ def _ultrack_workdir(self) -> Path | None:
214
+ ws = self._workspace
215
+ return ws.ultrack_workdir if ws is not None else None
216
+
217
+ def _ultrack_db_path(self) -> Path | None:
218
+ ws = self._workspace
219
+ return ws.ultrack_db if ws is not None else None
220
+
221
+ def _tracked_path(self) -> Path | None:
222
+ ws = self._workspace
223
+ return ws.tracked if ws is not None else None
224
+
225
+ # ── Status / progress helpers ─────────────────────────────────────────────
226
+
227
+ def _status(self, msg: str) -> None:
228
+ self.pipeline_status_lbl.setText(msg)
229
+ self.pipeline_status_lbl.setVisible(bool(msg))
230
+ if msg:
231
+ logger.info(msg)
232
+
233
+ def _progress(self, done: int, total: int, msg: str) -> None:
234
+ self.pipeline_progress_bar.setVisible(True)
235
+ self.pipeline_progress_bar.setRange(0, total)
236
+ self.pipeline_progress_bar.setValue(done)
237
+ self._status(msg)
238
+
239
+ def _on_progress(self, data) -> None:
240
+ if self._running_stage is None:
241
+ return
242
+ if isinstance(data, tuple):
243
+ self._progress(*data)
244
+ else:
245
+ self._status(str(data))
246
+
247
+ def _clear_progress(self) -> None:
248
+ self.pipeline_progress_bar.setValue(0)
249
+ self.pipeline_progress_bar.setVisible(False)
250
+
251
+ def _set_running_stage(self, stage_key: str | None) -> None:
252
+ """Swap the run buttons' ▶/✕ affordance for the active stage.
253
+
254
+ ``None`` means idle (all ▶); ``"seg" | "db" | "ultrack"`` shows ✕ on
255
+ that row. Button *enablement* is owned by the UI gate — this only
256
+ updates the cancel/run glyph and tooltip, then notifies the gate to
257
+ recompute (the gate's predicates read ``self._running_stage``).
258
+ """
259
+ self._running_stage = stage_key
260
+ if stage_key is None:
261
+ self.seg_run_btn.setText("▶")
262
+ self.seg_run_btn.setToolTip("Run segmentation inputs.")
263
+ self.db_run_btn.setText("▶")
264
+ self.db_run_btn.setToolTip("Run Ultrack database build.")
265
+ self.solve_run_btn.setText("▶")
266
+ self.solve_run_btn.setToolTip("Run Ultrack solve.")
267
+ else:
268
+ run_btn = {
269
+ "seg": self.seg_run_btn,
270
+ "db": self.db_run_btn,
271
+ "ultrack": self.solve_run_btn,
272
+ }[stage_key]
273
+ run_btn.setText("✕")
274
+ run_btn.setToolTip("Cancel.")
275
+ if self._sync_viewer_activity_callback is not None:
276
+ self._sync_viewer_activity_callback()
277
+
278
+ # ── Config delegation ─────────────────────────────────────────────────────
279
+
280
+ def _db_gen_config_from_controls(self):
281
+ return self._tracking_inputs_provider().db_gen_config()
282
+
283
+ def _ultrack_config_from_controls(self):
284
+ return self._tracking_inputs_provider().ultrack_config()
285
+
286
+ # ── Viewer helpers ────────────────────────────────────────────────────────
287
+
288
+ def _update_labels_layer(
289
+ self,
290
+ name: str,
291
+ data: np.ndarray,
292
+ *,
293
+ metadata: dict | None = None,
294
+ ) -> None:
295
+ from napari.layers import Labels
296
+ if name in self.viewer.layers and isinstance(self.viewer.layers[name], Labels):
297
+ layer = self.viewer.layers[name]
298
+ layer.data = data
299
+ layer.metadata = dict(metadata or {})
300
+ return
301
+ if name in self.viewer.layers:
302
+ self.viewer.layers.remove(name)
303
+ self.viewer.add_labels(data, name=name, metadata=dict(metadata or {}))
304
+
305
+ def _update_tracked_display(
306
+ self, labels: np.ndarray, t: int | None = None,
307
+ ) -> None:
308
+ if _TRACKED_LAYER in self.viewer.layers and t is not None:
309
+ layer = self.viewer.layers[_TRACKED_LAYER]
310
+ if layer.data.ndim == 3:
311
+ if t < layer.data.shape[0]:
312
+ new_data = layer.data.copy()
313
+ new_data[t] = labels
314
+ layer.data = new_data
315
+ return
316
+ new_data = np.concatenate(
317
+ [layer.data, labels[np.newaxis].astype(layer.data.dtype)], axis=0,
318
+ )
319
+ layer.data = new_data
320
+ return
321
+ display = labels[np.newaxis].copy() if labels.ndim == 2 else labels
322
+ self._update_labels_layer(_TRACKED_LAYER, display)
323
+
324
+ def _ensure_tracked_layer_data(self) -> np.ndarray | None:
325
+ """Return the tracked labelmap from the viewer layer if present, else
326
+ read it from disk. Does not add anything to the viewer."""
327
+ if _TRACKED_LAYER in self.viewer.layers:
328
+ return np.asarray(self.viewer.layers[_TRACKED_LAYER].data)
329
+ tracked_path = self._tracked_path()
330
+ if tracked_path is None or not tracked_path.exists():
331
+ return None
332
+ self._status(f"Reading {tracked_path.name} from disk…")
333
+ labels = np.asarray(tifffile.imread(str(tracked_path)), dtype=np.uint32)
334
+ if labels.ndim == 4 and labels.shape[1] == 1:
335
+ labels = labels[:, 0]
336
+ return labels
337
+
338
+ # ── Pipeline handlers — segmentation inputs ───────────────────────────────
339
+
340
+ def _on_build_segmentation_inputs(self) -> None:
341
+ # Cellpose maps and atom extraction now produce the candidate inputs;
342
+ # there is no separate source-threshold build step here.
343
+ self._status("Segmentation inputs are produced by Atom Extraction.")
344
+
345
+ # ── Pipeline handlers — DB generation ────────────────────────────────────
346
+
347
+ def _atoms_path(self) -> Path | None:
348
+ ws = self._workspace
349
+ return ws.atoms if ws is not None else None
350
+
351
+ def _on_run_db_generation(self) -> None:
352
+ pos_dir = self._pos_dir
353
+ if pos_dir is None:
354
+ self._status("No project open."); return
355
+ atoms_path = self._atoms_path()
356
+ if atoms_path is None or not atoms_path.exists():
357
+ self._status("Missing: atoms.tif — run Atom Extraction first.")
358
+ return
359
+ if not _ultrack_available():
360
+ self._status("ultrack not installed — activate the itasc conda environment."); return
361
+
362
+ cfg = self._db_gen_config_from_controls()
363
+ working_dir = self._ultrack_workdir()
364
+ foreground_path = self._foreground_path()
365
+ contour_path = self._contours_path()
366
+
367
+ self.pipeline_progress_bar.setRange(0, 0)
368
+ self.pipeline_progress_bar.setVisible(True)
369
+ self._status("Starting DB generation…")
370
+ self._set_running_stage("db")
371
+ cancel_event = threading.Event()
372
+ self._db_gen_cancel = cancel_event
373
+
374
+ @thread_worker(connect={
375
+ "yielded": self._on_progress,
376
+ "returned": self._on_db_gen_done,
377
+ "errored": self._on_db_gen_worker_error,
378
+ })
379
+ def _worker():
380
+ import queue as _queue
381
+ import threading
382
+
383
+ msg_queue: _queue.SimpleQueue = _queue.SimpleQueue()
384
+ result_holder: list = []
385
+ exc_holder: list = []
386
+
387
+ def _progress_cb(msg: str) -> None:
388
+ msg_queue.put(msg)
389
+
390
+ def _run() -> None:
391
+ try:
392
+ report = build_atom_union_database(
393
+ atoms_path,
394
+ working_dir,
395
+ cfg,
396
+ _progress_cb,
397
+ contour_maps_path=contour_path,
398
+ cancel=cancel_event.is_set,
399
+ )
400
+ if cancel_event.is_set():
401
+ raise CancelledError("Operation cancelled.")
402
+ if foreground_path is not None and foreground_path.exists():
403
+ _progress_cb("Scoring node probabilities...")
404
+ apply_annotations_and_score(
405
+ working_dir=working_dir,
406
+ cfg=cfg,
407
+ score_signal_path=foreground_path,
408
+ corrections=None,
409
+ validated_tracks=None,
410
+ tracked_labels=None,
411
+ )
412
+ if cancel_event.is_set():
413
+ raise CancelledError("Operation cancelled.")
414
+ result_holder.append(report)
415
+ except Exception as e:
416
+ exc_holder.append(e)
417
+
418
+ t = threading.Thread(target=_run, daemon=True)
419
+ t.start()
420
+ while t.is_alive() or not msg_queue.empty():
421
+ try:
422
+ yield msg_queue.get_nowait()
423
+ except _queue.Empty:
424
+ t.join(timeout=0.05)
425
+ if exc_holder:
426
+ raise exc_holder[0]
427
+ return (pos_dir, result_holder[0] if result_holder else None)
428
+
429
+ self._db_gen_worker = _worker()
430
+
431
+ def _on_db_gen_done(self, result) -> None:
432
+ self._db_gen_worker = None
433
+ self._db_gen_cancel = None
434
+ self._clear_progress()
435
+ pos_dir, _ = result
436
+ self._status("DB generation complete.")
437
+ self._refresh_files_callback()
438
+ self._refresh_db_browser_callback()
439
+ self._set_running_stage(None)
440
+
441
+ def _on_db_gen_worker_error(self, exc: Exception) -> None:
442
+ self._db_gen_worker = None
443
+ self._db_gen_cancel = None
444
+ self._set_running_stage(None)
445
+ self._clear_progress()
446
+ if isinstance(exc, CancelledError):
447
+ self._status("Cancelled.")
448
+ return
449
+ self._status(f"Error: {exc}")
450
+ logger.exception("DB generation worker error", exc_info=exc)
451
+
452
+ # ── Pipeline handlers — Ultrack tracking ─────────────────────────────────
453
+
454
+ def _on_run_ultrack(self) -> None:
455
+ pos_dir = self._pos_dir
456
+ if pos_dir is None:
457
+ self._status("No project open."); return
458
+ db_path = self._ultrack_db_path()
459
+ if db_path is None or not db_path.exists():
460
+ self._status("data.db not found — run DB Generation first."); return
461
+ working_dir = self._ultrack_workdir()
462
+ tracked_path = self._tracked_path()
463
+
464
+ cfg = self._ultrack_config_from_controls()
465
+ corrections = read_corrections(pos_dir)
466
+ validated_tracks = read_validated_tracks(pos_dir) or None
467
+ tracked_labels = None
468
+ if corrections or validated_tracks:
469
+ tracked_labels = self._ensure_tracked_layer_data()
470
+ if tracked_labels is None:
471
+ self._status(
472
+ "Validated-aware export requires tracked_labels.tif "
473
+ "(layer not loaded and file not on disk)."
474
+ ); return
475
+ if corrections and validated_tracks and tracked_labels is not None:
476
+ existing = {
477
+ (int(c.cell_id), int(c.t))
478
+ for c in corrections
479
+ if getattr(c, "kind", None) == "validated"
480
+ }
481
+ corrections = list(corrections) + [
482
+ c for c in corrections_from_validated_tracks(validated_tracks, tracked_labels)
483
+ if (int(c.cell_id), int(c.t)) not in existing
484
+ ]
485
+ validated_tracks = None
486
+
487
+ self.pipeline_progress_bar.setRange(0, 100)
488
+ self.pipeline_progress_bar.setVisible(True)
489
+ self.pipeline_progress_bar.setValue(0)
490
+ self._status("Starting Ultrack solve…")
491
+ self._set_running_stage("ultrack")
492
+
493
+ @thread_worker(connect={
494
+ "yielded": self._on_ultrack_progress,
495
+ "returned": self._on_run_ultrack_done,
496
+ "errored": self._on_ultrack_worker_error,
497
+ })
498
+ def _worker():
499
+ for step, total, label in run_solve(working_dir, cfg, overwrite=True):
500
+ yield (step, total, f"[solve] {label}")
501
+ yield "Exporting tracked labels…"
502
+ labels = export_tracked_labels(
503
+ working_dir, cfg, tracked_path,
504
+ corrections=corrections,
505
+ validated_tracks=validated_tracks,
506
+ tracked_labels=tracked_labels,
507
+ )
508
+ yield "Ordering track IDs by quality…"
509
+ try:
510
+ scores = track_quality_scores(db_path, cfg)
511
+ except Exception:
512
+ logger.exception("Quality scoring failed; leaving track IDs unordered.")
513
+ scores = {}
514
+ if scores:
515
+ relabeled, old_to_new = reorder_stack_by_quality(labels, scores, pos_dir)
516
+ if old_to_new:
517
+ labels = relabeled
518
+ imwrite_grayscale(tracked_path, labels, compression="zlib")
519
+ return labels
520
+
521
+ self._ultrack_worker = _worker()
522
+
523
+ def _on_ultrack_progress(self, data) -> None:
524
+ if self._running_stage is None:
525
+ return
526
+ if isinstance(data, tuple):
527
+ step, total, msg = data
528
+ self._status(msg)
529
+ if total > 0:
530
+ self.pipeline_progress_bar.setRange(0, total)
531
+ self.pipeline_progress_bar.setValue(step)
532
+ else:
533
+ self._status(str(data))
534
+
535
+ def _on_run_ultrack_done(self, labels: np.ndarray | None) -> None:
536
+ self._ultrack_worker = None
537
+ self._clear_progress()
538
+ if labels is None:
539
+ self._set_running_stage(None)
540
+ self._status("Ultrack tracking failed (no output).")
541
+ return
542
+ if labels.ndim == 4 and labels.shape[1] == 1:
543
+ labels = labels[:, 0]
544
+ nt = labels.shape[0]
545
+ self._update_tracked_display(labels)
546
+ self._refresh_files_callback()
547
+ self._status(f"Tracking done: {nt} frame(s).")
548
+ self._set_running_stage(None)
549
+
550
+ def _on_ultrack_worker_error(self, exc: Exception) -> None:
551
+ self._ultrack_worker = None
552
+ self._set_running_stage(None)
553
+ self._clear_progress()
554
+ self._status(f"Error: {exc}")
555
+ logger.exception("Ultrack worker error", exc_info=exc)
556
+
557
+ # ── Cancel ────────────────────────────────────────────────────────────────
558
+
559
+ def _on_cancel(self) -> None:
560
+ cancelled = False
561
+ db_worker = self._db_gen_worker
562
+ if db_worker is not None:
563
+ if self._db_gen_cancel is not None:
564
+ self._db_gen_cancel.set()
565
+ self._status(
566
+ "Cancelling DB generation after the current frame..."
567
+ )
568
+ return
569
+ db_worker.quit()
570
+ self._db_gen_worker = None
571
+ cancelled = True
572
+ ultrack_worker = self._ultrack_worker
573
+ if ultrack_worker is not None:
574
+ ultrack_worker.quit()
575
+ self._ultrack_worker = None
576
+ cancelled = True
577
+ self._set_running_stage(None)
578
+ self._clear_progress()
579
+ self._status("Cancelled." if cancelled else "Nothing running.")
@@ -0,0 +1,21 @@
1
+ """Segmentation input parameter section for the nucleus workflow widget."""
2
+ from __future__ import annotations
3
+
4
+ from qtpy.QtWidgets import QWidget
5
+
6
+ from itasc.napari.widgets import CollapsibleSection
7
+
8
+
9
+ class NucleusSegmentationInputsWidget(QWidget):
10
+ """Compatibility shell for the removed standalone Ultrack Inputs stage."""
11
+
12
+ def __init__(self, parent: QWidget | None = None) -> None:
13
+ super().__init__(parent)
14
+
15
+ inner = QWidget(self)
16
+
17
+ self.section = CollapsibleSection(
18
+ "Ultrack Input Parameters",
19
+ inner,
20
+ expanded=True,
21
+ )