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,393 @@
|
|
|
1
|
+
"""Qt construction for :class:`NucleusCorrectionWidget`.
|
|
2
|
+
|
|
3
|
+
The widget is large enough that its one-time control assembly — buttons,
|
|
4
|
+
parameter sliders, the embedded :class:`CorrectionWidget`, the reveal sections,
|
|
5
|
+
the toolbar and the signal wiring — is kept here so the widget module is about
|
|
6
|
+
behaviour rather than scaffolding. :func:`build_nucleus_correction_ui` populates
|
|
7
|
+
the passed widget in place (setting the same ``w.<name>`` attributes the rest of
|
|
8
|
+
the class reads), mirroring how the shared header / toolbar builders in
|
|
9
|
+
:mod:`itasc.napari.correction._correction_ui` already work.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from qtpy.QtCore import QTimer
|
|
15
|
+
from qtpy.QtWidgets import (
|
|
16
|
+
QCheckBox,
|
|
17
|
+
QHBoxLayout,
|
|
18
|
+
QLabel,
|
|
19
|
+
QVBoxLayout,
|
|
20
|
+
QWidget,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from itasc.napari._widget_helpers import (
|
|
24
|
+
btn as _btn,
|
|
25
|
+
dslider as _dslider,
|
|
26
|
+
heading as _heading,
|
|
27
|
+
make_status as _make_status,
|
|
28
|
+
tool_btn as _tool_btn,
|
|
29
|
+
)
|
|
30
|
+
from itasc.napari.correction._correction_ui import (
|
|
31
|
+
build_correction_header,
|
|
32
|
+
build_correction_toolbar,
|
|
33
|
+
build_shortcuts_widget,
|
|
34
|
+
flatten_embedded_section,
|
|
35
|
+
)
|
|
36
|
+
from itasc.napari.ui_style import danger_button
|
|
37
|
+
from itasc.napari.widgets import CollapsibleSection
|
|
38
|
+
from itasc.napari.correction.correction_widget import CorrectionWidget
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def build_nucleus_correction_ui(w) -> None:
|
|
42
|
+
"""Build and wire all of ``w``'s correction controls (run once from init)."""
|
|
43
|
+
root = QVBoxLayout(w)
|
|
44
|
+
root.setContentsMargins(0, 0, 0, 0)
|
|
45
|
+
root.setSpacing(0)
|
|
46
|
+
|
|
47
|
+
_init_candidate_refresh_timer(w)
|
|
48
|
+
|
|
49
|
+
inner = QWidget(w)
|
|
50
|
+
group_lay = QVBoxLayout(inner)
|
|
51
|
+
group_lay.setContentsMargins(0, 0, 0, 0)
|
|
52
|
+
group_lay.setSpacing(6)
|
|
53
|
+
|
|
54
|
+
_build_action_buttons(w)
|
|
55
|
+
_build_status_labels(w)
|
|
56
|
+
_build_correction_subwidget(w)
|
|
57
|
+
_build_extend_retrack_section(w)
|
|
58
|
+
_build_shortcuts_section(w)
|
|
59
|
+
_build_toolbar(w)
|
|
60
|
+
_build_view_toggle_buttons(w)
|
|
61
|
+
_assemble_reveal_area(w, group_lay)
|
|
62
|
+
|
|
63
|
+
# The full-width top bar carries the title, the activate / shortcuts /
|
|
64
|
+
# params toggles, the checkable view toggles, and a single one-line
|
|
65
|
+
# status (the save/action status only) right-aligned. The track /
|
|
66
|
+
# validated summary lives in the tracking-overview panel instead.
|
|
67
|
+
w.header, w.header_lbl = build_correction_header(
|
|
68
|
+
w,
|
|
69
|
+
shortcuts_btn=w.shortcuts_btn,
|
|
70
|
+
params_btn=w.params_btn,
|
|
71
|
+
active_btn=w.active_btn,
|
|
72
|
+
finalize_btn=getattr(w, "_finalize_header_btn", None),
|
|
73
|
+
view_toggle_btns=(
|
|
74
|
+
w.track_path_btn,
|
|
75
|
+
w.spotlight_btn,
|
|
76
|
+
w.filled_view_btn,
|
|
77
|
+
),
|
|
78
|
+
status_lbl=w.status_lbl,
|
|
79
|
+
)
|
|
80
|
+
# ``section`` is the full-width reveal area (params + shortcuts) shown
|
|
81
|
+
# below the top bar; both it and the header are reparented between the
|
|
82
|
+
# plugin dock (inactive) and the workspace dock (active).
|
|
83
|
+
w.section = CollapsibleSection("Correction", inner, expanded=False)
|
|
84
|
+
w.section._toggle.setVisible(False)
|
|
85
|
+
w.section._toggle.setEnabled(False)
|
|
86
|
+
|
|
87
|
+
w.correction_active_btn = w.active_btn
|
|
88
|
+
w.correction_shortcuts_btn = w.shortcuts_btn
|
|
89
|
+
w.correction_status_lbl = w.status_lbl
|
|
90
|
+
w.correction_mode_section = w.section
|
|
91
|
+
w._correction_active_content_visible = False
|
|
92
|
+
_connect_signals(w)
|
|
93
|
+
# Start collapsed: the inactive plugin-dock entry point shows only the
|
|
94
|
+
# on/off button (title + toggles appear once correction is active).
|
|
95
|
+
w._sync_correction_panel_visibility()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _init_candidate_refresh_timer(w) -> None:
|
|
99
|
+
# Searching + rendering the candidate gallery is expensive, so debounce
|
|
100
|
+
# it: every refresh request (re)starts this timer and the actual rebuild
|
|
101
|
+
# only fires once the frame has been still for the interval — fast
|
|
102
|
+
# scrubbing no longer recomputes candidates for every frame it sweeps.
|
|
103
|
+
w._candidate_refresh_timer = QTimer(w)
|
|
104
|
+
w._candidate_refresh_timer.setSingleShot(True)
|
|
105
|
+
w._candidate_refresh_timer.setInterval(200)
|
|
106
|
+
w._candidate_refresh_timer.timeout.connect(w._refresh_candidate_gallery_now)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _build_action_buttons(w) -> None:
|
|
110
|
+
w.active_btn = _tool_btn(
|
|
111
|
+
"⏻",
|
|
112
|
+
"Activate correction mode and show correction layers and controls.",
|
|
113
|
+
checkable=True,
|
|
114
|
+
)
|
|
115
|
+
w.active_btn.setToolTip(
|
|
116
|
+
"Activate correction mode and show correction layers and controls."
|
|
117
|
+
)
|
|
118
|
+
w.params_btn = _tool_btn(
|
|
119
|
+
"⚙", "Show correction parameters.", checkable=True
|
|
120
|
+
)
|
|
121
|
+
w.shortcuts_btn = _tool_btn(
|
|
122
|
+
"📖", "Show correction shortcuts.", checkable=True
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
w.save_tracked_btn = _tool_btn(
|
|
126
|
+
"💾", "Save corrected tracked nucleus labels to disk (S)."
|
|
127
|
+
)
|
|
128
|
+
w.extend_back_btn = _tool_btn(
|
|
129
|
+
"◀", "Extend selected track one frame backward (A)."
|
|
130
|
+
)
|
|
131
|
+
w.extend_fwd_btn = _tool_btn(
|
|
132
|
+
"▶", "Extend selected track one frame forward (D)."
|
|
133
|
+
)
|
|
134
|
+
w.retrack_back_btn = _tool_btn(
|
|
135
|
+
"↶", "Retrack all labels backward from current frame (Q)."
|
|
136
|
+
)
|
|
137
|
+
w.retrack_fwd_btn = _tool_btn(
|
|
138
|
+
"↷", "Retrack all labels forward from current frame (E)."
|
|
139
|
+
)
|
|
140
|
+
w.swap_smaller_btn = _tool_btn(
|
|
141
|
+
"⮂", "Swap selected cell with the next smaller candidate fragment (Z)."
|
|
142
|
+
)
|
|
143
|
+
w.swap_larger_btn = _tool_btn(
|
|
144
|
+
"⮀", "Swap selected cell with the next larger candidate fragment (C)."
|
|
145
|
+
)
|
|
146
|
+
w.reassign_ids_btn = _tool_btn(
|
|
147
|
+
"#",
|
|
148
|
+
"Reassign cell IDs to contiguous range 1-N (validated tracks first, "
|
|
149
|
+
"then by earliest start frame, then by longest track).",
|
|
150
|
+
)
|
|
151
|
+
w.validate_track_btn = _tool_btn(
|
|
152
|
+
"✓", "Lock selected cell geometry in every frame where it appears (V)."
|
|
153
|
+
)
|
|
154
|
+
w.anchor_here_btn = _tool_btn(
|
|
155
|
+
"⚓", "Anchor selected cell identity at the current frame (B)."
|
|
156
|
+
)
|
|
157
|
+
w.annotate_db_btn = _tool_btn(
|
|
158
|
+
"✎", "Apply saved validations and anchors to the Ultrack database."
|
|
159
|
+
)
|
|
160
|
+
w.remove_unvalidated_btn = _tool_btn(
|
|
161
|
+
"🗑",
|
|
162
|
+
"Remove nucleus label pixels not marked validated for their frame.",
|
|
163
|
+
)
|
|
164
|
+
danger_button(w.remove_unvalidated_btn)
|
|
165
|
+
|
|
166
|
+
w.commit_btn = _btn(
|
|
167
|
+
"Commit",
|
|
168
|
+
"Reassign cell IDs, remove unvalidated labels, and save tracked labels.",
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _build_status_labels(w) -> None:
|
|
173
|
+
w.status_lbl = _make_status()
|
|
174
|
+
# Drop the smaller status font so the status line reads at the same
|
|
175
|
+
# size as the rest of the controls column. Keep it to a single line so
|
|
176
|
+
# it never wraps and pushes the top-bar title pill onto two rows.
|
|
177
|
+
w.status_lbl.setStyleSheet("")
|
|
178
|
+
w.status_lbl.setWordWrap(False)
|
|
179
|
+
|
|
180
|
+
# The track / validated summary now lives in the tracking-overview title
|
|
181
|
+
# (see LineageCanvasController). This label is kept as a hidden sink so
|
|
182
|
+
# the existing counter-refresh plumbing keeps working without surfacing
|
|
183
|
+
# a second status line in the top bar; it is parented (never shown) so
|
|
184
|
+
# toggling its visibility can't spawn a stray top-level window.
|
|
185
|
+
w.validation_counter_lbl = QLabel("", w)
|
|
186
|
+
w.validation_counter_lbl.setVisible(False)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _build_correction_subwidget(w) -> None:
|
|
190
|
+
w.correction_widget = CorrectionWidget(
|
|
191
|
+
w.viewer,
|
|
192
|
+
show_activate_btn=False,
|
|
193
|
+
show_shortcuts=False,
|
|
194
|
+
inspector_first=True,
|
|
195
|
+
show_cleanup=False,
|
|
196
|
+
# The lineage canvas is the navigation surface now — drop the
|
|
197
|
+
# redundant "Inspect cell" group from the correction column.
|
|
198
|
+
show_inspector=False,
|
|
199
|
+
)
|
|
200
|
+
w.correction_widget.set_edit_callback(w._edit_callback)
|
|
201
|
+
w.correction_widget.set_protected_mask_callback(
|
|
202
|
+
w._manual_correction_protected_mask
|
|
203
|
+
)
|
|
204
|
+
w.correction_widget.set_intensity_frame_callback(
|
|
205
|
+
w._correction_intensity_frame
|
|
206
|
+
)
|
|
207
|
+
# Use an additive listener, not set_selection_callback: the workflow
|
|
208
|
+
# widget owns that single slot and would otherwise clobber the comet's
|
|
209
|
+
# rebuild-on-selection (so it would only build on first checkbox tick).
|
|
210
|
+
w.correction_widget.add_selection_listener(
|
|
211
|
+
w._on_track_selection_changed
|
|
212
|
+
)
|
|
213
|
+
w.correction_widget.set_spotlight_mask_provider(
|
|
214
|
+
w._track_path_spotlight_mask
|
|
215
|
+
)
|
|
216
|
+
w.correction_widget._status.setVisible(False)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _build_extend_retrack_section(w) -> None:
|
|
220
|
+
# Wide-and-short reveal area: each remaining parameter gets its own
|
|
221
|
+
# column so the sliders spread horizontally across the full-width panel
|
|
222
|
+
# rather than stacking. Greedy overwrite and the outline view are now
|
|
223
|
+
# always-on defaults, so neither is surfaced as a control here.
|
|
224
|
+
extend_retrack_inner = QWidget(w)
|
|
225
|
+
extend_retrack_lay = QVBoxLayout(extend_retrack_inner)
|
|
226
|
+
extend_retrack_lay.setContentsMargins(0, 0, 0, 0)
|
|
227
|
+
extend_retrack_lay.setSpacing(6)
|
|
228
|
+
|
|
229
|
+
columns = QHBoxLayout()
|
|
230
|
+
columns.setContentsMargins(0, 0, 0, 0)
|
|
231
|
+
columns.setSpacing(16)
|
|
232
|
+
|
|
233
|
+
def _column(*items) -> QWidget:
|
|
234
|
+
col = QWidget()
|
|
235
|
+
lay = QVBoxLayout(col)
|
|
236
|
+
lay.setContentsMargins(0, 0, 0, 0)
|
|
237
|
+
lay.setSpacing(4)
|
|
238
|
+
for item in items:
|
|
239
|
+
if isinstance(item, QWidget):
|
|
240
|
+
lay.addWidget(item)
|
|
241
|
+
else:
|
|
242
|
+
lay.addLayout(item)
|
|
243
|
+
lay.addStretch(1)
|
|
244
|
+
return col
|
|
245
|
+
|
|
246
|
+
# Greedy overwrite is now the always-on default: keep the (hidden)
|
|
247
|
+
# checkbox checked so the extend/retrack paint paths that read its state
|
|
248
|
+
# still behave greedily, but drop it from the UI.
|
|
249
|
+
w.extend_greedy_overwrite_check = QCheckBox("Greedy overwrite")
|
|
250
|
+
w.extend_greedy_overwrite_check.setChecked(True)
|
|
251
|
+
|
|
252
|
+
w.retrack_max_dist_spin = _dslider(0, 500, 20.0, 1.0, 1)
|
|
253
|
+
# Scoring weights for the retrack frame matcher. Kept under the
|
|
254
|
+
# ``extend_*`` attribute names for settings back-compat; extend no longer
|
|
255
|
+
# uses them.
|
|
256
|
+
w.extend_area_weight_spin = _dslider(0, 10, 1.0, 0.1, 2)
|
|
257
|
+
w.extend_iou_weight_spin = _dslider(0, 10, 1.0, 0.1, 2)
|
|
258
|
+
w.extend_distance_weight_spin = _dslider(0, 10, 0.05, 0.01, 3)
|
|
259
|
+
|
|
260
|
+
# Each retrack weight + the cell-radius control gets its own column
|
|
261
|
+
# (stretch=1) so they fan out across the wide reveal area. The cell-radius
|
|
262
|
+
# slider (middle-click cell creation) is reused straight from the embedded
|
|
263
|
+
# correction widget, dropped under a plain "Cell Radius" heading so it
|
|
264
|
+
# matches the other sliders rather than carrying its own inline label.
|
|
265
|
+
for heading_text, widget in (
|
|
266
|
+
("Max distance", w.retrack_max_dist_spin),
|
|
267
|
+
("Area weight", w.extend_area_weight_spin),
|
|
268
|
+
("IoU weight", w.extend_iou_weight_spin),
|
|
269
|
+
("Distance weight", w.extend_distance_weight_spin),
|
|
270
|
+
("Cell Radius", w.correction_widget._cell_radius_spin),
|
|
271
|
+
):
|
|
272
|
+
columns.addWidget(_column(_heading(heading_text), widget), stretch=1)
|
|
273
|
+
extend_retrack_lay.addLayout(columns)
|
|
274
|
+
|
|
275
|
+
w.extend_retrack_params_section = CollapsibleSection(
|
|
276
|
+
"Extend / Retrack Parameters",
|
|
277
|
+
extend_retrack_inner,
|
|
278
|
+
expanded=False,
|
|
279
|
+
)
|
|
280
|
+
flatten_embedded_section(w.extend_retrack_params_section)
|
|
281
|
+
w.extend_retrack_params_section.setVisible(False)
|
|
282
|
+
w.extend_params_section = w.extend_retrack_params_section
|
|
283
|
+
w.retrack_params_section = w.extend_retrack_params_section
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _build_shortcuts_section(w) -> None:
|
|
287
|
+
# The disclaimer / attribution label rides at the bottom of the wide
|
|
288
|
+
# shortcuts panel now (reparented out of the embedded correction widget).
|
|
289
|
+
w.shortcuts_section = CollapsibleSection(
|
|
290
|
+
"Correction Shortcuts",
|
|
291
|
+
build_shortcuts_widget(w.correction_widget._attrib_lbl),
|
|
292
|
+
expanded=False,
|
|
293
|
+
)
|
|
294
|
+
flatten_embedded_section(w.shortcuts_section)
|
|
295
|
+
w.shortcuts_section.setVisible(False)
|
|
296
|
+
w.correction_widget.setVisible(False)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _build_toolbar(w) -> None:
|
|
300
|
+
# Extend / swap are driven by clicking into the candidate gallery now,
|
|
301
|
+
# so they're dropped from the toolbar (the A/D/Z/C shortcuts still work).
|
|
302
|
+
groups = [
|
|
303
|
+
(w.save_tracked_btn,),
|
|
304
|
+
(w.retrack_back_btn, w.retrack_fwd_btn),
|
|
305
|
+
(w.validate_track_btn, w.anchor_here_btn),
|
|
306
|
+
(w.annotate_db_btn,),
|
|
307
|
+
(w.reassign_ids_btn, w.remove_unvalidated_btn),
|
|
308
|
+
]
|
|
309
|
+
# The host workflow widget's Finalize button rides at the tail of the
|
|
310
|
+
# toolbar (its own ruled group) — finalizing is the natural last step
|
|
311
|
+
# once the tracks are corrected.
|
|
312
|
+
if getattr(w, "_finalize_btn", None) is not None:
|
|
313
|
+
groups.append((w._finalize_btn,))
|
|
314
|
+
w.toolbar = build_correction_toolbar(w, groups)
|
|
315
|
+
w.toolbar.setVisible(False)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _build_view_toggle_buttons(w) -> None:
|
|
319
|
+
# View toggles ride in the top bar as checkable icon tool-buttons; the
|
|
320
|
+
# old "Lineage canvas" toggle is gone — the accordion is the always-on
|
|
321
|
+
# main surface. Tooltips carry the longer descriptions the old check
|
|
322
|
+
# captions spelled out.
|
|
323
|
+
w.track_path_btn = _tool_btn(
|
|
324
|
+
"👁",
|
|
325
|
+
"Track path: paint the selected track's whole trajectory as a fading "
|
|
326
|
+
"comet (viridis, oldest→newest) with a frame number in each mask.",
|
|
327
|
+
checkable=True,
|
|
328
|
+
)
|
|
329
|
+
w.spotlight_btn = _tool_btn(
|
|
330
|
+
"🔦",
|
|
331
|
+
"Spotlight: dim everything outside the selected cell. Off = mark the "
|
|
332
|
+
"selection with a plain yellow border instead (the rest of the frame "
|
|
333
|
+
"stays at full brightness).",
|
|
334
|
+
checkable=True,
|
|
335
|
+
)
|
|
336
|
+
# Spotlight is the default selection indicator, so the toggle starts on.
|
|
337
|
+
w.spotlight_btn.setChecked(True)
|
|
338
|
+
w.filled_view_btn = _tool_btn(
|
|
339
|
+
"🎨",
|
|
340
|
+
"Filled labels (by ID): hide the cell + nucleus images and draw the "
|
|
341
|
+
"labels and tracks opaque and filled (not outlines), coloured by ID. "
|
|
342
|
+
"Off = the default outline view in one neutral colour.",
|
|
343
|
+
checkable=True,
|
|
344
|
+
)
|
|
345
|
+
# The candidate gallery is shown/hidden by its own ✕ button + slim
|
|
346
|
+
# show-tab (see CollapsiblePane), not a top-bar toggle.
|
|
347
|
+
for button in (w.track_path_btn, w.spotlight_btn, w.filled_view_btn):
|
|
348
|
+
button.setVisible(False)
|
|
349
|
+
w.validation_counter_lbl.setVisible(False)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _assemble_reveal_area(w, group_lay: QVBoxLayout) -> None:
|
|
353
|
+
# The section body is the full-width reveal area below the top bar: the
|
|
354
|
+
# params and shortcuts panels, openable independently. The toolbar, the
|
|
355
|
+
# view toggles and the status now live elsewhere (the toolbar in the body
|
|
356
|
+
# splitter's thin left column, the toggles + status in the top bar). The
|
|
357
|
+
# embedded correction widget is a logic holder only — its visible bits
|
|
358
|
+
# (outline + spawn controls, attribution label) were reparented into the
|
|
359
|
+
# params / shortcuts panels — so it is kept hidden here to stay owned.
|
|
360
|
+
group_lay.addWidget(w.extend_retrack_params_section)
|
|
361
|
+
group_lay.addWidget(w.shortcuts_section)
|
|
362
|
+
w.correction_widget.setVisible(False)
|
|
363
|
+
group_lay.addWidget(w.correction_widget)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _connect_signals(w) -> None:
|
|
367
|
+
w.save_tracked_btn.clicked.connect(w._on_save_tracked)
|
|
368
|
+
w.reassign_ids_btn.clicked.connect(w._on_reassign_ids)
|
|
369
|
+
w.validate_track_btn.clicked.connect(w._on_validate_track)
|
|
370
|
+
w.anchor_here_btn.clicked.connect(w._on_anchor_here)
|
|
371
|
+
w.annotate_db_btn.clicked.connect(w._on_annotate_database)
|
|
372
|
+
w.extend_back_btn.clicked.connect(w._on_extend_backward)
|
|
373
|
+
w.extend_fwd_btn.clicked.connect(w._on_extend_forward)
|
|
374
|
+
w.retrack_back_btn.clicked.connect(w._on_retrack_backward)
|
|
375
|
+
w.retrack_fwd_btn.clicked.connect(w._on_retrack_forward)
|
|
376
|
+
w.swap_smaller_btn.clicked.connect(
|
|
377
|
+
lambda: w._on_swap_step(direction="smaller")
|
|
378
|
+
)
|
|
379
|
+
w.swap_larger_btn.clicked.connect(
|
|
380
|
+
lambda: w._on_swap_step(direction="larger")
|
|
381
|
+
)
|
|
382
|
+
w.remove_unvalidated_btn.clicked.connect(w._on_remove_unvalidated_labels)
|
|
383
|
+
w.commit_btn.clicked.connect(w._on_commit)
|
|
384
|
+
w.track_path_btn.toggled.connect(w._on_toggle_track_path)
|
|
385
|
+
w.spotlight_btn.toggled.connect(w._on_toggle_spotlight)
|
|
386
|
+
w.filled_view_btn.toggled.connect(w._on_toggle_filled_view)
|
|
387
|
+
w.params_btn.toggled.connect(w._on_correction_params_button_toggled)
|
|
388
|
+
w.shortcuts_btn.toggled.connect(w._on_correction_shortcuts_button_toggled)
|
|
389
|
+
w.active_btn.toggled.connect(w._on_correction_active_button_toggled)
|
|
390
|
+
w.correction_widget._activate_btn.toggled.connect(
|
|
391
|
+
w._on_correction_mode_toggled
|
|
392
|
+
)
|
|
393
|
+
w._install_correction_shortcuts()
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Iterable, Mapping
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class RetrackDirectionResult:
|
|
13
|
+
stack: np.ndarray
|
|
14
|
+
n_retracked: int
|
|
15
|
+
n_skipped: int
|
|
16
|
+
first_target_frame: int
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def frame_view_2d(arr: np.ndarray, t: int) -> np.ndarray | None:
|
|
20
|
+
"""Return frame *t* as a 2D view when the stack shape is unambiguous."""
|
|
21
|
+
if arr.ndim < 3 or t < 0 or t >= arr.shape[0]:
|
|
22
|
+
return None
|
|
23
|
+
view = arr[t]
|
|
24
|
+
while view.ndim > 2:
|
|
25
|
+
if view.shape[0] != 1:
|
|
26
|
+
return None
|
|
27
|
+
view = view[0]
|
|
28
|
+
return view
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def reassign_ids_ordered(
|
|
32
|
+
stack: np.ndarray, order: list[int]
|
|
33
|
+
) -> tuple[np.ndarray, int, dict[int, int]]:
|
|
34
|
+
"""Relabel non-zero IDs to contiguous IDs from 1, following *order*.
|
|
35
|
+
|
|
36
|
+
Old IDs listed in *order* are assigned new IDs first (best → 1, next → 2,
|
|
37
|
+
…); any present IDs not in *order* follow in ascending order. Passing an
|
|
38
|
+
empty *order* reproduces plain numeric compaction.
|
|
39
|
+
"""
|
|
40
|
+
unique_ids = np.unique(stack)
|
|
41
|
+
unique_ids = unique_ids[unique_ids != 0]
|
|
42
|
+
if unique_ids.size == 0:
|
|
43
|
+
return stack, 0, {}
|
|
44
|
+
present = {int(x) for x in unique_ids}
|
|
45
|
+
seen: set[int] = set()
|
|
46
|
+
ordered_ids: list[int] = []
|
|
47
|
+
for old_id in order:
|
|
48
|
+
old_id = int(old_id)
|
|
49
|
+
if old_id in present and old_id not in seen:
|
|
50
|
+
ordered_ids.append(old_id)
|
|
51
|
+
seen.add(old_id)
|
|
52
|
+
ordered_ids.extend(sorted(present - seen))
|
|
53
|
+
lut = np.zeros(int(unique_ids.max()) + 1, dtype=np.uint32)
|
|
54
|
+
old_to_new: dict[int, int] = {}
|
|
55
|
+
for new_id, old_id in enumerate(ordered_ids, start=1):
|
|
56
|
+
lut[old_id] = new_id
|
|
57
|
+
old_to_new[old_id] = new_id
|
|
58
|
+
return lut[stack], len(ordered_ids), old_to_new
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def track_order_by_frame_and_size(
|
|
62
|
+
stack: np.ndarray, priority_ids: Iterable[int] = ()
|
|
63
|
+
) -> list[int]:
|
|
64
|
+
"""Order present track IDs for ID reassignment.
|
|
65
|
+
|
|
66
|
+
Tracks listed in *priority_ids* (e.g. validated tracks) form the first
|
|
67
|
+
group and the remaining present tracks the second; the two groups stay
|
|
68
|
+
separate so validated tracks always receive the lower IDs. Within each
|
|
69
|
+
group, IDs are sorted by starting frame (earliest first), then by track
|
|
70
|
+
length in frames (longest first), with the numeric ID as a final tiebreaker
|
|
71
|
+
for determinism.
|
|
72
|
+
"""
|
|
73
|
+
stack = np.asarray(stack)
|
|
74
|
+
frames = stack if stack.ndim >= 3 else stack[None, ...]
|
|
75
|
+
start_frame: dict[int, int] = {}
|
|
76
|
+
length: dict[int, int] = {}
|
|
77
|
+
for t, frame in enumerate(frames):
|
|
78
|
+
for cid in np.unique(frame):
|
|
79
|
+
cid = int(cid)
|
|
80
|
+
if cid == 0:
|
|
81
|
+
continue
|
|
82
|
+
start_frame.setdefault(cid, t)
|
|
83
|
+
length[cid] = length.get(cid, 0) + 1
|
|
84
|
+
|
|
85
|
+
def sort_key(cid: int) -> tuple[int, int, int]:
|
|
86
|
+
return (start_frame[cid], -length[cid], cid)
|
|
87
|
+
|
|
88
|
+
present = set(start_frame)
|
|
89
|
+
priority = {int(cid) for cid in priority_ids} & present
|
|
90
|
+
return sorted(priority, key=sort_key) + sorted(present - priority, key=sort_key)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def reassign_ids_stack(stack: np.ndarray) -> tuple[np.ndarray, int, dict[int, int]]:
|
|
94
|
+
"""Compact non-zero label IDs in a stack to contiguous IDs from 1."""
|
|
95
|
+
return reassign_ids_ordered(stack, [])
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def reorder_stack_by_quality(
|
|
99
|
+
stack: np.ndarray,
|
|
100
|
+
scores: Mapping[int, float],
|
|
101
|
+
pos_dir: str | Path | None = None,
|
|
102
|
+
) -> tuple[np.ndarray, dict[int, int]]:
|
|
103
|
+
"""Relabel a tracked stack so track IDs follow quality order, best → 1.
|
|
104
|
+
|
|
105
|
+
*scores* maps track_id -> quality score (see
|
|
106
|
+
:func:`itasc.tracking_ultrack.track_quality.track_quality_scores`). When
|
|
107
|
+
*pos_dir* is given, existing validations/anchors are remapped onto the new
|
|
108
|
+
IDs so prior work stays attached. Returns ``(relabeled_stack, old_to_new)``;
|
|
109
|
+
a no-op (empty mapping, original array) when there is nothing to reorder.
|
|
110
|
+
"""
|
|
111
|
+
from itasc.tracking_ultrack.track_quality import quality_order
|
|
112
|
+
|
|
113
|
+
stack = np.asarray(stack)
|
|
114
|
+
order = quality_order(scores) if scores else []
|
|
115
|
+
if not order:
|
|
116
|
+
return stack, {}
|
|
117
|
+
relabeled, _n, old_to_new = reassign_ids_ordered(
|
|
118
|
+
stack.astype(np.uint32, copy=False), order
|
|
119
|
+
)
|
|
120
|
+
if old_to_new and pos_dir is not None:
|
|
121
|
+
from itasc.tracking_ultrack.validation_state import remap_validated_tracks
|
|
122
|
+
|
|
123
|
+
remap_validated_tracks(Path(pos_dir), old_to_new)
|
|
124
|
+
return relabeled, old_to_new
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def remove_unvalidated_labels(
|
|
128
|
+
data: np.ndarray,
|
|
129
|
+
validated_tracks: dict[int, set[int]],
|
|
130
|
+
) -> tuple[int, int]:
|
|
131
|
+
"""Remove labels not validated for their frame from a 2D or time-first stack."""
|
|
132
|
+
frame_count = int(data.shape[0]) if data.ndim >= 3 else 1
|
|
133
|
+
changed_pixels = changed_frames = 0
|
|
134
|
+
for t in range(frame_count):
|
|
135
|
+
frame = data[t] if data.ndim >= 3 else data
|
|
136
|
+
if frame.ndim != 2:
|
|
137
|
+
raise ValueError("Tracked layer must be a time-first stack.")
|
|
138
|
+
validated_ids = {
|
|
139
|
+
cid for cid, frames in validated_tracks.items() if t in frames
|
|
140
|
+
}
|
|
141
|
+
remove_mask = frame != 0
|
|
142
|
+
if validated_ids:
|
|
143
|
+
remove_mask &= ~np.isin(frame, list(validated_ids))
|
|
144
|
+
n_remove = int(np.count_nonzero(remove_mask))
|
|
145
|
+
if not n_remove:
|
|
146
|
+
continue
|
|
147
|
+
frame[remove_mask] = 0
|
|
148
|
+
changed_pixels += n_remove
|
|
149
|
+
changed_frames += 1
|
|
150
|
+
return changed_frames, changed_pixels
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def retrack_stack_direction(
|
|
154
|
+
stack: np.ndarray,
|
|
155
|
+
*,
|
|
156
|
+
start_frame: int,
|
|
157
|
+
direction: Literal["forward", "backward"],
|
|
158
|
+
fully_validated_frames: set[int],
|
|
159
|
+
validated_cells_at_frame: Callable[[int], set[int]],
|
|
160
|
+
retrack_frame: Callable[..., np.ndarray],
|
|
161
|
+
max_dist_px: float,
|
|
162
|
+
reserved_ids: set[int],
|
|
163
|
+
area_weight: float = 1.0,
|
|
164
|
+
iou_weight: float = 1.0,
|
|
165
|
+
distance_weight: float = 0.05,
|
|
166
|
+
) -> RetrackDirectionResult:
|
|
167
|
+
"""Retrack a time-first stack in one direction, skipping validated frames."""
|
|
168
|
+
if stack.ndim != 3 or stack.shape[0] < 2:
|
|
169
|
+
raise ValueError("Tracked layer must be a 3D time-first stack.")
|
|
170
|
+
|
|
171
|
+
out = stack.copy()
|
|
172
|
+
n_retracked = n_skipped = 0
|
|
173
|
+
if direction == "forward":
|
|
174
|
+
frame_range = range(start_frame + 1, out.shape[0])
|
|
175
|
+
previous_frame = lambda t: out[t - 1]
|
|
176
|
+
elif direction == "backward":
|
|
177
|
+
frame_range = range(start_frame - 1, -1, -1)
|
|
178
|
+
previous_frame = lambda t: out[t + 1]
|
|
179
|
+
else:
|
|
180
|
+
raise ValueError(f"Unknown retrack direction: {direction!r}")
|
|
181
|
+
|
|
182
|
+
first_target_frame = next(iter(frame_range), start_frame)
|
|
183
|
+
for t in frame_range:
|
|
184
|
+
if t in fully_validated_frames:
|
|
185
|
+
n_skipped += 1
|
|
186
|
+
continue
|
|
187
|
+
out[t] = retrack_frame(
|
|
188
|
+
previous_frame(t),
|
|
189
|
+
out[t],
|
|
190
|
+
validated_cells_at_frame(t),
|
|
191
|
+
max_dist_px=max_dist_px,
|
|
192
|
+
reserved_ids=reserved_ids,
|
|
193
|
+
area_weight=area_weight,
|
|
194
|
+
iou_weight=iou_weight,
|
|
195
|
+
distance_weight=distance_weight,
|
|
196
|
+
)
|
|
197
|
+
n_retracked += 1
|
|
198
|
+
|
|
199
|
+
return RetrackDirectionResult(
|
|
200
|
+
stack=out,
|
|
201
|
+
n_retracked=n_retracked,
|
|
202
|
+
n_skipped=n_skipped,
|
|
203
|
+
first_target_frame=first_target_frame,
|
|
204
|
+
)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from itasc.napari.correction._correction_utils import frame_view_2d
|
|
8
|
+
from itasc.tracking_ultrack.corrections import Correction
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class SelectedCorrectionTarget:
|
|
13
|
+
cell_id: int
|
|
14
|
+
frame: int
|
|
15
|
+
y: float
|
|
16
|
+
x: float
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def selected_correction_target(
|
|
20
|
+
data: np.ndarray,
|
|
21
|
+
*,
|
|
22
|
+
cell_id: int,
|
|
23
|
+
frame: int,
|
|
24
|
+
) -> SelectedCorrectionTarget | None:
|
|
25
|
+
"""Return the selected cell centroid at *frame* when present."""
|
|
26
|
+
frame_view = _label_frame(data, frame)
|
|
27
|
+
if frame_view is None or not np.any(frame_view == cell_id):
|
|
28
|
+
return None
|
|
29
|
+
y, x = _label_centroid(frame_view, cell_id)
|
|
30
|
+
return SelectedCorrectionTarget(
|
|
31
|
+
cell_id=int(cell_id),
|
|
32
|
+
frame=int(frame),
|
|
33
|
+
y=y,
|
|
34
|
+
x=x,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def correction_for_label_frame(
|
|
39
|
+
data: np.ndarray,
|
|
40
|
+
*,
|
|
41
|
+
cell_id: int,
|
|
42
|
+
frame: int,
|
|
43
|
+
) -> Correction | None:
|
|
44
|
+
"""Build a validated correction for *cell_id* at *frame* when present."""
|
|
45
|
+
target = selected_correction_target(data, cell_id=cell_id, frame=frame)
|
|
46
|
+
if target is None:
|
|
47
|
+
return None
|
|
48
|
+
return Correction(
|
|
49
|
+
cell_id=target.cell_id,
|
|
50
|
+
t=target.frame,
|
|
51
|
+
kind="validated",
|
|
52
|
+
y=target.y,
|
|
53
|
+
x=target.x,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def corrections_for_label_frames(
|
|
58
|
+
data: np.ndarray,
|
|
59
|
+
*,
|
|
60
|
+
cell_id: int,
|
|
61
|
+
frames: list[int] | tuple[int, ...],
|
|
62
|
+
) -> list[Correction]:
|
|
63
|
+
"""Build validated corrections for frames where *cell_id* is present."""
|
|
64
|
+
corrections: list[Correction] = []
|
|
65
|
+
for frame in frames:
|
|
66
|
+
correction = correction_for_label_frame(
|
|
67
|
+
data,
|
|
68
|
+
cell_id=cell_id,
|
|
69
|
+
frame=int(frame),
|
|
70
|
+
)
|
|
71
|
+
if correction is not None:
|
|
72
|
+
corrections.append(correction)
|
|
73
|
+
return corrections
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _label_frame(data: np.ndarray, frame: int) -> np.ndarray | None:
|
|
77
|
+
arr = np.asarray(data)
|
|
78
|
+
return frame_view_2d(arr, frame) if arr.ndim >= 3 else arr
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _label_centroid(frame: np.ndarray, cell_id: int) -> tuple[float, float]:
|
|
82
|
+
yy, xx = np.nonzero(frame == cell_id)
|
|
83
|
+
return float(np.mean(yy)), float(np.mean(xx))
|