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.
- itasc/__init__.py +16 -0
- itasc/cellpose/__init__.py +31 -0
- itasc/cellpose/cellpose_runner.py +334 -0
- itasc/cellpose/divergence_maps.py +380 -0
- itasc/cellpose/flow_following.py +283 -0
- itasc/cellpose/joint.py +98 -0
- itasc/cellpose/napari.yaml +10 -0
- itasc/cellpose/native_masks.py +313 -0
- itasc/cellpose/retrack.py +232 -0
- itasc/cellpose/shape.py +51 -0
- itasc/cellpose/track_laptrack.py +279 -0
- itasc/contact_analysis/__init__.py +61 -0
- itasc/contact_analysis/_provenance.py +29 -0
- itasc/contact_analysis/catalog.py +393 -0
- itasc/contact_analysis/config.py +155 -0
- itasc/contact_analysis/contacts/__init__.py +24 -0
- itasc/contact_analysis/contacts/batch.py +158 -0
- itasc/contact_analysis/contacts/build.py +837 -0
- itasc/contact_analysis/contacts/contact_labels.py +136 -0
- itasc/contact_analysis/contacts/neighborhood.py +146 -0
- itasc/contact_analysis/contacts/reader.py +58 -0
- itasc/contact_analysis/contacts/signed_contact_length.py +143 -0
- itasc/contact_analysis/curation.py +185 -0
- itasc/contact_analysis/dynamics/__init__.py +43 -0
- itasc/contact_analysis/dynamics/collective.py +206 -0
- itasc/contact_analysis/dynamics/kinematics.py +278 -0
- itasc/contact_analysis/dynamics/msd.py +165 -0
- itasc/contact_analysis/dynamics/store.py +251 -0
- itasc/contact_analysis/dynamics/trajectories.py +100 -0
- itasc/contact_analysis/frame_interval.py +85 -0
- itasc/contact_analysis/napari.yaml +10 -0
- itasc/contact_analysis/pipeline.py +313 -0
- itasc/contact_analysis/pixel_size.py +166 -0
- itasc/contact_analysis/quantifier.py +204 -0
- itasc/contact_analysis/quantifiers/__init__.py +7 -0
- itasc/contact_analysis/quantifiers/_contacts_derived.py +42 -0
- itasc/contact_analysis/quantifiers/cell_density.py +65 -0
- itasc/contact_analysis/quantifiers/cell_dynamics.py +81 -0
- itasc/contact_analysis/quantifiers/cell_shape.py +32 -0
- itasc/contact_analysis/quantifiers/contacts.py +72 -0
- itasc/contact_analysis/quantifiers/neighbor_count.py +26 -0
- itasc/contact_analysis/quantifiers/nucleus_dynamics.py +77 -0
- itasc/contact_analysis/quantifiers/nucleus_shape.py +35 -0
- itasc/contact_analysis/quantifiers/shape_relational.py +34 -0
- itasc/contact_analysis/quantifiers/signed_contact_length.py +44 -0
- itasc/contact_analysis/records.py +129 -0
- itasc/contact_analysis/shape/__init__.py +19 -0
- itasc/contact_analysis/shape/core.py +141 -0
- itasc/contact_analysis/shape/relational.py +134 -0
- itasc/contact_analysis/shape_tables.py +387 -0
- itasc/core/__init__.py +0 -0
- itasc/core/cancellation.py +15 -0
- itasc/core/commit.py +61 -0
- itasc/core/imageops.py +34 -0
- itasc/core/label_store.py +53 -0
- itasc/core/lineage.py +113 -0
- itasc/core/logging.py +51 -0
- itasc/core/paths.py +34 -0
- itasc/core/tiff.py +13 -0
- itasc/correction/__init__.py +25 -0
- itasc/correction/labels.py +983 -0
- itasc/napari/__init__.py +7 -0
- itasc/napari/_experiments_panel.py +1091 -0
- itasc/napari/_flow_layout.py +98 -0
- itasc/napari/_icons.py +126 -0
- itasc/napari/_napari_compat.py +118 -0
- itasc/napari/_paths.py +231 -0
- itasc/napari/_preview_cache.py +60 -0
- itasc/napari/_spotlight.py +46 -0
- itasc/napari/_stage_loader.py +146 -0
- itasc/napari/_stage_status.py +110 -0
- itasc/napari/_standalone_paths.py +89 -0
- itasc/napari/_state.py +146 -0
- itasc/napari/_status_rail.py +128 -0
- itasc/napari/_track_render.py +114 -0
- itasc/napari/_widget_helpers.py +236 -0
- itasc/napari/aggregate_widget.py +339 -0
- itasc/napari/candidate_gallery_controller.py +285 -0
- itasc/napari/cell_workflow_widget.py +1405 -0
- itasc/napari/cellpose_segment_track_widget.py +1253 -0
- itasc/napari/cellpose_widget.py +1034 -0
- itasc/napari/contact_analysis/__init__.py +6 -0
- itasc/napari/contact_analysis/curation_controller.py +94 -0
- itasc/napari/contact_analysis/plugins/__init__.py +102 -0
- itasc/napari/contact_analysis/plugins/catalog_summary.py +56 -0
- itasc/napari/contact_analysis/plugins/curation.py +247 -0
- itasc/napari/contact_analysis/plugins/visualize_contacts.py +64 -0
- itasc/napari/contact_analysis_widget.py +951 -0
- itasc/napari/contact_visualization.py +895 -0
- itasc/napari/correction/__init__.py +8 -0
- itasc/napari/correction/_correction_anchor.py +54 -0
- itasc/napari/correction/_correction_candidate_panel.py +259 -0
- itasc/napari/correction/_correction_candidates.py +169 -0
- itasc/napari/correction/_correction_centroids.py +132 -0
- itasc/napari/correction/_correction_commit.py +71 -0
- itasc/napari/correction/_correction_events.py +39 -0
- itasc/napari/correction/_correction_keymap.py +80 -0
- itasc/napari/correction/_correction_layer_lifecycle.py +137 -0
- itasc/napari/correction/_correction_layer_loader.py +86 -0
- itasc/napari/correction/_correction_navigation.py +97 -0
- itasc/napari/correction/_correction_paint.py +46 -0
- itasc/napari/correction/_correction_playback.py +68 -0
- itasc/napari/correction/_correction_protection.py +44 -0
- itasc/napari/correction/_correction_takeover.py +64 -0
- itasc/napari/correction/_correction_track_accordion.py +566 -0
- itasc/napari/correction/_correction_track_path.py +388 -0
- itasc/napari/correction/_correction_ui.py +367 -0
- itasc/napari/correction/_correction_ui_nucleus.py +393 -0
- itasc/napari/correction/_correction_utils.py +204 -0
- itasc/napari/correction/_correction_validation.py +83 -0
- itasc/napari/correction/cell_correction_widget.py +1190 -0
- itasc/napari/correction/correction_widget.py +1363 -0
- itasc/napari/correction/nucleus_correction_widget.py +2185 -0
- itasc/napari/divergence_maps_widget.py +795 -0
- itasc/napari/lineage_canvas_controller.py +351 -0
- itasc/napari/main_widget.py +1184 -0
- itasc/napari/nucleus_atom_extraction_widget.py +648 -0
- itasc/napari/nucleus_db_browser_widget.py +1442 -0
- itasc/napari/nucleus_pipeline_widget.py +579 -0
- itasc/napari/nucleus_segmentation_inputs_widget.py +21 -0
- itasc/napari/nucleus_tracking_inputs_widget.py +248 -0
- itasc/napari/nucleus_workflow_widget.py +963 -0
- itasc/napari/track_path_controller.py +387 -0
- itasc/napari/ui_gate.py +266 -0
- itasc/napari/ui_style.py +503 -0
- itasc/napari/validated_overlay_controller.py +254 -0
- itasc/napari/widgets.py +616 -0
- itasc/napari.yaml +45 -0
- itasc/segmentation/__init__.py +60 -0
- itasc/segmentation/cell_divergence_segmentation.py +433 -0
- itasc/segmentation/cell_label_icm.py +637 -0
- itasc/segmentation/contour_filtering.py +128 -0
- itasc/segmentation/lineage.py +18 -0
- itasc/segmentation/nucleus_segmentation.py +35 -0
- itasc/tracking_ultrack/__init__.py +12 -0
- itasc/tracking_ultrack/_node_geometry.py +191 -0
- itasc/tracking_ultrack/atoms.py +470 -0
- itasc/tracking_ultrack/config.py +76 -0
- itasc/tracking_ultrack/corrections.py +1213 -0
- itasc/tracking_ultrack/db_build.py +492 -0
- itasc/tracking_ultrack/db_query.py +952 -0
- itasc/tracking_ultrack/export.py +133 -0
- itasc/tracking_ultrack/extend.py +243 -0
- itasc/tracking_ultrack/ingest.py +83 -0
- itasc/tracking_ultrack/linking.py +286 -0
- itasc/tracking_ultrack/multi_threshold.py +46 -0
- itasc/tracking_ultrack/napari.yaml +10 -0
- itasc/tracking_ultrack/reseed.py +456 -0
- itasc/tracking_ultrack/retracker.py +153 -0
- itasc/tracking_ultrack/scoring.py +74 -0
- itasc/tracking_ultrack/seed_prior.py +175 -0
- itasc/tracking_ultrack/solve.py +52 -0
- itasc/tracking_ultrack/swap_candidate.py +171 -0
- itasc/tracking_ultrack/track_quality.py +142 -0
- itasc/tracking_ultrack/validation_nodes.py +315 -0
- itasc/tracking_ultrack/validation_state.py +430 -0
- itasc-0.2.0.dev0.data/data/itasc/napari.yaml +45 -0
- itasc-0.2.0.dev0.dist-info/METADATA +186 -0
- itasc-0.2.0.dev0.dist-info/RECORD +162 -0
- itasc-0.2.0.dev0.dist-info/WHEEL +4 -0
- itasc-0.2.0.dev0.dist-info/entry_points.txt +2 -0
- 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
|
+
)
|