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,351 @@
|
|
|
1
|
+
"""Unified-accordion state — assembles the track bars + selected-track band.
|
|
2
|
+
|
|
3
|
+
Owns the correction accordion panel: it builds the per-track swimlane bars
|
|
4
|
+
(present runs from :func:`~itasc.segmentation.lineage.build_lineage`, status
|
|
5
|
+
frames from the project's validation records) and, for the *selected* track only,
|
|
6
|
+
the per-frame thumbnail band (via
|
|
7
|
+
:func:`~itasc.napari.correction._correction_track_path.build_track_film_strip`). Building
|
|
8
|
+
crops for a single track keeps refresh cheap no matter how many tracks exist.
|
|
9
|
+
|
|
10
|
+
The bar assembly and crops are pure/testable; this is the glue that reads layers,
|
|
11
|
+
owns the single :class:`~itasc.napari.correction._correction_track_accordion.TrackAccordionPanel`
|
|
12
|
+
(embedded by the host into its workspace splitter), and turns a bar/thumbnail
|
|
13
|
+
click into a viewer jump + cell selection via ``on_activate``.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from collections.abc import Callable
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
|
|
23
|
+
from itasc.tracking_ultrack.validation_state import read_corrections, read_validated_tracks
|
|
24
|
+
from itasc.napari.correction._correction_track_accordion import LaneView, TrackAccordionPanel
|
|
25
|
+
from itasc.napari.correction._correction_track_path import (
|
|
26
|
+
TrackFilmStrip,
|
|
27
|
+
build_track_film_strip,
|
|
28
|
+
)
|
|
29
|
+
from itasc.core.lineage import build_lineage
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
_NODE_OUTLINE = (0.75, 0.75, 0.75) # fallback neutral cell outline for the detail
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class LineageCanvasController:
|
|
37
|
+
"""Own the unified track-accordion panel for a correction session."""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
viewer,
|
|
42
|
+
*,
|
|
43
|
+
tracked_data_provider: Callable[[], np.ndarray | None],
|
|
44
|
+
tracked_layer_provider: Callable[[], object | None],
|
|
45
|
+
intensity_layer_provider: Callable[[], object | None],
|
|
46
|
+
selected_label_provider: Callable[[], int],
|
|
47
|
+
current_t_provider: Callable[[], int],
|
|
48
|
+
on_activate: Callable[[int, int], None],
|
|
49
|
+
pos_dir_provider: Callable[[], Path | None] | None = None,
|
|
50
|
+
validated_tracks_provider: Callable[[], dict[int, set[int]]] | None = None,
|
|
51
|
+
) -> None:
|
|
52
|
+
self.viewer = viewer
|
|
53
|
+
self._tracked_data_provider = tracked_data_provider
|
|
54
|
+
self._tracked_layer_provider = tracked_layer_provider
|
|
55
|
+
self._intensity_layer_provider = intensity_layer_provider
|
|
56
|
+
self._selected_label_provider = selected_label_provider
|
|
57
|
+
self._current_t_provider = current_t_provider
|
|
58
|
+
self._on_activate = on_activate
|
|
59
|
+
self._pos_dir_provider = pos_dir_provider
|
|
60
|
+
# DB-free hosts (no project directory to hold validated_cells.json) hand
|
|
61
|
+
# in their own in-memory {cell_id: {frames}} store instead; anchors are
|
|
62
|
+
# an Ultrack-workflow concept and stay empty in that case.
|
|
63
|
+
self._validated_tracks_provider = validated_tracks_provider
|
|
64
|
+
# The unified accordion panel is embedded as a bare widget into the
|
|
65
|
+
# host's workspace splitter; the controller no longer owns napari docks.
|
|
66
|
+
self._panel: TrackAccordionPanel | None = None
|
|
67
|
+
# Cached from the last refresh so a selection can rebuild only the detail.
|
|
68
|
+
self._occupied: dict[int, list[int]] = {}
|
|
69
|
+
self._validated_map: dict[int, set[int]] = {}
|
|
70
|
+
self._anchored_map: dict[int, set[int]] = {}
|
|
71
|
+
# Structural lanes (cell_id, column, segments) from the last full assemble,
|
|
72
|
+
# so a flag-only change can recolour without re-running build_lineage.
|
|
73
|
+
self._lane_structure: list[tuple[int, int, tuple[tuple[int, int], ...]]] | None = None
|
|
74
|
+
self._n_frames: int = 0
|
|
75
|
+
|
|
76
|
+
def refresh(self) -> None:
|
|
77
|
+
"""Rebuild the track bars and the selected track's band."""
|
|
78
|
+
tracked = self._tracked_data_provider()
|
|
79
|
+
if tracked is None:
|
|
80
|
+
if self._panel is not None:
|
|
81
|
+
self._panel.set_overview([], n_frames=0)
|
|
82
|
+
return
|
|
83
|
+
try:
|
|
84
|
+
lanes, n_frames = self._assemble(np.asarray(tracked))
|
|
85
|
+
except Exception:
|
|
86
|
+
logger.exception("lineage overview assembly failed")
|
|
87
|
+
return
|
|
88
|
+
self._ensure_panel()
|
|
89
|
+
self._panel.set_overview(
|
|
90
|
+
lanes, n_frames=n_frames, title=self._overview_title(lanes),
|
|
91
|
+
)
|
|
92
|
+
self.set_current_frame(self._current_t_provider())
|
|
93
|
+
self.set_selection(int(self._selected_label_provider() or 0))
|
|
94
|
+
|
|
95
|
+
def _assemble(self, tracked: np.ndarray) -> tuple[list[LaneView], int]:
|
|
96
|
+
model = build_lineage(tracked)
|
|
97
|
+
self._validated_map, self._anchored_map = self._validated_anchored_maps()
|
|
98
|
+
self._occupied = {}
|
|
99
|
+
lanes: list[LaneView] = []
|
|
100
|
+
for column, lane in enumerate(model.lanes):
|
|
101
|
+
cid = int(lane.cell_id)
|
|
102
|
+
segments = tuple((int(s.start), int(s.end)) for s in lane.segments)
|
|
103
|
+
self._occupied[cid] = [
|
|
104
|
+
f for s, e in segments for f in range(s, e + 1)
|
|
105
|
+
]
|
|
106
|
+
lanes.append(LaneView(
|
|
107
|
+
cell_id=cid,
|
|
108
|
+
column=column,
|
|
109
|
+
segments=segments,
|
|
110
|
+
validated=frozenset(self._validated_map.get(cid, ())),
|
|
111
|
+
anchored=frozenset(self._anchored_map.get(cid, ())),
|
|
112
|
+
))
|
|
113
|
+
self._lane_structure = [
|
|
114
|
+
(ln.cell_id, ln.column, ln.segments) for ln in lanes
|
|
115
|
+
]
|
|
116
|
+
self._n_frames = model.n_frames
|
|
117
|
+
return lanes, model.n_frames
|
|
118
|
+
|
|
119
|
+
def set_selection(self, cell_id: int) -> None:
|
|
120
|
+
"""Mark the selected track and rebuild only its expanded thumbnail band."""
|
|
121
|
+
cell_id = int(cell_id or 0)
|
|
122
|
+
if self._panel is None:
|
|
123
|
+
return
|
|
124
|
+
self._panel.set_selection(cell_id)
|
|
125
|
+
self._panel.set_strip(
|
|
126
|
+
self._build_detail(cell_id), title=self._detail_title(cell_id)
|
|
127
|
+
)
|
|
128
|
+
self._panel.set_current_frame(self._current_t_provider())
|
|
129
|
+
|
|
130
|
+
def set_current_frame(self, frame: int) -> None:
|
|
131
|
+
"""Move the shared frame guide / tile highlight without rebuilding."""
|
|
132
|
+
frame = int(frame)
|
|
133
|
+
if self._panel is not None:
|
|
134
|
+
self._panel.set_current_frame(frame)
|
|
135
|
+
|
|
136
|
+
def center_on_track(self, cell_id: int) -> None:
|
|
137
|
+
"""Scroll the panel so ``cell_id``'s bar row is vertically centered."""
|
|
138
|
+
if self._panel is not None:
|
|
139
|
+
self._panel.center_on_track(int(cell_id or 0))
|
|
140
|
+
|
|
141
|
+
def center_on_strip(self, cell_id: int | None = None) -> None:
|
|
142
|
+
"""Scroll the panel so the selected track's film strip is centered.
|
|
143
|
+
|
|
144
|
+
Falls back to centering on the bar row when the band is not built yet
|
|
145
|
+
(no selection / empty strip).
|
|
146
|
+
"""
|
|
147
|
+
if self._panel is None:
|
|
148
|
+
return
|
|
149
|
+
if not self._panel.center_on_strip() and cell_id is not None:
|
|
150
|
+
self._panel.center_on_track(int(cell_id or 0))
|
|
151
|
+
|
|
152
|
+
def step_film_frame(self, *, dx: int = 0, dy: int = 0, wrap: bool = False) -> None:
|
|
153
|
+
"""Move the current frame to the neighbouring thumbnail in the band.
|
|
154
|
+
|
|
155
|
+
``dx`` walks the strip left/right (reading order); ``dy`` jumps a whole
|
|
156
|
+
wrapped row up/down. ``wrap`` (the viewer's loop mode) makes an off-the-end
|
|
157
|
+
step return to the opposite end. Navigation reuses the same frame-jump
|
|
158
|
+
path a thumbnail click takes (select the frame on the selected track).
|
|
159
|
+
"""
|
|
160
|
+
if self._panel is None:
|
|
161
|
+
return
|
|
162
|
+
target = self._panel.grid_neighbor_frame(
|
|
163
|
+
int(self._current_t_provider()), dx=dx, dy=dy, wrap=wrap
|
|
164
|
+
)
|
|
165
|
+
if target is not None:
|
|
166
|
+
self._on_film_frame_clicked(int(target))
|
|
167
|
+
|
|
168
|
+
def refresh_status(self) -> None:
|
|
169
|
+
"""Recolour validated/anchored flags without rescanning the stack.
|
|
170
|
+
|
|
171
|
+
Validation and anchoring change only per-frame *status*, never track
|
|
172
|
+
topology, so the expensive whole-stack ``build_lineage`` in
|
|
173
|
+
:meth:`refresh` is unnecessary. This re-reads the validation records and
|
|
174
|
+
re-applies them to the lanes cached by the last full refresh, keeping
|
|
175
|
+
the GUI responsive on long tracks (the per-frame ``build_lineage`` froze
|
|
176
|
+
it for seconds). Falls back to a full :meth:`refresh` when no structure
|
|
177
|
+
has been cached yet.
|
|
178
|
+
"""
|
|
179
|
+
if self._panel is None:
|
|
180
|
+
return
|
|
181
|
+
if self._lane_structure is None:
|
|
182
|
+
self.refresh()
|
|
183
|
+
return
|
|
184
|
+
self._validated_map, self._anchored_map = self._validated_anchored_maps()
|
|
185
|
+
lanes = [
|
|
186
|
+
LaneView(
|
|
187
|
+
cell_id=cid,
|
|
188
|
+
column=column,
|
|
189
|
+
segments=segments,
|
|
190
|
+
validated=frozenset(self._validated_map.get(cid, ())),
|
|
191
|
+
anchored=frozenset(self._anchored_map.get(cid, ())),
|
|
192
|
+
)
|
|
193
|
+
for cid, column, segments in self._lane_structure
|
|
194
|
+
]
|
|
195
|
+
self._panel.set_overview(
|
|
196
|
+
lanes, n_frames=self._n_frames, title=self._overview_title(lanes),
|
|
197
|
+
)
|
|
198
|
+
self.set_current_frame(self._current_t_provider())
|
|
199
|
+
self.set_selection(int(self._selected_label_provider() or 0))
|
|
200
|
+
|
|
201
|
+
@staticmethod
|
|
202
|
+
def _overview_title(lanes) -> str:
|
|
203
|
+
"""Overview header: total tracks plus how many carry any validation.
|
|
204
|
+
|
|
205
|
+
This is the home for the validation summary now that the top status bar
|
|
206
|
+
is just the one-line save/action status — the count tracks every
|
|
207
|
+
validate / anchor / edit through the panel's own refresh.
|
|
208
|
+
"""
|
|
209
|
+
n_validated = sum(1 for lane in lanes if lane.validated)
|
|
210
|
+
return f"{len(lanes)} track(s), {n_validated} validated"
|
|
211
|
+
|
|
212
|
+
def refresh_detail(self) -> None:
|
|
213
|
+
"""Rebuild only the selected track's detail strip (no overview rescan).
|
|
214
|
+
|
|
215
|
+
Used after a rapid live edit (stepping swap candidates with Z / C): it
|
|
216
|
+
re-crops just the selected track from the cached frame set, so the strip
|
|
217
|
+
reflects the new pixels *without* re-running the whole-stack lineage
|
|
218
|
+
build that the full :meth:`refresh` does (that froze the GUI when fired
|
|
219
|
+
on every keystroke). The overview is left as-is until the next
|
|
220
|
+
full refresh (selection change, validate/anchor, reload).
|
|
221
|
+
"""
|
|
222
|
+
if self._panel is None:
|
|
223
|
+
return
|
|
224
|
+
self.set_selection(int(self._selected_label_provider() or 0))
|
|
225
|
+
|
|
226
|
+
def panel(self) -> TrackAccordionPanel:
|
|
227
|
+
"""The unified accordion widget (created on first access) to embed."""
|
|
228
|
+
self._ensure_panel()
|
|
229
|
+
return self._panel
|
|
230
|
+
|
|
231
|
+
def teardown(self) -> None:
|
|
232
|
+
"""Drop the reference to the panel for deactivate.
|
|
233
|
+
|
|
234
|
+
The panel is embedded as a bare widget in the host's workspace splitter
|
|
235
|
+
and is deleted when that dock is torn down; here we just drop our
|
|
236
|
+
reference so a later re-activate recreates it.
|
|
237
|
+
"""
|
|
238
|
+
self._panel = None
|
|
239
|
+
self._lane_structure = None
|
|
240
|
+
|
|
241
|
+
# -- assembly helpers ---------------------------------------------------
|
|
242
|
+
def _validated_anchored_maps(
|
|
243
|
+
self,
|
|
244
|
+
) -> tuple[dict[int, set[int]], dict[int, set[int]]]:
|
|
245
|
+
"""Per-track validated / anchored frame sets, read once per refresh."""
|
|
246
|
+
if self._validated_tracks_provider is not None:
|
|
247
|
+
try:
|
|
248
|
+
validated = {
|
|
249
|
+
int(cell_id): {int(f) for f in frames}
|
|
250
|
+
for cell_id, frames in (self._validated_tracks_provider() or {}).items()
|
|
251
|
+
}
|
|
252
|
+
except Exception:
|
|
253
|
+
logger.exception("could not read in-memory validated tracks for the canvas")
|
|
254
|
+
validated = {}
|
|
255
|
+
return validated, {}
|
|
256
|
+
pos_dir = self._pos_dir_provider() if self._pos_dir_provider else None
|
|
257
|
+
if pos_dir is None:
|
|
258
|
+
return {}, {}
|
|
259
|
+
try:
|
|
260
|
+
validated = {
|
|
261
|
+
int(cell_id): {int(f) for f in frames}
|
|
262
|
+
for cell_id, frames in (read_validated_tracks(pos_dir) or {}).items()
|
|
263
|
+
}
|
|
264
|
+
anchored: dict[int, set[int]] = {}
|
|
265
|
+
for corr in read_corrections(pos_dir):
|
|
266
|
+
if getattr(corr, "kind", None) == "anchor":
|
|
267
|
+
anchored.setdefault(int(corr.cell_id), set()).add(int(corr.t))
|
|
268
|
+
except Exception:
|
|
269
|
+
logger.exception("could not read validated/anchored frames for the canvas")
|
|
270
|
+
return {}, {}
|
|
271
|
+
return validated, anchored
|
|
272
|
+
|
|
273
|
+
def _build_detail(self, cell_id: int) -> TrackFilmStrip:
|
|
274
|
+
tracked = self._tracked_data_provider()
|
|
275
|
+
intensity_layer = self._intensity_layer_provider()
|
|
276
|
+
if not cell_id or tracked is None or intensity_layer is None:
|
|
277
|
+
return TrackFilmStrip(tiles=())
|
|
278
|
+
try:
|
|
279
|
+
return build_track_film_strip(
|
|
280
|
+
np.asarray(tracked),
|
|
281
|
+
np.asarray(intensity_layer.data),
|
|
282
|
+
cell_id,
|
|
283
|
+
colormap=self._intensity_colormap(intensity_layer),
|
|
284
|
+
outline_color=self._track_outline_color(cell_id),
|
|
285
|
+
frames=self._occupied.get(cell_id),
|
|
286
|
+
validated_frames=self._validated_map.get(cell_id),
|
|
287
|
+
anchored_frames=self._anchored_map.get(cell_id),
|
|
288
|
+
# Pad incomplete tracks to the full timeline so frames the track
|
|
289
|
+
# skips render as empty placeholder thumbnails.
|
|
290
|
+
total_frames=self._n_frames,
|
|
291
|
+
)
|
|
292
|
+
except Exception:
|
|
293
|
+
logger.exception("detail film strip build failed")
|
|
294
|
+
return TrackFilmStrip(tiles=())
|
|
295
|
+
|
|
296
|
+
def _detail_title(self, cell_id: int) -> str:
|
|
297
|
+
if not cell_id:
|
|
298
|
+
return "No track selected"
|
|
299
|
+
return f"Track {cell_id} — {len(self._occupied.get(cell_id, ()))} frame(s)"
|
|
300
|
+
|
|
301
|
+
def _track_outline_color(self, cell_id: int):
|
|
302
|
+
"""RGB (0..1) the tracked labels layer paints ``cell_id`` with, or None."""
|
|
303
|
+
layer = self._tracked_layer_provider()
|
|
304
|
+
color_dict = getattr(getattr(layer, "colormap", None), "color_dict", None)
|
|
305
|
+
try:
|
|
306
|
+
raw = color_dict.get(int(cell_id)) if color_dict is not None else None
|
|
307
|
+
except Exception:
|
|
308
|
+
raw = None
|
|
309
|
+
if raw is None or isinstance(raw, str):
|
|
310
|
+
return _NODE_OUTLINE
|
|
311
|
+
rgba = np.asarray(raw, dtype=float).ravel()
|
|
312
|
+
if rgba.size < 3:
|
|
313
|
+
return _NODE_OUTLINE
|
|
314
|
+
return (float(rgba[0]), float(rgba[1]), float(rgba[2]))
|
|
315
|
+
|
|
316
|
+
@staticmethod
|
|
317
|
+
def _intensity_colormap(layer):
|
|
318
|
+
"""Adapt the intensity layer's colormap (e.g. 'bop purple') to (h,w)->RGB."""
|
|
319
|
+
cmap = getattr(layer, "colormap", None)
|
|
320
|
+
if cmap is None or not hasattr(cmap, "map"):
|
|
321
|
+
return None
|
|
322
|
+
|
|
323
|
+
def _map(values: np.ndarray) -> np.ndarray:
|
|
324
|
+
flat = np.asarray(values, dtype=float).ravel()
|
|
325
|
+
mapped = np.asarray(cmap.map(flat), dtype=float)
|
|
326
|
+
return mapped.reshape(values.shape + (mapped.shape[-1],))
|
|
327
|
+
|
|
328
|
+
return _map
|
|
329
|
+
|
|
330
|
+
def _ensure_panel(self) -> None:
|
|
331
|
+
"""Create the unified accordion panel as a bare widget (idempotent)."""
|
|
332
|
+
if self._panel is None:
|
|
333
|
+
panel = TrackAccordionPanel(tile_px=72)
|
|
334
|
+
panel.node_activated.connect(self._on_node_activated)
|
|
335
|
+
panel.frame_clicked.connect(self._on_film_frame_clicked)
|
|
336
|
+
self._panel = panel
|
|
337
|
+
|
|
338
|
+
def _on_node_activated(self, frame: int, cell_id: int) -> None:
|
|
339
|
+
try:
|
|
340
|
+
self._on_activate(int(frame), int(cell_id))
|
|
341
|
+
except Exception:
|
|
342
|
+
logger.exception("lineage canvas navigation failed")
|
|
343
|
+
|
|
344
|
+
def _on_film_frame_clicked(self, frame: int) -> None:
|
|
345
|
+
cell_id = int(self._selected_label_provider() or 0)
|
|
346
|
+
if not cell_id:
|
|
347
|
+
return
|
|
348
|
+
self._on_node_activated(int(frame), cell_id)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
__all__ = ["LineageCanvasController"]
|