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,286 @@
|
|
|
1
|
+
"""Linking step: wire NodeDB into LinkDB.
|
|
2
|
+
|
|
3
|
+
Default mode uses Ultrack's built-in linker.
|
|
4
|
+
Shape mode uses the custom shape-scoring linker.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import math
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from collections.abc import Generator
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
from itasc.tracking_ultrack import scoring
|
|
15
|
+
from itasc.tracking_ultrack.config import TrackingConfig
|
|
16
|
+
from itasc.tracking_ultrack.ingest import _build_ultrack_config
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def run_linking(
|
|
20
|
+
working_dir: str | Path,
|
|
21
|
+
cfg: TrackingConfig,
|
|
22
|
+
*,
|
|
23
|
+
overwrite: bool = True,
|
|
24
|
+
) -> Generator[tuple[int, int, str], None, None]:
|
|
25
|
+
"""Run the linking step, yielding (step, total, label) progress tuples."""
|
|
26
|
+
if cfg.linking_mode == "shape":
|
|
27
|
+
yield from _run_shape_linking(working_dir, cfg, overwrite=overwrite)
|
|
28
|
+
return
|
|
29
|
+
if cfg.linking_mode != "default":
|
|
30
|
+
raise ValueError(f"Unknown linking_mode={cfg.linking_mode!r}")
|
|
31
|
+
|
|
32
|
+
total = 3
|
|
33
|
+
wd = Path(working_dir)
|
|
34
|
+
ultrack_cfg = _build_ultrack_config(cfg, wd)
|
|
35
|
+
|
|
36
|
+
from ultrack.core.linking.processing import link
|
|
37
|
+
from ultrack.core.linking.utils import clear_linking_data
|
|
38
|
+
|
|
39
|
+
if overwrite:
|
|
40
|
+
yield (0, total, "Clearing existing links…")
|
|
41
|
+
clear_linking_data(ultrack_cfg.data_config.database_path)
|
|
42
|
+
else:
|
|
43
|
+
yield (0, total, "Skipping link clear (overwrite=False)…")
|
|
44
|
+
|
|
45
|
+
yield (1, total, "Running Ultrack linker…")
|
|
46
|
+
link(ultrack_cfg, overwrite=False)
|
|
47
|
+
|
|
48
|
+
yield (total, total, "Linking done.")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
# Shape-scoring linking
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
def _node_coords_centroid(node, ndim: int | None = None) -> tuple[np.ndarray, np.ndarray] | None:
|
|
56
|
+
"""Return (absolute-pixel coords, centroid) for node, or None on failure.
|
|
57
|
+
|
|
58
|
+
ndim defaults to the mask's own dimensionality.
|
|
59
|
+
"""
|
|
60
|
+
mask = getattr(node, "mask", None)
|
|
61
|
+
if mask is None:
|
|
62
|
+
return None
|
|
63
|
+
mask = np.asarray(mask, dtype=bool)
|
|
64
|
+
if mask.ndim == 0 or not mask.any():
|
|
65
|
+
return None
|
|
66
|
+
if ndim is None:
|
|
67
|
+
ndim = int(mask.ndim)
|
|
68
|
+
|
|
69
|
+
# Resolve origin
|
|
70
|
+
origin = None
|
|
71
|
+
for attr in ("origin", "offset", "bbox_start", "bbox_min", "start"):
|
|
72
|
+
value = getattr(node, attr, None)
|
|
73
|
+
if value is None:
|
|
74
|
+
continue
|
|
75
|
+
arr = np.asarray(value, dtype=np.float32).reshape(-1)
|
|
76
|
+
if arr.size >= ndim:
|
|
77
|
+
origin = arr[-ndim:]
|
|
78
|
+
break
|
|
79
|
+
if origin is None:
|
|
80
|
+
bbox = getattr(node, "bbox", None)
|
|
81
|
+
if bbox is None:
|
|
82
|
+
return None
|
|
83
|
+
if isinstance(bbox, tuple) and bbox and all(hasattr(item, "start") for item in bbox):
|
|
84
|
+
starts = [0.0 if item.start is None else float(item.start) for item in bbox]
|
|
85
|
+
arr = np.asarray(starts, dtype=np.float32).reshape(-1)
|
|
86
|
+
if arr.size >= ndim:
|
|
87
|
+
origin = arr[-ndim:]
|
|
88
|
+
if origin is None:
|
|
89
|
+
arr = np.asarray(bbox, dtype=np.float32).reshape(-1)
|
|
90
|
+
# bbox is [min_0, ..., min_k, max_0, ..., max_k]; take first half for mins
|
|
91
|
+
if arr.size >= ndim * 2 and arr.size % 2 == 0:
|
|
92
|
+
half = arr.size // 2
|
|
93
|
+
origin = arr[:half][-ndim:]
|
|
94
|
+
elif arr.size >= ndim:
|
|
95
|
+
origin = arr[-ndim:]
|
|
96
|
+
if origin is None:
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
coords = np.argwhere(mask).astype(np.float32) + origin
|
|
100
|
+
centroid = np.asarray(node.centroid, dtype=np.float32).reshape(-1)[-ndim:]
|
|
101
|
+
return coords, centroid
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _shape_pair_score(
|
|
105
|
+
src_coords: np.ndarray,
|
|
106
|
+
src_centroid: np.ndarray,
|
|
107
|
+
src_area: float,
|
|
108
|
+
tgt_coords: np.ndarray,
|
|
109
|
+
tgt_centroid: np.ndarray,
|
|
110
|
+
tgt_area: float,
|
|
111
|
+
distance: float,
|
|
112
|
+
cfg: TrackingConfig,
|
|
113
|
+
) -> float | None:
|
|
114
|
+
"""Gate and score one source→target pair under shape mode.
|
|
115
|
+
|
|
116
|
+
Returns None when the pair fails the area-ratio or IoU threshold,
|
|
117
|
+
otherwise returns the similarity score.
|
|
118
|
+
"""
|
|
119
|
+
if src_area <= 0 or tgt_area <= 0:
|
|
120
|
+
return None
|
|
121
|
+
area_ratio = min(src_area, tgt_area) / max(src_area, tgt_area)
|
|
122
|
+
if area_ratio < cfg.min_area_ratio:
|
|
123
|
+
return None
|
|
124
|
+
iou = scoring.centroid_corrected_iou_from_coords(
|
|
125
|
+
src_coords, src_centroid, tgt_coords, tgt_centroid
|
|
126
|
+
)
|
|
127
|
+
if iou < cfg.min_link_iou:
|
|
128
|
+
return None
|
|
129
|
+
return scoring.similarity_score(
|
|
130
|
+
area_ratio=area_ratio,
|
|
131
|
+
centroid_corrected_iou=iou,
|
|
132
|
+
distance=distance,
|
|
133
|
+
area_weight=cfg.area_weight,
|
|
134
|
+
iou_weight=cfg.iou_weight,
|
|
135
|
+
distance_weight=cfg.distance_weight,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def compute_edge_weight(
|
|
140
|
+
source_node,
|
|
141
|
+
target_node,
|
|
142
|
+
distance: float,
|
|
143
|
+
cfg: TrackingConfig,
|
|
144
|
+
) -> float | None:
|
|
145
|
+
"""Per-pair edge weight matching the active linker mode.
|
|
146
|
+
|
|
147
|
+
Returns None when shape mode filters the pair out. Default mode never
|
|
148
|
+
filters and always returns a float.
|
|
149
|
+
"""
|
|
150
|
+
if cfg.linking_mode == "shape":
|
|
151
|
+
result = _node_coords_centroid(source_node)
|
|
152
|
+
if result is None:
|
|
153
|
+
return None
|
|
154
|
+
src_coords, src_centroid = result
|
|
155
|
+
|
|
156
|
+
result = _node_coords_centroid(target_node)
|
|
157
|
+
if result is None:
|
|
158
|
+
return None
|
|
159
|
+
tgt_coords, tgt_centroid = result
|
|
160
|
+
|
|
161
|
+
src_area = float(getattr(source_node, "area", len(src_coords)))
|
|
162
|
+
tgt_area = float(getattr(target_node, "area", len(tgt_coords)))
|
|
163
|
+
return _shape_pair_score(
|
|
164
|
+
src_coords, src_centroid, src_area,
|
|
165
|
+
tgt_coords, tgt_centroid, tgt_area,
|
|
166
|
+
distance, cfg,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
if cfg.linking_mode != "default":
|
|
170
|
+
raise ValueError(f"Unknown linking_mode={cfg.linking_mode!r}")
|
|
171
|
+
iou = float(source_node.IoU(target_node))
|
|
172
|
+
return iou - cfg.distance_weight * float(distance)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _run_shape_linking(
|
|
176
|
+
working_dir: str | Path,
|
|
177
|
+
cfg: TrackingConfig,
|
|
178
|
+
*,
|
|
179
|
+
overwrite: bool = True,
|
|
180
|
+
) -> Generator[tuple[int, int, str], None, None]:
|
|
181
|
+
total = 4
|
|
182
|
+
wd = Path(working_dir)
|
|
183
|
+
ultrack_cfg = _build_ultrack_config(cfg, wd)
|
|
184
|
+
|
|
185
|
+
from ultrack.core.database import NodeDB, maximum_time_from_database
|
|
186
|
+
from ultrack.core.linking.processing import add_links
|
|
187
|
+
from ultrack.core.linking.utils import clear_linking_data
|
|
188
|
+
from scipy.spatial import KDTree
|
|
189
|
+
import sqlalchemy as sqla
|
|
190
|
+
from sqlalchemy.orm import Session
|
|
191
|
+
|
|
192
|
+
if overwrite:
|
|
193
|
+
yield (0, total, "Clearing existing links…")
|
|
194
|
+
clear_linking_data(ultrack_cfg.data_config.database_path)
|
|
195
|
+
else:
|
|
196
|
+
yield (0, total, "Skipping link clear (overwrite=False)…")
|
|
197
|
+
|
|
198
|
+
engine = sqla.create_engine(ultrack_cfg.data_config.database_path)
|
|
199
|
+
max_t = int(maximum_time_from_database(ultrack_cfg.data_config))
|
|
200
|
+
if max_t <= 0:
|
|
201
|
+
yield (total, total, "No frames; skipping shape linking.")
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
yield (1, total, "Computing shape-weighted links…")
|
|
205
|
+
total_links = 0
|
|
206
|
+
|
|
207
|
+
with Session(engine) as session:
|
|
208
|
+
for time in range(max_t):
|
|
209
|
+
source_nodes = [n for (n,) in session.query(NodeDB.pickle).where(NodeDB.t == time)]
|
|
210
|
+
target_rows = list(
|
|
211
|
+
session.query(NodeDB.pickle, NodeDB.z_shift, NodeDB.y_shift, NodeDB.x_shift)
|
|
212
|
+
.where(NodeDB.t == time + 1)
|
|
213
|
+
)
|
|
214
|
+
if not source_nodes or not target_rows:
|
|
215
|
+
continue
|
|
216
|
+
target_nodes = [r[0] for r in target_rows]
|
|
217
|
+
|
|
218
|
+
source_pos = np.array([n.centroid for n in source_nodes], dtype=np.float32)
|
|
219
|
+
target_pos = np.array([n.centroid for n in target_nodes], dtype=np.float32)
|
|
220
|
+
for i, row in enumerate(target_rows):
|
|
221
|
+
shift = np.asarray(row[1:], dtype=np.float32)
|
|
222
|
+
target_pos[i] += shift[-target_pos.shape[1]:]
|
|
223
|
+
|
|
224
|
+
tree = KDTree(source_pos)
|
|
225
|
+
k = min(len(source_nodes), max(1, 2 * cfg.max_neighbors))
|
|
226
|
+
dists, neigh_idx = tree.query(target_pos, k=k, distance_upper_bound=cfg.max_distance)
|
|
227
|
+
if dists.ndim == 1:
|
|
228
|
+
dists, neigh_idx = dists[:, None], neigh_idx[:, None]
|
|
229
|
+
|
|
230
|
+
# Cache (coords, centroid, area) per source node id
|
|
231
|
+
src_cache: dict[int, tuple[np.ndarray, np.ndarray, float] | None] = {}
|
|
232
|
+
|
|
233
|
+
src_ids, tgt_ids, weights = [], [], []
|
|
234
|
+
for ti, (dist_row, ni_row) in enumerate(zip(dists, neigh_idx)):
|
|
235
|
+
target = target_nodes[ti]
|
|
236
|
+
candidates = []
|
|
237
|
+
for dist, si in zip(dist_row, ni_row):
|
|
238
|
+
if si >= len(source_nodes) or not np.isfinite(dist):
|
|
239
|
+
continue
|
|
240
|
+
source = source_nodes[si]
|
|
241
|
+
sid = int(source.id)
|
|
242
|
+
if sid not in src_cache:
|
|
243
|
+
result = _node_coords_centroid(source)
|
|
244
|
+
if result is None:
|
|
245
|
+
src_cache[sid] = None
|
|
246
|
+
else:
|
|
247
|
+
src_coords, src_centroid = result
|
|
248
|
+
src_area = float(getattr(source, "area", len(src_coords)))
|
|
249
|
+
src_cache[sid] = (src_coords, src_centroid, src_area)
|
|
250
|
+
cached = src_cache[sid]
|
|
251
|
+
if cached is None:
|
|
252
|
+
continue
|
|
253
|
+
src_coords, src_centroid, src_area = cached
|
|
254
|
+
|
|
255
|
+
tgt_result = _node_coords_centroid(target)
|
|
256
|
+
if tgt_result is None:
|
|
257
|
+
continue
|
|
258
|
+
tgt_coords, tgt_centroid = tgt_result
|
|
259
|
+
tgt_area = float(getattr(target, "area", len(tgt_coords)))
|
|
260
|
+
|
|
261
|
+
w = _shape_pair_score(
|
|
262
|
+
src_coords, src_centroid, src_area,
|
|
263
|
+
tgt_coords, tgt_centroid, tgt_area,
|
|
264
|
+
float(dist), cfg,
|
|
265
|
+
)
|
|
266
|
+
if w is None:
|
|
267
|
+
continue
|
|
268
|
+
candidates.append((w, sid, int(target.id)))
|
|
269
|
+
|
|
270
|
+
candidates.sort(reverse=True)
|
|
271
|
+
for w, sid, tid in candidates[:cfg.max_neighbors]:
|
|
272
|
+
src_ids.append(sid)
|
|
273
|
+
tgt_ids.append(tid)
|
|
274
|
+
weights.append(w)
|
|
275
|
+
|
|
276
|
+
if src_ids:
|
|
277
|
+
add_links(ultrack_cfg, src_ids, tgt_ids, weights)
|
|
278
|
+
total_links += len(src_ids)
|
|
279
|
+
|
|
280
|
+
yield (
|
|
281
|
+
1 + int(math.floor((time + 1) / max(max_t, 1) * 2)),
|
|
282
|
+
total,
|
|
283
|
+
f"Linked t={time + 1}/{max_t} ({len(src_ids)} edges)",
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
yield (total, total, f"Shape linking done ({total_links} total edges).")
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Source-node provenance queries for Ultrack databases.
|
|
2
|
+
|
|
3
|
+
Reads the optional ``itasc_ultrack_source_nodes`` table that records
|
|
4
|
+
which merged node ids originated from which source segmentation. Used by
|
|
5
|
+
:mod:`itasc.tracking_ultrack.db_query` to scope hierarchy-cut-state
|
|
6
|
+
queries to a single source; returns empty results when the table is absent.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import sqlalchemy as sqla
|
|
13
|
+
|
|
14
|
+
SOURCE_NODE_TABLE = "itasc_ultrack_source_nodes"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _source_table_exists(conn) -> bool:
|
|
18
|
+
return (
|
|
19
|
+
conn.execute(
|
|
20
|
+
sqla.text(
|
|
21
|
+
"SELECT 1 FROM sqlite_master "
|
|
22
|
+
"WHERE type='table' AND name=:table_name"
|
|
23
|
+
),
|
|
24
|
+
{"table_name": SOURCE_NODE_TABLE},
|
|
25
|
+
).first()
|
|
26
|
+
is not None
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def query_source_node_ids(db_path: str | Path, source_index: int) -> tuple[int, ...]:
|
|
31
|
+
"""Return merged node ids that originated from ``source_index``."""
|
|
32
|
+
engine = sqla.create_engine(f"sqlite:///{Path(db_path)}")
|
|
33
|
+
try:
|
|
34
|
+
with engine.connect() as conn:
|
|
35
|
+
if not _source_table_exists(conn):
|
|
36
|
+
return ()
|
|
37
|
+
rows = conn.execute(
|
|
38
|
+
sqla.text(
|
|
39
|
+
f"SELECT node_id FROM {SOURCE_NODE_TABLE} "
|
|
40
|
+
"WHERE source_index=:source_index ORDER BY node_id"
|
|
41
|
+
),
|
|
42
|
+
{"source_index": int(source_index)},
|
|
43
|
+
).all()
|
|
44
|
+
return tuple(int(row[0]) for row in rows)
|
|
45
|
+
finally:
|
|
46
|
+
engine.dispose()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
name: itasc-tracking
|
|
2
|
+
display_name: ITASC Nucleus Tracking
|
|
3
|
+
contributions:
|
|
4
|
+
commands:
|
|
5
|
+
- id: itasc-tracking.widget
|
|
6
|
+
title: "Ultrack Segment + Track"
|
|
7
|
+
python_name: itasc.napari.nucleus_workflow_widget:make_nucleus_tracking_widget
|
|
8
|
+
widgets:
|
|
9
|
+
- command: itasc-tracking.widget
|
|
10
|
+
display_name: "Ultrack Segment + Track"
|