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,315 @@
1
+ """Inject validated tracked labels as annotated Ultrack nodes."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+
7
+ import numpy as np
8
+
9
+ from itasc.tracking_ultrack.config import TrackingConfig
10
+ from itasc.tracking_ultrack._node_geometry import (
11
+ intersects,
12
+ make_node_pickle,
13
+ node_bbox_and_mask,
14
+ node_pickle_ndim,
15
+ raw_iou,
16
+ )
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class ValidationInjectionReport:
21
+ inserted: int
22
+ skipped_missing: int
23
+ skipped: list[tuple[int, int]]
24
+ faked: int
25
+ overlaps_added: int
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class _MaskRecord:
30
+ cell_id: int
31
+ t: int
32
+ bbox: tuple[int, int, int, int]
33
+ mask: np.ndarray
34
+ area: int
35
+ y: float
36
+ x: float
37
+
38
+
39
+ def _frame_mask_for_cell(tracked_labels: np.ndarray, t: int, cell_id: int) -> np.ndarray:
40
+ frame = np.asarray(tracked_labels)[t]
41
+ if frame.ndim == 2:
42
+ return np.asarray(frame == cell_id)
43
+ if frame.ndim == 3:
44
+ return np.asarray(frame == cell_id).any(axis=0)
45
+ raise ValueError(f"Expected tracked frame to be 2D or 3D, got shape {frame.shape}")
46
+
47
+
48
+ def _validated_mask_records(
49
+ validated_tracks: dict[int, set[int]],
50
+ tracked_labels: np.ndarray,
51
+ ) -> tuple[list[_MaskRecord], list[tuple[int, int]]]:
52
+ records: list[_MaskRecord] = []
53
+ skipped: list[tuple[int, int]] = []
54
+ labels = np.asarray(tracked_labels)
55
+ n_frames = int(labels.shape[0])
56
+
57
+ for cell_id, frames in sorted(validated_tracks.items()):
58
+ for raw_t in sorted(frames):
59
+ t = int(raw_t)
60
+ if t < 0 or t >= n_frames:
61
+ skipped.append((int(cell_id), t))
62
+ continue
63
+
64
+ mask_2d = _frame_mask_for_cell(labels, t, int(cell_id))
65
+ if not mask_2d.any():
66
+ skipped.append((int(cell_id), t))
67
+ continue
68
+
69
+ rows = np.flatnonzero(mask_2d.any(axis=1))
70
+ cols = np.flatnonzero(mask_2d.any(axis=0))
71
+ y0, y1 = int(rows[0]), int(rows[-1]) + 1
72
+ x0, x1 = int(cols[0]), int(cols[-1]) + 1
73
+ crop = np.ascontiguousarray(mask_2d[y0:y1, x0:x1], dtype=bool)
74
+ ys, xs = np.nonzero(crop)
75
+ records.append(
76
+ _MaskRecord(
77
+ cell_id=int(cell_id),
78
+ t=t,
79
+ bbox=(y0, x0, y1, x1),
80
+ mask=crop,
81
+ area=int(crop.sum()),
82
+ y=float(y0 + ys.mean()),
83
+ x=float(x0 + xs.mean()),
84
+ )
85
+ )
86
+
87
+ return records, skipped
88
+
89
+
90
+ def _overlap_pair(lhs_id: int, rhs_id: int) -> tuple[int, int]:
91
+ return (max(lhs_id, rhs_id), min(lhs_id, rhs_id))
92
+
93
+
94
+ def _generate_node_id(index: int, time: int, max_segments: int) -> int:
95
+ return index + (time + 1) * max_segments
96
+
97
+
98
+ def _best_iou_assignments(
99
+ records: list[_MaskRecord],
100
+ candidates: dict[int, tuple[tuple[int, int, int, int], np.ndarray, int]],
101
+ ) -> dict[int, int]:
102
+ pairs: list[tuple[float, int, int, int, int]] = []
103
+ for record_index, record in enumerate(records):
104
+ for candidate_id, (candidate_bbox, candidate_mask, _ndim) in candidates.items():
105
+ iou = raw_iou(record.bbox, record.mask, candidate_bbox, candidate_mask)
106
+ pairs.append((-iou, record_index, int(candidate_id), record.cell_id, record.t))
107
+
108
+ pairs.sort()
109
+ assigned_records: set[int] = set()
110
+ assigned_candidates: set[int] = set()
111
+ assignments: dict[int, int] = {}
112
+ for _neg_iou, record_index, candidate_id, _cell_id, _t in pairs:
113
+ if record_index in assigned_records or candidate_id in assigned_candidates:
114
+ continue
115
+ assignments[record_index] = candidate_id
116
+ assigned_records.add(record_index)
117
+ assigned_candidates.add(candidate_id)
118
+ if len(assigned_records) == min(len(records), len(candidates)):
119
+ break
120
+ return assignments
121
+
122
+
123
+ def inject_validated_nodes(
124
+ working_dir: str | Path,
125
+ validated_tracks: dict[int, set[int]],
126
+ tracked_labels: np.ndarray,
127
+ cfg: TrackingConfig,
128
+ ) -> ValidationInjectionReport:
129
+ """Replace best-matching candidates with validated masks as fixed REAL nodes.
130
+
131
+ The best same-frame candidate by raw IoU is updated in place so its
132
+ hierarchy placement and temporal links are preserved. If no candidate is
133
+ available for a validated mask, a reserved REAL node is inserted instead.
134
+ Other candidates in the same frame that overlap a validated mask are marked
135
+ FAKE and paired with the REAL node in OverlapDB.
136
+ """
137
+ import sqlalchemy as sqla
138
+ from sqlalchemy.orm import Session
139
+ from ultrack.core.database import NodeDB, OverlapDB, VarAnnotation
140
+
141
+ records, skipped = _validated_mask_records(validated_tracks, tracked_labels)
142
+ if not records:
143
+ return ValidationInjectionReport(
144
+ inserted=0,
145
+ skipped_missing=len(skipped),
146
+ skipped=skipped,
147
+ faked=0,
148
+ overlaps_added=0,
149
+ )
150
+
151
+ working_dir = Path(working_dir)
152
+ engine = sqla.create_engine(f"sqlite:///{working_dir / 'data.db'}")
153
+ inserted = 0
154
+ faked_ids: set[int] = set()
155
+ overlap_pairs: set[tuple[int, int]] = set()
156
+ real_node_ids: set[int] = set()
157
+
158
+ with Session(engine) as session:
159
+ sample_node = session.query(NodeDB.pickle).limit(1).scalar()
160
+ fallback_ndim = node_pickle_ndim(sample_node) if sample_node is not None else 2
161
+ next_t_node_id: dict[int, int] = {}
162
+ for t in {record.t for record in records}:
163
+ max_t_node_id = (
164
+ session.query(sqla.func.max(NodeDB.t_node_id))
165
+ .where(NodeDB.t == t)
166
+ .scalar()
167
+ )
168
+ next_t_node_id[t] = int(max_t_node_id or 0) + 1
169
+
170
+ records_by_t: dict[int, list[tuple[int, _MaskRecord]]] = {}
171
+ for index, record in enumerate(records):
172
+ records_by_t.setdefault(record.t, []).append((index, record))
173
+
174
+ for t, indexed_records in records_by_t.items():
175
+ candidate_rows = (
176
+ session.query(NodeDB.id, NodeDB.pickle)
177
+ .where(NodeDB.t == t)
178
+ .where(NodeDB.t_hier_id != 0)
179
+ .all()
180
+ )
181
+ candidates: dict[int, tuple[tuple[int, int, int, int], np.ndarray, int]] = {}
182
+ for candidate_id, candidate_node in candidate_rows:
183
+ candidate_bbox, candidate_mask = node_bbox_and_mask(
184
+ int(candidate_id), candidate_node
185
+ )
186
+ candidates[int(candidate_id)] = (
187
+ candidate_bbox,
188
+ candidate_mask,
189
+ node_pickle_ndim(candidate_node),
190
+ )
191
+
192
+ frame_records = [record for _index, record in indexed_records]
193
+ local_assignments = _best_iou_assignments(frame_records, candidates)
194
+ real_node_by_local_index: dict[int, int] = {}
195
+ matched_candidate_ids: set[int] = set()
196
+
197
+ for local_index, record in enumerate(frame_records):
198
+ bbox_arr = np.asarray(record.bbox, dtype=np.int32)
199
+ candidate_id = local_assignments.get(local_index)
200
+ if candidate_id is None:
201
+ node_ndim = next(
202
+ (ndim for _bbox, _mask, ndim in candidates.values()),
203
+ fallback_ndim,
204
+ )
205
+ t_node_id = next_t_node_id[record.t]
206
+ next_t_node_id[record.t] += 1
207
+ node_id = _generate_node_id(
208
+ t_node_id, record.t, cfg.max_segments_per_time
209
+ )
210
+ session.add(
211
+ NodeDB(
212
+ id=node_id,
213
+ t=record.t,
214
+ t_node_id=t_node_id,
215
+ t_hier_id=0,
216
+ z=0,
217
+ y=record.y,
218
+ x=record.x,
219
+ area=record.area,
220
+ pickle=make_node_pickle(
221
+ record.t,
222
+ record.mask,
223
+ bbox_arr,
224
+ node_id,
225
+ ndim=node_ndim,
226
+ ),
227
+ node_prob=1.0,
228
+ node_annot=VarAnnotation.REAL,
229
+ )
230
+ )
231
+ inserted += 1
232
+ real_node_ids.add(node_id)
233
+ real_node_by_local_index[local_index] = node_id
234
+ continue
235
+
236
+ node_ndim = candidates[candidate_id][2]
237
+ session.query(NodeDB).where(NodeDB.id == candidate_id).update(
238
+ {
239
+ NodeDB.y: record.y,
240
+ NodeDB.x: record.x,
241
+ NodeDB.area: record.area,
242
+ NodeDB.pickle: make_node_pickle(
243
+ record.t,
244
+ record.mask,
245
+ bbox_arr,
246
+ candidate_id,
247
+ ndim=node_ndim,
248
+ ),
249
+ NodeDB.node_prob: 1.0,
250
+ NodeDB.node_annot: VarAnnotation.REAL,
251
+ },
252
+ synchronize_session=False,
253
+ )
254
+ inserted += 1
255
+ real_node_ids.add(candidate_id)
256
+ matched_candidate_ids.add(candidate_id)
257
+ real_node_by_local_index[local_index] = candidate_id
258
+
259
+ for local_index, record in enumerate(frame_records):
260
+ real_node_id = real_node_by_local_index[local_index]
261
+ for candidate_id, (
262
+ candidate_bbox,
263
+ candidate_mask,
264
+ _ndim,
265
+ ) in candidates.items():
266
+ if candidate_id in matched_candidate_ids:
267
+ continue
268
+ if not intersects(
269
+ record.bbox, record.mask, candidate_bbox, candidate_mask
270
+ ):
271
+ continue
272
+ faked_ids.add(candidate_id)
273
+ overlap_pairs.add(_overlap_pair(real_node_id, candidate_id))
274
+
275
+ if faked_ids:
276
+ session.query(NodeDB).where(NodeDB.id.in_(faked_ids)).update(
277
+ {NodeDB.node_annot: VarAnnotation.FAKE},
278
+ synchronize_session=False,
279
+ )
280
+
281
+ existing_pairs: set[tuple[int, int]] = set()
282
+ if real_node_ids:
283
+ session.query(OverlapDB).where(
284
+ sqla.or_(
285
+ OverlapDB.node_id.in_(real_node_ids),
286
+ OverlapDB.ancestor_id.in_(real_node_ids),
287
+ )
288
+ ).delete(synchronize_session=False)
289
+ if overlap_pairs:
290
+ node_ids = {pair[0] for pair in overlap_pairs} | {pair[1] for pair in overlap_pairs}
291
+ existing_pairs = {
292
+ (int(row.node_id), int(row.ancestor_id))
293
+ for row in session.query(OverlapDB)
294
+ .where(
295
+ sqla.or_(
296
+ OverlapDB.node_id.in_(node_ids),
297
+ OverlapDB.ancestor_id.in_(node_ids),
298
+ )
299
+ )
300
+ .all()
301
+ }
302
+ for node_id, ancestor_id in sorted(overlap_pairs - existing_pairs):
303
+ session.add(OverlapDB(node_id=node_id, ancestor_id=ancestor_id))
304
+
305
+ session.commit()
306
+
307
+ engine.dispose()
308
+ added_pairs = len(overlap_pairs - existing_pairs)
309
+ return ValidationInjectionReport(
310
+ inserted=inserted,
311
+ skipped_missing=len(skipped),
312
+ skipped=skipped,
313
+ faked=len(faked_ids),
314
+ overlaps_added=added_pairs,
315
+ )