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,133 @@
|
|
|
1
|
+
"""Export selected NodeDB nodes to a (T, Z, Y, X) tracked labelmap."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import shutil
|
|
5
|
+
import tempfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import tifffile
|
|
10
|
+
|
|
11
|
+
from itasc.core.tiff import imwrite_grayscale
|
|
12
|
+
from itasc.tracking_ultrack.config import TrackingConfig
|
|
13
|
+
from itasc.tracking_ultrack.corrections import (
|
|
14
|
+
Correction,
|
|
15
|
+
apply_post_solve_corrections,
|
|
16
|
+
corrections_from_validated_tracks,
|
|
17
|
+
)
|
|
18
|
+
from itasc.tracking_ultrack.solve import database_has_annotations
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _build_export_config(cfg: TrackingConfig, working_dir: Path):
|
|
22
|
+
from itasc.tracking_ultrack.ingest import _build_ultrack_config
|
|
23
|
+
|
|
24
|
+
return _build_ultrack_config(cfg, working_dir)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _materialize_labels(labels) -> np.ndarray:
|
|
28
|
+
if hasattr(labels, "compute"):
|
|
29
|
+
labels = labels.compute()
|
|
30
|
+
labels = np.asarray(labels, dtype=np.uint32)
|
|
31
|
+
if labels.ndim == 4 and labels.shape[1] == 1:
|
|
32
|
+
labels = labels[:, 0]
|
|
33
|
+
return labels
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def export_tracked_labels(
|
|
37
|
+
working_dir: str | Path,
|
|
38
|
+
cfg: TrackingConfig,
|
|
39
|
+
output_path: str | Path,
|
|
40
|
+
*,
|
|
41
|
+
corrections: list[Correction] | None = None,
|
|
42
|
+
validated_tracks: dict[int, set[int]] | None = None,
|
|
43
|
+
tracked_labels: np.ndarray | None = None,
|
|
44
|
+
preserve_validated_ids: bool | None = None,
|
|
45
|
+
) -> np.ndarray:
|
|
46
|
+
"""Write ``tracked_labels.tif`` and return the (T, [Z,] Y, X) array."""
|
|
47
|
+
wd = Path(working_dir)
|
|
48
|
+
output_path = Path(output_path)
|
|
49
|
+
annotated_db = database_has_annotations(wd)
|
|
50
|
+
if preserve_validated_ids is None:
|
|
51
|
+
preserve_validated_ids = annotated_db
|
|
52
|
+
if corrections is None and validated_tracks and tracked_labels is not None:
|
|
53
|
+
corrections = corrections_from_validated_tracks(
|
|
54
|
+
validated_tracks,
|
|
55
|
+
np.asarray(tracked_labels, dtype=np.uint32),
|
|
56
|
+
)
|
|
57
|
+
if preserve_validated_ids and not corrections and (
|
|
58
|
+
not validated_tracks or tracked_labels is None
|
|
59
|
+
):
|
|
60
|
+
raise ValueError(
|
|
61
|
+
"Validated-aware export requires validated tracks and tracked labels."
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
labels = _export_tracked_labels_raw(wd, cfg, output_path)
|
|
65
|
+
if corrections:
|
|
66
|
+
if tracked_labels is None:
|
|
67
|
+
tracked_labels = np.zeros_like(labels, dtype=np.uint32)
|
|
68
|
+
labels, _report = apply_post_solve_corrections(
|
|
69
|
+
labels,
|
|
70
|
+
corrections,
|
|
71
|
+
np.asarray(tracked_labels, dtype=np.uint32),
|
|
72
|
+
cfg,
|
|
73
|
+
working_dir=wd,
|
|
74
|
+
)
|
|
75
|
+
imwrite_grayscale(output_path, labels, compression="zlib")
|
|
76
|
+
return labels
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _export_tracked_labels_raw(
|
|
80
|
+
working_dir: str | Path,
|
|
81
|
+
cfg: TrackingConfig,
|
|
82
|
+
output_path: str | Path,
|
|
83
|
+
) -> np.ndarray:
|
|
84
|
+
wd = Path(working_dir)
|
|
85
|
+
output_path = Path(output_path)
|
|
86
|
+
ultrack_cfg = _build_export_config(cfg, wd)
|
|
87
|
+
|
|
88
|
+
# Prefer public track export: tracks_to_zarr rasterizes each segment with
|
|
89
|
+
# its track_id, while label-export helpers may expose per-frame segment IDs.
|
|
90
|
+
# The import is the capability probe — only a missing API (ImportError /
|
|
91
|
+
# AttributeError) should fall through to the next strategy. A runtime failure
|
|
92
|
+
# (corrupt DB, OOM) must propagate rather than silently degrade the output.
|
|
93
|
+
try:
|
|
94
|
+
from ultrack.core.export import to_tracks_layer, tracks_to_zarr # type: ignore[import]
|
|
95
|
+
except (ImportError, AttributeError):
|
|
96
|
+
pass
|
|
97
|
+
else:
|
|
98
|
+
tracks_df, _graph = to_tracks_layer(ultrack_cfg)
|
|
99
|
+
labels = _materialize_labels(
|
|
100
|
+
tracks_to_zarr(ultrack_cfg, tracks_df, overwrite=True)
|
|
101
|
+
)
|
|
102
|
+
imwrite_grayscale(output_path, labels, compression="zlib")
|
|
103
|
+
return labels
|
|
104
|
+
|
|
105
|
+
# Try the modern to_labels API next (returns dask or numpy)
|
|
106
|
+
try:
|
|
107
|
+
from ultrack.core.export.labels import to_labels # type: ignore[import]
|
|
108
|
+
except (ImportError, AttributeError):
|
|
109
|
+
pass
|
|
110
|
+
else:
|
|
111
|
+
labels = _materialize_labels(to_labels(ultrack_cfg))
|
|
112
|
+
imwrite_grayscale(output_path, labels, compression="zlib")
|
|
113
|
+
return labels
|
|
114
|
+
|
|
115
|
+
# Fallback: CTC export → stack TIFFs
|
|
116
|
+
tmpdir = Path(tempfile.mkdtemp(prefix="ultrack_ctc_"))
|
|
117
|
+
try:
|
|
118
|
+
from ultrack.core.export.ctc import to_ctc # type: ignore[import]
|
|
119
|
+
|
|
120
|
+
to_ctc(tmpdir, ultrack_cfg, overwrite=True)
|
|
121
|
+
mask_files = sorted(tmpdir.rglob("mask*.tif"))
|
|
122
|
+
if not mask_files:
|
|
123
|
+
mask_files = sorted(tmpdir.rglob("man_track*.tif"))
|
|
124
|
+
if not mask_files:
|
|
125
|
+
mask_files = sorted(tmpdir.rglob("*.tif"))
|
|
126
|
+
if not mask_files:
|
|
127
|
+
raise RuntimeError("CTC export produced no mask files.")
|
|
128
|
+
frames = [tifffile.imread(str(f)) for f in mask_files]
|
|
129
|
+
labels = _materialize_labels(np.stack(frames, axis=0))
|
|
130
|
+
imwrite_grayscale(output_path, labels, compression="zlib")
|
|
131
|
+
return labels
|
|
132
|
+
finally:
|
|
133
|
+
shutil.rmtree(tmpdir, ignore_errors=True)
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from itasc.tracking_ultrack._node_geometry import (
|
|
10
|
+
match_mask_to_node,
|
|
11
|
+
node_bbox_and_mask,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
# How many ranked candidates the gallery API surfaces per direction.
|
|
15
|
+
_EXTEND_CANDIDATE_LIMIT = 6
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class ExtendAssignment:
|
|
20
|
+
cell_id: int
|
|
21
|
+
candidate_label: int # ultrack node id of the linked candidate
|
|
22
|
+
candidate_partition: int
|
|
23
|
+
mask_2d: np.ndarray # bool, full-frame (Y, X)
|
|
24
|
+
bbox: tuple[int, int, int, int]
|
|
25
|
+
weight: float # LinkDB.weight (association score); higher = better
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class ExtendResult:
|
|
30
|
+
target_frame: int
|
|
31
|
+
candidate_label: int
|
|
32
|
+
candidate_partition: int
|
|
33
|
+
mask_2d: np.ndarray # bool, full-frame (Y, X)
|
|
34
|
+
bbox: tuple[int, int, int, int] # (y0, x0, y1, x1)
|
|
35
|
+
weight: float # LinkDB.weight of the chosen link
|
|
36
|
+
assignments: tuple[ExtendAssignment, ...] = ()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class ExtendCandidates:
|
|
41
|
+
"""Ranked extend shortlist for one direction, best-first, for a gallery.
|
|
42
|
+
|
|
43
|
+
``target_frame`` is the adjacent frame the candidates live on (one step in
|
|
44
|
+
``direction`` from the source); ``assignments`` carries each candidate's
|
|
45
|
+
full-frame mask, bbox and link weight so the UI can render clickable
|
|
46
|
+
thumbnails.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
target_frame: int
|
|
50
|
+
assignments: tuple[ExtendAssignment, ...] = ()
|
|
51
|
+
|
|
52
|
+
def is_empty(self) -> bool:
|
|
53
|
+
return not self.assignments
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _ranked_link_targets(
|
|
57
|
+
session, node_id: int, direction: Literal["forward", "backward"]
|
|
58
|
+
) -> list[tuple[int, float]]:
|
|
59
|
+
"""Linked candidate node ids in the adjacent frame, best (highest weight) first.
|
|
60
|
+
|
|
61
|
+
``LinkDB`` edges point forward in time (``source_id`` at ``t``, ``target_id``
|
|
62
|
+
at ``t + 1``). Forward extension reads rows where the matched node is the
|
|
63
|
+
source and collects the targets; backward extension reads rows where it is
|
|
64
|
+
the target and collects the sources. A NULL weight is treated as ``1.0``.
|
|
65
|
+
"""
|
|
66
|
+
from ultrack.core.database import LinkDB
|
|
67
|
+
|
|
68
|
+
if direction == "forward":
|
|
69
|
+
rows = (
|
|
70
|
+
session.query(LinkDB.target_id, LinkDB.weight)
|
|
71
|
+
.filter(LinkDB.source_id == node_id)
|
|
72
|
+
.all()
|
|
73
|
+
)
|
|
74
|
+
else:
|
|
75
|
+
rows = (
|
|
76
|
+
session.query(LinkDB.source_id, LinkDB.weight)
|
|
77
|
+
.filter(LinkDB.target_id == node_id)
|
|
78
|
+
.all()
|
|
79
|
+
)
|
|
80
|
+
ranked = [
|
|
81
|
+
(int(nid), float(weight) if weight is not None else 1.0)
|
|
82
|
+
for nid, weight in rows
|
|
83
|
+
]
|
|
84
|
+
ranked.sort(key=lambda item: item[1], reverse=True)
|
|
85
|
+
return ranked
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _extend_assignments(
|
|
89
|
+
*,
|
|
90
|
+
source_id: int,
|
|
91
|
+
source_frame: int,
|
|
92
|
+
direction: Literal["forward", "backward"],
|
|
93
|
+
tracked_labels: np.ndarray,
|
|
94
|
+
db_path: Path,
|
|
95
|
+
limit: int,
|
|
96
|
+
) -> tuple[int, list[ExtendAssignment]]:
|
|
97
|
+
"""Shared core: ``(target_frame, ranked assignments)`` for the adjacent frame.
|
|
98
|
+
|
|
99
|
+
Matches the source label's mask to an Ultrack node, then walks the
|
|
100
|
+
precomputed ``LinkDB`` edges from that node into the adjacent frame, ranked
|
|
101
|
+
by association weight. Only the top ``limit`` linked candidates' masks are
|
|
102
|
+
deserialized. Returns an empty assignment list (with the computed
|
|
103
|
+
``target_frame``) when the DB is missing, the target frame is out of range,
|
|
104
|
+
the source cell is absent, the source mask matches no node, or the matched
|
|
105
|
+
node has no link in the requested direction.
|
|
106
|
+
"""
|
|
107
|
+
target_frame = source_frame + (1 if direction == "forward" else -1)
|
|
108
|
+
if not Path(db_path).exists():
|
|
109
|
+
return target_frame, []
|
|
110
|
+
if target_frame < 0 or target_frame >= tracked_labels.shape[0]:
|
|
111
|
+
return target_frame, []
|
|
112
|
+
|
|
113
|
+
source_mask = tracked_labels[source_frame] == source_id
|
|
114
|
+
if not source_mask.any():
|
|
115
|
+
return target_frame, []
|
|
116
|
+
|
|
117
|
+
frame_shape = tracked_labels.shape[1:]
|
|
118
|
+
|
|
119
|
+
import sqlalchemy as sqla
|
|
120
|
+
from sqlalchemy.orm import Session
|
|
121
|
+
from ultrack.core.database import NodeDB
|
|
122
|
+
|
|
123
|
+
engine = sqla.create_engine(
|
|
124
|
+
f"sqlite:///{db_path}", connect_args={"check_same_thread": False}
|
|
125
|
+
)
|
|
126
|
+
assignments: list[ExtendAssignment] = []
|
|
127
|
+
try:
|
|
128
|
+
with Session(engine) as session:
|
|
129
|
+
matched = match_mask_to_node(session, source_frame, source_mask)
|
|
130
|
+
if matched is None:
|
|
131
|
+
return target_frame, []
|
|
132
|
+
matched_id, _bbox, _mask = matched
|
|
133
|
+
|
|
134
|
+
ranked = _ranked_link_targets(session, matched_id, direction)
|
|
135
|
+
if not ranked:
|
|
136
|
+
return target_frame, []
|
|
137
|
+
|
|
138
|
+
# Load only the linked candidates' masks (a handful), restricted to
|
|
139
|
+
# the adjacent frame as a safety net against malformed links.
|
|
140
|
+
node_rows = {
|
|
141
|
+
int(node.id): node
|
|
142
|
+
for node in session.query(NodeDB)
|
|
143
|
+
.filter(
|
|
144
|
+
NodeDB.t == target_frame,
|
|
145
|
+
NodeDB.id.in_([nid for nid, _ in ranked]),
|
|
146
|
+
)
|
|
147
|
+
.all()
|
|
148
|
+
}
|
|
149
|
+
for nid, weight in ranked:
|
|
150
|
+
node = node_rows.get(nid)
|
|
151
|
+
if node is None:
|
|
152
|
+
continue
|
|
153
|
+
try:
|
|
154
|
+
(y0, x0, y1, x1), mask_crop = node_bbox_and_mask(nid, node.pickle)
|
|
155
|
+
except Exception:
|
|
156
|
+
continue
|
|
157
|
+
if mask_crop.shape != (y1 - y0, x1 - x0):
|
|
158
|
+
continue
|
|
159
|
+
full_mask = np.zeros(frame_shape, dtype=bool)
|
|
160
|
+
full_mask[y0:y1, x0:x1] = mask_crop
|
|
161
|
+
if not full_mask.any():
|
|
162
|
+
continue
|
|
163
|
+
assignments.append(
|
|
164
|
+
ExtendAssignment(
|
|
165
|
+
cell_id=source_id,
|
|
166
|
+
candidate_label=nid,
|
|
167
|
+
candidate_partition=0,
|
|
168
|
+
mask_2d=full_mask,
|
|
169
|
+
bbox=(y0, x0, y1, x1),
|
|
170
|
+
weight=weight,
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
if limit and len(assignments) >= limit:
|
|
174
|
+
break
|
|
175
|
+
finally:
|
|
176
|
+
engine.dispose()
|
|
177
|
+
return target_frame, assignments
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def extend_track_from_db(
|
|
181
|
+
*,
|
|
182
|
+
source_id: int,
|
|
183
|
+
source_frame: int,
|
|
184
|
+
direction: Literal["forward", "backward"],
|
|
185
|
+
tracked_labels: np.ndarray, # (T, Y, X) uint32
|
|
186
|
+
db_path: Path,
|
|
187
|
+
) -> ExtendResult | None:
|
|
188
|
+
"""Extend a track to the adjacent frame using ultrack ``LinkDB`` candidates.
|
|
189
|
+
|
|
190
|
+
The source label's mask is matched to an Ultrack node; the highest-weight
|
|
191
|
+
``LinkDB`` edge into the adjacent frame picks the continuation. Returns
|
|
192
|
+
``None`` when the DB is missing, the target frame is out of range, the
|
|
193
|
+
source cell is absent, or the matched node has no link in ``direction`` (the
|
|
194
|
+
caller should show a local status message and leave the frame untouched).
|
|
195
|
+
"""
|
|
196
|
+
target_frame, assignments = _extend_assignments(
|
|
197
|
+
source_id=source_id,
|
|
198
|
+
source_frame=source_frame,
|
|
199
|
+
direction=direction,
|
|
200
|
+
tracked_labels=tracked_labels,
|
|
201
|
+
db_path=db_path,
|
|
202
|
+
limit=1,
|
|
203
|
+
)
|
|
204
|
+
if not assignments:
|
|
205
|
+
return None
|
|
206
|
+
best = assignments[0]
|
|
207
|
+
return ExtendResult(
|
|
208
|
+
target_frame=target_frame,
|
|
209
|
+
candidate_label=best.candidate_label,
|
|
210
|
+
candidate_partition=best.candidate_partition,
|
|
211
|
+
mask_2d=best.mask_2d,
|
|
212
|
+
bbox=best.bbox,
|
|
213
|
+
weight=best.weight,
|
|
214
|
+
assignments=(best,),
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def list_extend_candidates(
|
|
219
|
+
*,
|
|
220
|
+
source_id: int,
|
|
221
|
+
source_frame: int,
|
|
222
|
+
direction: Literal["forward", "backward"],
|
|
223
|
+
tracked_labels: np.ndarray, # (T, Y, X) uint32
|
|
224
|
+
db_path: Path,
|
|
225
|
+
limit: int = _EXTEND_CANDIDATE_LIMIT,
|
|
226
|
+
) -> ExtendCandidates:
|
|
227
|
+
"""Ranked extend candidates at the adjacent frame, best-first, for a gallery.
|
|
228
|
+
|
|
229
|
+
Same matching as :func:`extend_track_from_db`, but returns the whole ranked
|
|
230
|
+
shortlist of ``LinkDB`` candidates (each with its full-frame mask) instead of
|
|
231
|
+
only the winner, so the correction UI can render clickable candidate
|
|
232
|
+
thumbnails. Returns an empty :class:`ExtendCandidates` (still carrying
|
|
233
|
+
``target_frame``) when there is no DB, target frame, matched node or link.
|
|
234
|
+
"""
|
|
235
|
+
target_frame, assignments = _extend_assignments(
|
|
236
|
+
source_id=source_id,
|
|
237
|
+
source_frame=source_frame,
|
|
238
|
+
direction=direction,
|
|
239
|
+
tracked_labels=tracked_labels,
|
|
240
|
+
db_path=db_path,
|
|
241
|
+
limit=limit,
|
|
242
|
+
)
|
|
243
|
+
return ExtendCandidates(target_frame=target_frame, assignments=tuple(assignments))
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Ultrack configuration helpers for ITASC tracking."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from itasc.tracking_ultrack.config import TrackingConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _signed_power_transform(values, *, power: float, bias: float):
|
|
12
|
+
values = np.asarray(values)
|
|
13
|
+
return np.sign(values) * np.power(np.abs(values), power) + bias
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _install_signed_power_transform(ultrack_cfg) -> None:
|
|
17
|
+
tracking_cfg = getattr(ultrack_cfg, "tracking_config", None)
|
|
18
|
+
if tracking_cfg is None:
|
|
19
|
+
return
|
|
20
|
+
link_function = getattr(tracking_cfg, "link_function", None)
|
|
21
|
+
if getattr(link_function, "value", link_function) != "power":
|
|
22
|
+
return
|
|
23
|
+
if not hasattr(tracking_cfg, "dict"):
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
base_cls = tracking_cfg.__class__
|
|
27
|
+
|
|
28
|
+
class _ITASCSignedPowerTrackingConfig(base_cls):
|
|
29
|
+
@property
|
|
30
|
+
def apply_link_function(self):
|
|
31
|
+
return lambda values: _signed_power_transform(
|
|
32
|
+
values,
|
|
33
|
+
power=self.power,
|
|
34
|
+
bias=self.bias,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
ultrack_cfg.tracking_config = _ITASCSignedPowerTrackingConfig.parse_obj(
|
|
38
|
+
tracking_cfg.dict()
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _build_ultrack_config(cfg: TrackingConfig, working_dir: Path):
|
|
43
|
+
from ultrack.config import MainConfig
|
|
44
|
+
from ultrack.config.segmentationconfig import NAME_TO_WS_HIER
|
|
45
|
+
|
|
46
|
+
ultrack_cfg = MainConfig(
|
|
47
|
+
data={"working_dir": str(working_dir)},
|
|
48
|
+
linking={
|
|
49
|
+
"max_distance": cfg.max_distance,
|
|
50
|
+
"max_neighbors": cfg.max_neighbors,
|
|
51
|
+
"distance_weight": cfg.distance_weight,
|
|
52
|
+
"n_workers": cfg.link_n_workers,
|
|
53
|
+
},
|
|
54
|
+
tracking={
|
|
55
|
+
"solver_name": _select_solver(),
|
|
56
|
+
"appear_weight": cfg.appear_weight,
|
|
57
|
+
"disappear_weight": cfg.disappear_weight,
|
|
58
|
+
"division_weight": cfg.division_weight,
|
|
59
|
+
"link_function": cfg.link_function,
|
|
60
|
+
"power": cfg.power,
|
|
61
|
+
"bias": cfg.bias,
|
|
62
|
+
"solution_gap": cfg.solution_gap,
|
|
63
|
+
"time_limit": cfg.time_limit,
|
|
64
|
+
"window_size": cfg.window_size if cfg.window_size > 0 else None,
|
|
65
|
+
},
|
|
66
|
+
)
|
|
67
|
+
_install_signed_power_transform(ultrack_cfg)
|
|
68
|
+
sc = ultrack_cfg.segmentation_config
|
|
69
|
+
sc.min_area = cfg.seg_min_area
|
|
70
|
+
sc.max_area = cfg.seg_max_area
|
|
71
|
+
sc.threshold = cfg.seg_foreground_threshold
|
|
72
|
+
sc.min_frontier = cfg.seg_min_frontier
|
|
73
|
+
sc.ws_hierarchy = NAME_TO_WS_HIER[cfg.seg_ws_hierarchy]
|
|
74
|
+
sc.n_workers = cfg.seg_n_workers
|
|
75
|
+
return ultrack_cfg
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _select_solver() -> str:
|
|
79
|
+
try:
|
|
80
|
+
import gurobipy # noqa: F401
|
|
81
|
+
return "GUROBI"
|
|
82
|
+
except ImportError:
|
|
83
|
+
return "CBC"
|