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,285 @@
|
|
|
1
|
+
"""Docked candidate-gallery state for the correction workspace.
|
|
2
|
+
|
|
3
|
+
Owns the :class:`CandidateGalleryPanel` and keeps its three columns
|
|
4
|
+
(extend-backward · swap · extend-forward) in sync with the selected track and
|
|
5
|
+
current frame: on :meth:`refresh` it enumerates the swap lineage
|
|
6
|
+
(``list_swap_candidates``) and the extend shortlist in each direction
|
|
7
|
+
(``list_extend_candidates``), renders each as a thumbnail strip over the
|
|
8
|
+
intensity frame (via the pure :func:`build_candidate_strip`), and caches the
|
|
9
|
+
candidates by key. A thumbnail click is routed back through the injected
|
|
10
|
+
``apply_swap`` / ``apply_extend`` callbacks — the host widget owns the actual
|
|
11
|
+
label mutation, history and protection — after which the gallery refreshes.
|
|
12
|
+
|
|
13
|
+
The DB enumerators are injected so the click-routing and strip-building are
|
|
14
|
+
unit-testable with fakes; the colormap/outline adaptation mirrors
|
|
15
|
+
:class:`~itasc.napari.lineage_canvas_controller.LineageCanvasController`.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from collections.abc import Callable
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
|
|
25
|
+
from itasc.napari.correction._correction_candidates import (
|
|
26
|
+
CandidateSpec,
|
|
27
|
+
CandidateStrip,
|
|
28
|
+
build_candidate_strip,
|
|
29
|
+
)
|
|
30
|
+
from itasc.napari.correction._correction_candidate_panel import CandidateGalleryPanel
|
|
31
|
+
from itasc.tracking_ultrack.extend import list_extend_candidates
|
|
32
|
+
from itasc.tracking_ultrack.swap_candidate import list_swap_candidates
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
_NEUTRAL_OUTLINE = (0.75, 0.75, 0.75)
|
|
37
|
+
|
|
38
|
+
# direction-keyed extend columns
|
|
39
|
+
_EXTEND_DIRECTION = {
|
|
40
|
+
CandidateGalleryPanel.EXTEND_BACKWARD: "backward",
|
|
41
|
+
CandidateGalleryPanel.EXTEND_FORWARD: "forward",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class CandidateGalleryController:
|
|
46
|
+
"""Own the docked candidate galleries and apply a clicked candidate."""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
viewer,
|
|
51
|
+
*,
|
|
52
|
+
tracked_data_provider: Callable[[], np.ndarray | None],
|
|
53
|
+
tracked_layer_provider: Callable[[], object | None],
|
|
54
|
+
intensity_layer_provider: Callable[[], object | None],
|
|
55
|
+
selected_label_provider: Callable[[], int],
|
|
56
|
+
current_t_provider: Callable[[], int],
|
|
57
|
+
db_path_provider: Callable[[], Path | None],
|
|
58
|
+
protected_mask_provider: Callable[[int], np.ndarray | None],
|
|
59
|
+
extend_kwargs_provider: Callable[[], dict],
|
|
60
|
+
apply_swap: Callable[[object], None],
|
|
61
|
+
apply_extend: Callable[[str, int, object], None],
|
|
62
|
+
status_callback: Callable[[str], None] = lambda _msg: None,
|
|
63
|
+
list_swap: Callable[..., list] = list_swap_candidates,
|
|
64
|
+
list_extend: Callable[..., object] = list_extend_candidates,
|
|
65
|
+
) -> None:
|
|
66
|
+
self.viewer = viewer
|
|
67
|
+
self._tracked_data_provider = tracked_data_provider
|
|
68
|
+
self._tracked_layer_provider = tracked_layer_provider
|
|
69
|
+
self._intensity_layer_provider = intensity_layer_provider
|
|
70
|
+
self._selected_label_provider = selected_label_provider
|
|
71
|
+
self._current_t_provider = current_t_provider
|
|
72
|
+
self._db_path_provider = db_path_provider
|
|
73
|
+
self._protected_mask_provider = protected_mask_provider
|
|
74
|
+
self._extend_kwargs_provider = extend_kwargs_provider
|
|
75
|
+
self._apply_swap = apply_swap
|
|
76
|
+
self._apply_extend = apply_extend
|
|
77
|
+
self._status = status_callback
|
|
78
|
+
self._list_swap = list_swap
|
|
79
|
+
self._list_extend = list_extend
|
|
80
|
+
|
|
81
|
+
self._panel: CandidateGalleryPanel | None = None
|
|
82
|
+
# key caches for click routing, rebuilt each refresh
|
|
83
|
+
self._swap_by_key: dict[int, object] = {}
|
|
84
|
+
self._extend_cache: dict[str, tuple[int, dict[int, object]]] = {}
|
|
85
|
+
|
|
86
|
+
# -- lifecycle ----------------------------------------------------------
|
|
87
|
+
def _ensure_panel(self) -> CandidateGalleryPanel:
|
|
88
|
+
if self._panel is not None:
|
|
89
|
+
return self._panel
|
|
90
|
+
panel = CandidateGalleryPanel()
|
|
91
|
+
panel.candidate_activated.connect(self._on_activated)
|
|
92
|
+
self._panel = panel
|
|
93
|
+
return panel
|
|
94
|
+
|
|
95
|
+
def widget(self) -> CandidateGalleryPanel:
|
|
96
|
+
"""The gallery widget (created on first access) to embed in the splitter.
|
|
97
|
+
|
|
98
|
+
``refresh`` skips populating until a cell is selected, so the workspace
|
|
99
|
+
layout calls this to materialise the (possibly empty) panel eagerly and
|
|
100
|
+
slot it into the splitter strip before any selection exists.
|
|
101
|
+
"""
|
|
102
|
+
return self._ensure_panel()
|
|
103
|
+
|
|
104
|
+
def teardown(self) -> None:
|
|
105
|
+
"""Forget the panel (next refresh re-creates it).
|
|
106
|
+
|
|
107
|
+
The panel is embedded as a bare widget in the host's workspace splitter
|
|
108
|
+
and is deleted when that dock is torn down; here we only drop the
|
|
109
|
+
reference so a later re-activate rebuilds it.
|
|
110
|
+
"""
|
|
111
|
+
self._panel = None
|
|
112
|
+
self._swap_by_key = {}
|
|
113
|
+
self._extend_cache = {}
|
|
114
|
+
|
|
115
|
+
def clear(self) -> None:
|
|
116
|
+
if self._panel is not None:
|
|
117
|
+
self._panel.clear()
|
|
118
|
+
self._swap_by_key = {}
|
|
119
|
+
self._extend_cache = {}
|
|
120
|
+
|
|
121
|
+
# -- refresh ------------------------------------------------------------
|
|
122
|
+
def refresh(self) -> None:
|
|
123
|
+
"""Recompute and repaint the three candidate columns for the selection."""
|
|
124
|
+
lab = int(self._selected_label_provider() or 0)
|
|
125
|
+
tracked = self._tracked_data_provider()
|
|
126
|
+
db_path = self._db_path_provider()
|
|
127
|
+
if not lab or tracked is None or db_path is None:
|
|
128
|
+
self.clear()
|
|
129
|
+
return
|
|
130
|
+
tracked = np.asarray(tracked)
|
|
131
|
+
if tracked.ndim != 3:
|
|
132
|
+
self.clear()
|
|
133
|
+
return
|
|
134
|
+
t = int(self._current_t_provider())
|
|
135
|
+
if t < 0 or t >= tracked.shape[0]:
|
|
136
|
+
self.clear()
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
panel = self._ensure_panel()
|
|
140
|
+
intensity_2d, colormap = self._intensity_frame(t, tracked.shape[1:])
|
|
141
|
+
outline = self._track_color(lab)
|
|
142
|
+
|
|
143
|
+
self._populate_swap(panel, tracked, t, lab, intensity_2d, colormap, outline, db_path)
|
|
144
|
+
self._populate_extend(panel, tracked, t, lab, intensity_2d, colormap, outline, db_path)
|
|
145
|
+
|
|
146
|
+
def _populate_swap(
|
|
147
|
+
self, panel, tracked, t, lab, intensity_2d, colormap, outline, db_path
|
|
148
|
+
) -> None:
|
|
149
|
+
source_mask = tracked[t] == lab
|
|
150
|
+
self._swap_by_key = {}
|
|
151
|
+
if not source_mask.any():
|
|
152
|
+
panel.set_column(panel.SWAP, CandidateStrip())
|
|
153
|
+
return
|
|
154
|
+
try:
|
|
155
|
+
candidates = self._list_swap(
|
|
156
|
+
db_path=db_path,
|
|
157
|
+
frame=t,
|
|
158
|
+
source_mask=source_mask,
|
|
159
|
+
frame_shape=tuple(tracked.shape[1:]),
|
|
160
|
+
protected_mask=self._protected_mask_provider(t),
|
|
161
|
+
)
|
|
162
|
+
except Exception:
|
|
163
|
+
logger.exception("swap candidate enumeration failed")
|
|
164
|
+
candidates = []
|
|
165
|
+
specs = [
|
|
166
|
+
CandidateSpec(key=int(c.node_id), mask=c.mask_2d, caption=f"{int(c.area)} px")
|
|
167
|
+
for c in candidates
|
|
168
|
+
]
|
|
169
|
+
self._swap_by_key = {int(c.node_id): c for c in candidates}
|
|
170
|
+
panel.set_column(
|
|
171
|
+
panel.SWAP,
|
|
172
|
+
build_candidate_strip(
|
|
173
|
+
intensity_2d, specs, colormap=colormap, outline_color=outline
|
|
174
|
+
),
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def _populate_extend(
|
|
178
|
+
self, panel, tracked, t, lab, intensity_2d, colormap, outline, db_path
|
|
179
|
+
) -> None:
|
|
180
|
+
extend_kwargs = self._extend_kwargs_provider() or {}
|
|
181
|
+
self._extend_cache = {}
|
|
182
|
+
for which, direction in _EXTEND_DIRECTION.items():
|
|
183
|
+
try:
|
|
184
|
+
result = self._list_extend(
|
|
185
|
+
source_id=lab,
|
|
186
|
+
source_frame=t,
|
|
187
|
+
direction=direction,
|
|
188
|
+
tracked_labels=tracked,
|
|
189
|
+
db_path=db_path,
|
|
190
|
+
**extend_kwargs,
|
|
191
|
+
)
|
|
192
|
+
except Exception:
|
|
193
|
+
logger.exception("extend candidate enumeration failed (%s)", direction)
|
|
194
|
+
panel.set_column(which, CandidateStrip())
|
|
195
|
+
continue
|
|
196
|
+
assignments = tuple(getattr(result, "assignments", ()) or ())
|
|
197
|
+
target_frame = int(getattr(result, "target_frame", -1))
|
|
198
|
+
specs = [
|
|
199
|
+
CandidateSpec(
|
|
200
|
+
key=int(a.candidate_label),
|
|
201
|
+
mask=a.mask_2d,
|
|
202
|
+
caption=self._extend_caption(a),
|
|
203
|
+
)
|
|
204
|
+
for a in assignments
|
|
205
|
+
]
|
|
206
|
+
self._extend_cache[which] = (
|
|
207
|
+
target_frame,
|
|
208
|
+
{int(a.candidate_label): a for a in assignments},
|
|
209
|
+
)
|
|
210
|
+
# Extend candidates live on the *adjacent* frame, so crop their masks
|
|
211
|
+
# over that frame's intensity rather than the current one.
|
|
212
|
+
adj_intensity, adj_cmap = (
|
|
213
|
+
self._intensity_frame(target_frame, tracked.shape[1:])
|
|
214
|
+
if 0 <= target_frame < tracked.shape[0]
|
|
215
|
+
else (intensity_2d, colormap)
|
|
216
|
+
)
|
|
217
|
+
panel.set_column(
|
|
218
|
+
which,
|
|
219
|
+
build_candidate_strip(
|
|
220
|
+
adj_intensity, specs, colormap=adj_cmap, outline_color=outline
|
|
221
|
+
),
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
@staticmethod
|
|
225
|
+
def _extend_caption(assignment) -> str:
|
|
226
|
+
weight = float(getattr(assignment, "weight", 0.0))
|
|
227
|
+
return f"w {weight:.2f}"
|
|
228
|
+
|
|
229
|
+
# -- click routing ------------------------------------------------------
|
|
230
|
+
def _on_activated(self, which: str, key: int) -> None:
|
|
231
|
+
try:
|
|
232
|
+
if which == CandidateGalleryPanel.SWAP:
|
|
233
|
+
candidate = self._swap_by_key.get(int(key))
|
|
234
|
+
if candidate is not None:
|
|
235
|
+
self._apply_swap(candidate)
|
|
236
|
+
else:
|
|
237
|
+
target_frame, mapping = self._extend_cache.get(which, (-1, {}))
|
|
238
|
+
assignment = mapping.get(int(key))
|
|
239
|
+
if assignment is not None:
|
|
240
|
+
self._apply_extend(which, target_frame, assignment)
|
|
241
|
+
except Exception:
|
|
242
|
+
logger.exception("applying candidate failed (%s, %s)", which, key)
|
|
243
|
+
return
|
|
244
|
+
self.refresh()
|
|
245
|
+
|
|
246
|
+
# -- intensity / colour helpers ----------------------------------------
|
|
247
|
+
def _intensity_frame(self, t: int, frame_shape) -> tuple[np.ndarray, object]:
|
|
248
|
+
layer = self._intensity_layer_provider()
|
|
249
|
+
if layer is None:
|
|
250
|
+
return np.zeros(frame_shape, dtype=np.float32), None
|
|
251
|
+
data = np.asarray(layer.data)
|
|
252
|
+
plane = data[t] if data.ndim >= 3 else data
|
|
253
|
+
while plane.ndim > 2:
|
|
254
|
+
plane = plane[0]
|
|
255
|
+
return plane, self._adapt_colormap(layer)
|
|
256
|
+
|
|
257
|
+
@staticmethod
|
|
258
|
+
def _adapt_colormap(layer):
|
|
259
|
+
cmap = getattr(layer, "colormap", None)
|
|
260
|
+
if cmap is None or not hasattr(cmap, "map"):
|
|
261
|
+
return None
|
|
262
|
+
|
|
263
|
+
def _map(values: np.ndarray) -> np.ndarray:
|
|
264
|
+
flat = np.asarray(values, dtype=float).ravel()
|
|
265
|
+
mapped = np.asarray(cmap.map(flat), dtype=float)
|
|
266
|
+
return mapped.reshape(values.shape + (mapped.shape[-1],))
|
|
267
|
+
|
|
268
|
+
return _map
|
|
269
|
+
|
|
270
|
+
def _track_color(self, cell_id: int):
|
|
271
|
+
layer = self._tracked_layer_provider()
|
|
272
|
+
color_dict = getattr(getattr(layer, "colormap", None), "color_dict", None)
|
|
273
|
+
try:
|
|
274
|
+
raw = color_dict.get(int(cell_id)) if color_dict is not None else None
|
|
275
|
+
except Exception:
|
|
276
|
+
raw = None
|
|
277
|
+
if raw is None or isinstance(raw, str):
|
|
278
|
+
return _NEUTRAL_OUTLINE
|
|
279
|
+
rgba = np.asarray(raw, dtype=float).ravel()
|
|
280
|
+
if rgba.size < 3:
|
|
281
|
+
return _NEUTRAL_OUTLINE
|
|
282
|
+
return (float(rgba[0]), float(rgba[1]), float(rgba[2]))
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
__all__ = ["CandidateGalleryController"]
|