itasc 0.2.0.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. itasc/__init__.py +16 -0
  2. itasc/cellpose/__init__.py +31 -0
  3. itasc/cellpose/cellpose_runner.py +334 -0
  4. itasc/cellpose/divergence_maps.py +380 -0
  5. itasc/cellpose/flow_following.py +283 -0
  6. itasc/cellpose/joint.py +98 -0
  7. itasc/cellpose/napari.yaml +10 -0
  8. itasc/cellpose/native_masks.py +313 -0
  9. itasc/cellpose/retrack.py +232 -0
  10. itasc/cellpose/shape.py +51 -0
  11. itasc/cellpose/track_laptrack.py +279 -0
  12. itasc/contact_analysis/__init__.py +61 -0
  13. itasc/contact_analysis/_provenance.py +29 -0
  14. itasc/contact_analysis/catalog.py +393 -0
  15. itasc/contact_analysis/config.py +155 -0
  16. itasc/contact_analysis/contacts/__init__.py +24 -0
  17. itasc/contact_analysis/contacts/batch.py +158 -0
  18. itasc/contact_analysis/contacts/build.py +837 -0
  19. itasc/contact_analysis/contacts/contact_labels.py +136 -0
  20. itasc/contact_analysis/contacts/neighborhood.py +146 -0
  21. itasc/contact_analysis/contacts/reader.py +58 -0
  22. itasc/contact_analysis/contacts/signed_contact_length.py +143 -0
  23. itasc/contact_analysis/curation.py +185 -0
  24. itasc/contact_analysis/dynamics/__init__.py +43 -0
  25. itasc/contact_analysis/dynamics/collective.py +206 -0
  26. itasc/contact_analysis/dynamics/kinematics.py +278 -0
  27. itasc/contact_analysis/dynamics/msd.py +165 -0
  28. itasc/contact_analysis/dynamics/store.py +251 -0
  29. itasc/contact_analysis/dynamics/trajectories.py +100 -0
  30. itasc/contact_analysis/frame_interval.py +85 -0
  31. itasc/contact_analysis/napari.yaml +10 -0
  32. itasc/contact_analysis/pipeline.py +313 -0
  33. itasc/contact_analysis/pixel_size.py +166 -0
  34. itasc/contact_analysis/quantifier.py +204 -0
  35. itasc/contact_analysis/quantifiers/__init__.py +7 -0
  36. itasc/contact_analysis/quantifiers/_contacts_derived.py +42 -0
  37. itasc/contact_analysis/quantifiers/cell_density.py +65 -0
  38. itasc/contact_analysis/quantifiers/cell_dynamics.py +81 -0
  39. itasc/contact_analysis/quantifiers/cell_shape.py +32 -0
  40. itasc/contact_analysis/quantifiers/contacts.py +72 -0
  41. itasc/contact_analysis/quantifiers/neighbor_count.py +26 -0
  42. itasc/contact_analysis/quantifiers/nucleus_dynamics.py +77 -0
  43. itasc/contact_analysis/quantifiers/nucleus_shape.py +35 -0
  44. itasc/contact_analysis/quantifiers/shape_relational.py +34 -0
  45. itasc/contact_analysis/quantifiers/signed_contact_length.py +44 -0
  46. itasc/contact_analysis/records.py +129 -0
  47. itasc/contact_analysis/shape/__init__.py +19 -0
  48. itasc/contact_analysis/shape/core.py +141 -0
  49. itasc/contact_analysis/shape/relational.py +134 -0
  50. itasc/contact_analysis/shape_tables.py +387 -0
  51. itasc/core/__init__.py +0 -0
  52. itasc/core/cancellation.py +15 -0
  53. itasc/core/commit.py +61 -0
  54. itasc/core/imageops.py +34 -0
  55. itasc/core/label_store.py +53 -0
  56. itasc/core/lineage.py +113 -0
  57. itasc/core/logging.py +51 -0
  58. itasc/core/paths.py +34 -0
  59. itasc/core/tiff.py +13 -0
  60. itasc/correction/__init__.py +25 -0
  61. itasc/correction/labels.py +983 -0
  62. itasc/napari/__init__.py +7 -0
  63. itasc/napari/_experiments_panel.py +1091 -0
  64. itasc/napari/_flow_layout.py +98 -0
  65. itasc/napari/_icons.py +126 -0
  66. itasc/napari/_napari_compat.py +118 -0
  67. itasc/napari/_paths.py +231 -0
  68. itasc/napari/_preview_cache.py +60 -0
  69. itasc/napari/_spotlight.py +46 -0
  70. itasc/napari/_stage_loader.py +146 -0
  71. itasc/napari/_stage_status.py +110 -0
  72. itasc/napari/_standalone_paths.py +89 -0
  73. itasc/napari/_state.py +146 -0
  74. itasc/napari/_status_rail.py +128 -0
  75. itasc/napari/_track_render.py +114 -0
  76. itasc/napari/_widget_helpers.py +236 -0
  77. itasc/napari/aggregate_widget.py +339 -0
  78. itasc/napari/candidate_gallery_controller.py +285 -0
  79. itasc/napari/cell_workflow_widget.py +1405 -0
  80. itasc/napari/cellpose_segment_track_widget.py +1253 -0
  81. itasc/napari/cellpose_widget.py +1034 -0
  82. itasc/napari/contact_analysis/__init__.py +6 -0
  83. itasc/napari/contact_analysis/curation_controller.py +94 -0
  84. itasc/napari/contact_analysis/plugins/__init__.py +102 -0
  85. itasc/napari/contact_analysis/plugins/catalog_summary.py +56 -0
  86. itasc/napari/contact_analysis/plugins/curation.py +247 -0
  87. itasc/napari/contact_analysis/plugins/visualize_contacts.py +64 -0
  88. itasc/napari/contact_analysis_widget.py +951 -0
  89. itasc/napari/contact_visualization.py +895 -0
  90. itasc/napari/correction/__init__.py +8 -0
  91. itasc/napari/correction/_correction_anchor.py +54 -0
  92. itasc/napari/correction/_correction_candidate_panel.py +259 -0
  93. itasc/napari/correction/_correction_candidates.py +169 -0
  94. itasc/napari/correction/_correction_centroids.py +132 -0
  95. itasc/napari/correction/_correction_commit.py +71 -0
  96. itasc/napari/correction/_correction_events.py +39 -0
  97. itasc/napari/correction/_correction_keymap.py +80 -0
  98. itasc/napari/correction/_correction_layer_lifecycle.py +137 -0
  99. itasc/napari/correction/_correction_layer_loader.py +86 -0
  100. itasc/napari/correction/_correction_navigation.py +97 -0
  101. itasc/napari/correction/_correction_paint.py +46 -0
  102. itasc/napari/correction/_correction_playback.py +68 -0
  103. itasc/napari/correction/_correction_protection.py +44 -0
  104. itasc/napari/correction/_correction_takeover.py +64 -0
  105. itasc/napari/correction/_correction_track_accordion.py +566 -0
  106. itasc/napari/correction/_correction_track_path.py +388 -0
  107. itasc/napari/correction/_correction_ui.py +367 -0
  108. itasc/napari/correction/_correction_ui_nucleus.py +393 -0
  109. itasc/napari/correction/_correction_utils.py +204 -0
  110. itasc/napari/correction/_correction_validation.py +83 -0
  111. itasc/napari/correction/cell_correction_widget.py +1190 -0
  112. itasc/napari/correction/correction_widget.py +1363 -0
  113. itasc/napari/correction/nucleus_correction_widget.py +2185 -0
  114. itasc/napari/divergence_maps_widget.py +795 -0
  115. itasc/napari/lineage_canvas_controller.py +351 -0
  116. itasc/napari/main_widget.py +1184 -0
  117. itasc/napari/nucleus_atom_extraction_widget.py +648 -0
  118. itasc/napari/nucleus_db_browser_widget.py +1442 -0
  119. itasc/napari/nucleus_pipeline_widget.py +579 -0
  120. itasc/napari/nucleus_segmentation_inputs_widget.py +21 -0
  121. itasc/napari/nucleus_tracking_inputs_widget.py +248 -0
  122. itasc/napari/nucleus_workflow_widget.py +963 -0
  123. itasc/napari/track_path_controller.py +387 -0
  124. itasc/napari/ui_gate.py +266 -0
  125. itasc/napari/ui_style.py +503 -0
  126. itasc/napari/validated_overlay_controller.py +254 -0
  127. itasc/napari/widgets.py +616 -0
  128. itasc/napari.yaml +45 -0
  129. itasc/segmentation/__init__.py +60 -0
  130. itasc/segmentation/cell_divergence_segmentation.py +433 -0
  131. itasc/segmentation/cell_label_icm.py +637 -0
  132. itasc/segmentation/contour_filtering.py +128 -0
  133. itasc/segmentation/lineage.py +18 -0
  134. itasc/segmentation/nucleus_segmentation.py +35 -0
  135. itasc/tracking_ultrack/__init__.py +12 -0
  136. itasc/tracking_ultrack/_node_geometry.py +191 -0
  137. itasc/tracking_ultrack/atoms.py +470 -0
  138. itasc/tracking_ultrack/config.py +76 -0
  139. itasc/tracking_ultrack/corrections.py +1213 -0
  140. itasc/tracking_ultrack/db_build.py +492 -0
  141. itasc/tracking_ultrack/db_query.py +952 -0
  142. itasc/tracking_ultrack/export.py +133 -0
  143. itasc/tracking_ultrack/extend.py +243 -0
  144. itasc/tracking_ultrack/ingest.py +83 -0
  145. itasc/tracking_ultrack/linking.py +286 -0
  146. itasc/tracking_ultrack/multi_threshold.py +46 -0
  147. itasc/tracking_ultrack/napari.yaml +10 -0
  148. itasc/tracking_ultrack/reseed.py +456 -0
  149. itasc/tracking_ultrack/retracker.py +153 -0
  150. itasc/tracking_ultrack/scoring.py +74 -0
  151. itasc/tracking_ultrack/seed_prior.py +175 -0
  152. itasc/tracking_ultrack/solve.py +52 -0
  153. itasc/tracking_ultrack/swap_candidate.py +171 -0
  154. itasc/tracking_ultrack/track_quality.py +142 -0
  155. itasc/tracking_ultrack/validation_nodes.py +315 -0
  156. itasc/tracking_ultrack/validation_state.py +430 -0
  157. itasc-0.2.0.dev0.data/data/itasc/napari.yaml +45 -0
  158. itasc-0.2.0.dev0.dist-info/METADATA +186 -0
  159. itasc-0.2.0.dev0.dist-info/RECORD +162 -0
  160. itasc-0.2.0.dev0.dist-info/WHEEL +4 -0
  161. itasc-0.2.0.dev0.dist-info/entry_points.txt +2 -0
  162. itasc-0.2.0.dev0.dist-info/licenses/LICENSE +661 -0
@@ -0,0 +1,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"]