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,60 @@
|
|
|
1
|
+
"""Segmentation package — shared utilities and submodule re-exports."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from itasc.segmentation.contour_filtering import (
|
|
5
|
+
ContourFilterParams,
|
|
6
|
+
compute_filtered_contour_maps,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
from itasc.segmentation.cell_label_icm import (
|
|
10
|
+
CellICMState,
|
|
11
|
+
CellLabelICMParams,
|
|
12
|
+
assemble_cost_field,
|
|
13
|
+
balance_strength_to_weights,
|
|
14
|
+
commit_labels,
|
|
15
|
+
initialize_icm,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from itasc.segmentation.cell_divergence_segmentation import (
|
|
19
|
+
CellDivergenceParams,
|
|
20
|
+
CellDivergenceResult,
|
|
21
|
+
CellForegroundResult,
|
|
22
|
+
clean_and_smooth_contours,
|
|
23
|
+
compute_cell_foreground,
|
|
24
|
+
segment_cells_divergence,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
from itasc.segmentation.nucleus_segmentation import (
|
|
28
|
+
CancelledError,
|
|
29
|
+
_fill_and_close_labels,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
from itasc.segmentation.lineage import (
|
|
33
|
+
LineageModel,
|
|
34
|
+
TrackLane,
|
|
35
|
+
TrackSegment,
|
|
36
|
+
build_lineage,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"CancelledError",
|
|
41
|
+
"CellDivergenceParams",
|
|
42
|
+
"CellDivergenceResult",
|
|
43
|
+
"CellForegroundResult",
|
|
44
|
+
"CellICMState",
|
|
45
|
+
"CellLabelICMParams",
|
|
46
|
+
"ContourFilterParams",
|
|
47
|
+
"LineageModel",
|
|
48
|
+
"TrackLane",
|
|
49
|
+
"TrackSegment",
|
|
50
|
+
"assemble_cost_field",
|
|
51
|
+
"balance_strength_to_weights",
|
|
52
|
+
"build_lineage",
|
|
53
|
+
"clean_and_smooth_contours",
|
|
54
|
+
"commit_labels",
|
|
55
|
+
"compute_cell_foreground",
|
|
56
|
+
"compute_filtered_contour_maps",
|
|
57
|
+
"initialize_icm",
|
|
58
|
+
"segment_cells_divergence",
|
|
59
|
+
"_fill_and_close_labels",
|
|
60
|
+
]
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
"""Unary-only cell segmentation from cached divergence maps.
|
|
2
|
+
|
|
3
|
+
Chains the simplified cell pipeline validated in
|
|
4
|
+
``scripts/experiment_divergence_icm.py`` into a single, Qt-free helper so the
|
|
5
|
+
cell widget can drive both its live single-frame preview and its full-stack run
|
|
6
|
+
through the same code path:
|
|
7
|
+
|
|
8
|
+
1. Map cleanup — local-mean residual + threshold on foreground and
|
|
9
|
+
contours (the nucleus/atom ``residual`` scheme applied
|
|
10
|
+
symmetrically to both maps).
|
|
11
|
+
2. Temporal smooth — bidirectional signal-adaptive EMA on the cleaned
|
|
12
|
+
contours (full-stack only; needs the whole movie).
|
|
13
|
+
3. Foreground mask — ``(foreground_clean > fg_threshold) | (nucleus > 0)``.
|
|
14
|
+
4. Segmentation — unary-only geodesic Voronoi: ``initialize_icm`` assigns
|
|
15
|
+
each foreground pixel to its nearest nucleus seed
|
|
16
|
+
through the contour-aware cost field (per-pixel argmin
|
|
17
|
+
of the unary).
|
|
18
|
+
|
|
19
|
+
The divergence maps themselves (``cell_contours.tif`` + ``cell_foreground.tif``)
|
|
20
|
+
are produced upstream by ``DivergenceMapsWidget``; this helper only consumes
|
|
21
|
+
them.
|
|
22
|
+
|
|
23
|
+
The returned :class:`CellDivergenceResult` carries every intermediate plus the
|
|
24
|
+
weighted cost field, so the widget can drop each into a preview layer without
|
|
25
|
+
recomputing.
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
from collections.abc import Callable
|
|
31
|
+
|
|
32
|
+
import numpy as np
|
|
33
|
+
|
|
34
|
+
from itasc.segmentation.cell_label_icm import (
|
|
35
|
+
CellLabelICMParams,
|
|
36
|
+
assemble_cost_field,
|
|
37
|
+
balance_strength_to_weights,
|
|
38
|
+
initialize_icm,
|
|
39
|
+
)
|
|
40
|
+
from itasc.segmentation.contour_filtering import contour_memory_filter
|
|
41
|
+
from itasc.core.imageops import residual
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"CellDivergenceParams",
|
|
45
|
+
"CellDivergenceResult",
|
|
46
|
+
"CellForegroundResult",
|
|
47
|
+
"clean_and_smooth_contours",
|
|
48
|
+
"compute_cell_foreground",
|
|
49
|
+
"segment_cells_divergence",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class CellDivergenceParams:
|
|
55
|
+
"""Parameters for the unary-only divergence cell pipeline.
|
|
56
|
+
|
|
57
|
+
Defaults match the values that held across pos00/pos01 in the prototype.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
# ── Map cleanup (same trio per map as the nucleus/atom widget) ──────────
|
|
61
|
+
fg_window: int = 51
|
|
62
|
+
"""Local-mean window for the foreground residual (px, forced odd)."""
|
|
63
|
+
fg_strength: float = 0.0
|
|
64
|
+
"""Foreground residual strength: 0 = raw sigmoid, 1 = full subtraction."""
|
|
65
|
+
fg_threshold: float = 0.1
|
|
66
|
+
"""Cleaned-foreground cutoff producing the fill mask (sigmoid scale)."""
|
|
67
|
+
contour_window: int = 51
|
|
68
|
+
"""Local-mean window for the contour residual (px, forced odd)."""
|
|
69
|
+
contour_strength: float = 1.0
|
|
70
|
+
"""Contour residual strength: 0 = raw, 1 = full local-mean subtraction."""
|
|
71
|
+
contour_threshold: float = 0.0
|
|
72
|
+
"""Noise floor on the normalized contour [0, 1]; below → 0."""
|
|
73
|
+
contour_norm_pct: float = 99.0
|
|
74
|
+
"""Percentile of the positive contour signal mapped to 1.0 in [0, 1]."""
|
|
75
|
+
|
|
76
|
+
# ── Temporal smoothing ──────────────────────────────────────────────────
|
|
77
|
+
memory_tau: float = 0.0
|
|
78
|
+
"""EMA crossover (~the contour value you call "weak"). 0 = off."""
|
|
79
|
+
memory_floor: float = 0.01
|
|
80
|
+
"""Minimum per-frame alpha; ghost half-life (~69 frames @ 0.01)."""
|
|
81
|
+
|
|
82
|
+
# ── Segmentation ────────────────────────────────────────────────────────
|
|
83
|
+
balance: float = 0.98
|
|
84
|
+
"""Contour↔foreground split ``r`` in ``[0, 1]`` (``1`` = pure contour).
|
|
85
|
+
See :func:`balance_strength_to_weights`."""
|
|
86
|
+
feature_strength: float = 100.0
|
|
87
|
+
"""Overall feature weight ``s >= 0`` relative to the base cost of 1."""
|
|
88
|
+
n_workers: int = 4
|
|
89
|
+
"""Parallel workers for geodesic computation (compute only)."""
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass
|
|
93
|
+
class CellDivergenceResult:
|
|
94
|
+
"""All pipeline intermediates plus the final labels.
|
|
95
|
+
|
|
96
|
+
Arrays are ``(T, Y, X)`` for a full-stack run and ``(Y, X)`` for a
|
|
97
|
+
single-frame (``frame`` given) run.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
foreground_raw: np.ndarray
|
|
101
|
+
"""Raw input foreground map (sigmoid)."""
|
|
102
|
+
foreground_clean: np.ndarray
|
|
103
|
+
"""Foreground after residual cleanup (sigmoid scale)."""
|
|
104
|
+
contours_raw: np.ndarray
|
|
105
|
+
"""Raw input contour map (positive divergence)."""
|
|
106
|
+
contours_clean: np.ndarray
|
|
107
|
+
"""Contours after residual + normalize + floor (and temporal smoothing
|
|
108
|
+
for a full-stack run)."""
|
|
109
|
+
foreground_mask: np.ndarray
|
|
110
|
+
"""Fill territory: ``(foreground_clean > fg_threshold) | (nucleus > 0)``."""
|
|
111
|
+
cost_field: np.ndarray
|
|
112
|
+
"""Weighted geodesic cost over the mask; ``inf`` outside."""
|
|
113
|
+
labels: np.ndarray | None
|
|
114
|
+
"""Cell labels — the unary argmin (tracked nucleus IDs). ``None`` when the
|
|
115
|
+
geodesic label assignment was skipped (``with_labels=False``)."""
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass
|
|
119
|
+
class CellForegroundResult:
|
|
120
|
+
"""Foreground-path intermediates — pipeline stages 1 + 3, foreground only.
|
|
121
|
+
|
|
122
|
+
Arrays are ``(T, Y, X)`` for a full-stack run and ``(Y, X)`` for a
|
|
123
|
+
single-frame (``frame`` given) run.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
foreground_raw: np.ndarray
|
|
127
|
+
"""Raw input foreground map (sigmoid), clipped to ``[0, 1]``."""
|
|
128
|
+
foreground_clean: np.ndarray
|
|
129
|
+
"""Foreground after residual cleanup (sigmoid scale)."""
|
|
130
|
+
foreground_mask: np.ndarray
|
|
131
|
+
"""Fill territory: ``(foreground_clean > fg_threshold)`` unioned with the
|
|
132
|
+
nucleus seeds when ``nuc`` is supplied."""
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _robust_normalize_01(contours: np.ndarray, pct: float) -> np.ndarray:
|
|
136
|
+
"""Scale positive-divergence contours into [0, 1] by a high percentile.
|
|
137
|
+
|
|
138
|
+
Raw positive divergence has an arbitrary positive scale, so dividing by a
|
|
139
|
+
high percentile of the nonzero signal keeps ``alpha`` interpretable and
|
|
140
|
+
comparable across frames/datasets. Mirrors the prototype.
|
|
141
|
+
"""
|
|
142
|
+
c = np.clip(np.asarray(contours, dtype=np.float32), 0.0, None)
|
|
143
|
+
nz = c[c > 0]
|
|
144
|
+
if nz.size == 0:
|
|
145
|
+
return c
|
|
146
|
+
hi = float(np.percentile(nz, pct))
|
|
147
|
+
if hi <= 0.0:
|
|
148
|
+
return c
|
|
149
|
+
return np.clip(c / hi, 0.0, 1.0).astype(np.float32)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _clean_foreground(fg: np.ndarray, params: CellDivergenceParams) -> np.ndarray:
|
|
153
|
+
"""Per-frame local-mean residual; stays on the native sigmoid scale.
|
|
154
|
+
|
|
155
|
+
``fg_strength=0`` makes ``residual`` a no-op (returns the raw, non-negative
|
|
156
|
+
map), so the baseline reproduces the raw-sigmoid foreground.
|
|
157
|
+
"""
|
|
158
|
+
return np.stack([
|
|
159
|
+
residual(fg[t], params.fg_window, params.fg_strength)
|
|
160
|
+
for t in range(fg.shape[0])
|
|
161
|
+
]).astype(np.float32)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _clean_contours(contours: np.ndarray, params: CellDivergenceParams) -> np.ndarray:
|
|
165
|
+
"""Per-frame residual → [0, 1] normalize → noise floor."""
|
|
166
|
+
cleaned = np.stack([
|
|
167
|
+
residual(contours[t], params.contour_window, params.contour_strength)
|
|
168
|
+
for t in range(contours.shape[0])
|
|
169
|
+
]).astype(np.float32)
|
|
170
|
+
cleaned = _robust_normalize_01(cleaned, params.contour_norm_pct)
|
|
171
|
+
if params.contour_threshold > 0.0:
|
|
172
|
+
cleaned = np.where(cleaned < params.contour_threshold, 0.0, cleaned)
|
|
173
|
+
return cleaned.astype(np.float32)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def clean_and_smooth_contours(
|
|
177
|
+
contours: np.ndarray, params: CellDivergenceParams
|
|
178
|
+
) -> np.ndarray:
|
|
179
|
+
"""Full-stack contour cleanup + temporal smoothing — pipeline stages 1+2.
|
|
180
|
+
|
|
181
|
+
Returns the ``(T, Y, X)`` cleaned (residual → global-percentile normalize →
|
|
182
|
+
floor) and, when ``memory_tau > 0`` and there is more than one frame,
|
|
183
|
+
temporally smoothed contour stack — exactly the ``contours_clean`` the
|
|
184
|
+
full run feeds the segmenter.
|
|
185
|
+
|
|
186
|
+
The widget's live preview computes this once over the whole movie, caches it,
|
|
187
|
+
and slices the current frame back into :func:`segment_cells_divergence` via
|
|
188
|
+
``contours_clean_override`` so the previewed cost field / labels for a frame
|
|
189
|
+
match the full run (which the per-frame path cannot, since both the global
|
|
190
|
+
percentile and the bidirectional EMA need every frame).
|
|
191
|
+
"""
|
|
192
|
+
contours = _to_tyx(contours, np.float32)
|
|
193
|
+
cleaned = _clean_contours(contours, params)
|
|
194
|
+
if params.memory_tau > 0.0 and cleaned.shape[0] > 1:
|
|
195
|
+
cleaned = contour_memory_filter(
|
|
196
|
+
cleaned, tau=params.memory_tau, floor=params.memory_floor,
|
|
197
|
+
)
|
|
198
|
+
return cleaned.astype(np.float32)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _to_tyx(arr: np.ndarray, dtype) -> np.ndarray:
|
|
202
|
+
a = np.asarray(arr, dtype=dtype)
|
|
203
|
+
if a.ndim == 4 and a.shape[1] == 1:
|
|
204
|
+
a = a[:, 0]
|
|
205
|
+
if a.ndim == 2:
|
|
206
|
+
a = a[np.newaxis]
|
|
207
|
+
return a
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def compute_cell_foreground(
|
|
211
|
+
foreground: np.ndarray,
|
|
212
|
+
params: CellDivergenceParams,
|
|
213
|
+
nuc: np.ndarray | None = None,
|
|
214
|
+
*,
|
|
215
|
+
frame: int | None = None,
|
|
216
|
+
) -> CellForegroundResult:
|
|
217
|
+
"""Foreground cleanup + fill mask, independent of contours and the geodesic.
|
|
218
|
+
|
|
219
|
+
Drives the cell widget's dedicated foreground-tuning stage. It runs exactly
|
|
220
|
+
the foreground half of :func:`segment_cells_divergence` — the local-mean
|
|
221
|
+
residual cleanup (stage 1) and the fill-mask threshold (stage 3) — with no
|
|
222
|
+
contour cleanup, cost field, or geodesic walk, so foreground tuning stays
|
|
223
|
+
cheap and does not require the contour map.
|
|
224
|
+
|
|
225
|
+
``nuc`` is optional: when given, its seeds are unioned into the mask exactly
|
|
226
|
+
as the full run does (``(foreground_clean > fg_threshold) | (nuc > 0)``), so
|
|
227
|
+
the previewed mask matches the territory segmentation will fill; when
|
|
228
|
+
``None`` the mask is the bare threshold, letting the foreground be tuned
|
|
229
|
+
before nucleus tracking exists.
|
|
230
|
+
|
|
231
|
+
``frame`` selects a single frame (2-D result); ``None`` processes the whole
|
|
232
|
+
stack (3-D result).
|
|
233
|
+
"""
|
|
234
|
+
foreground = _to_tyx(foreground, np.float32)
|
|
235
|
+
nuc_tyx = _to_tyx(nuc, np.uint32) if nuc is not None else None
|
|
236
|
+
|
|
237
|
+
single = frame is not None
|
|
238
|
+
if single:
|
|
239
|
+
t = max(0, min(int(frame), foreground.shape[0] - 1))
|
|
240
|
+
foreground = foreground[t:t + 1]
|
|
241
|
+
if nuc_tyx is not None:
|
|
242
|
+
tn = max(0, min(int(frame), nuc_tyx.shape[0] - 1))
|
|
243
|
+
nuc_tyx = nuc_tyx[tn:tn + 1]
|
|
244
|
+
|
|
245
|
+
foreground_raw = np.clip(foreground, 0.0, 1.0).astype(np.float32)
|
|
246
|
+
foreground_clean = _clean_foreground(foreground, params)
|
|
247
|
+
foreground_mask = foreground_clean > params.fg_threshold
|
|
248
|
+
if nuc_tyx is not None:
|
|
249
|
+
n = min(foreground_mask.shape[0], nuc_tyx.shape[0])
|
|
250
|
+
foreground_raw = foreground_raw[:n]
|
|
251
|
+
foreground_clean = foreground_clean[:n]
|
|
252
|
+
foreground_mask = foreground_mask[:n] | (nuc_tyx[:n] > 0)
|
|
253
|
+
|
|
254
|
+
if single:
|
|
255
|
+
return CellForegroundResult(
|
|
256
|
+
foreground_raw=foreground_raw[0],
|
|
257
|
+
foreground_clean=foreground_clean[0],
|
|
258
|
+
foreground_mask=foreground_mask[0],
|
|
259
|
+
)
|
|
260
|
+
return CellForegroundResult(
|
|
261
|
+
foreground_raw=foreground_raw,
|
|
262
|
+
foreground_clean=foreground_clean,
|
|
263
|
+
foreground_mask=foreground_mask,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def segment_cells_divergence(
|
|
268
|
+
contours: np.ndarray,
|
|
269
|
+
foreground: np.ndarray,
|
|
270
|
+
nuc: np.ndarray,
|
|
271
|
+
params: CellDivergenceParams,
|
|
272
|
+
*,
|
|
273
|
+
frame: int | None = None,
|
|
274
|
+
with_labels: bool = True,
|
|
275
|
+
contours_clean_override: np.ndarray | None = None,
|
|
276
|
+
foreground_mask_override: np.ndarray | None = None,
|
|
277
|
+
progress_cb: Callable[[str], None] | None = None,
|
|
278
|
+
) -> CellDivergenceResult:
|
|
279
|
+
"""Run the unary-only divergence pipeline and return all intermediates.
|
|
280
|
+
|
|
281
|
+
Parameters
|
|
282
|
+
----------
|
|
283
|
+
contours, foreground : (T, Y, X) float
|
|
284
|
+
Cached divergence maps (raw positive divergence and the sigmoid
|
|
285
|
+
foreground, respectively).
|
|
286
|
+
nuc : (T, Y, X) integer
|
|
287
|
+
Tracked nucleus seeds.
|
|
288
|
+
params : CellDivergenceParams
|
|
289
|
+
frame : int, optional
|
|
290
|
+
When given, only that frame is processed and **temporal smoothing is
|
|
291
|
+
skipped** (it needs the whole stack); the result arrays are 2-D. When
|
|
292
|
+
``None``, the whole stack is processed including temporal smoothing.
|
|
293
|
+
with_labels : bool, default True
|
|
294
|
+
When ``False``, the geodesic Voronoi label assignment (the single
|
|
295
|
+
slowest step) is skipped: every cleanup intermediate plus the weighted
|
|
296
|
+
cost field is still returned, but ``result.labels`` is ``None``. The
|
|
297
|
+
live preview uses this to stay responsive — the cost field already
|
|
298
|
+
explains every boundary the labels would land on.
|
|
299
|
+
contours_clean_override : (Y, X) float, optional
|
|
300
|
+
Single-frame-only. When given (with ``frame`` set), this pre-cleaned —
|
|
301
|
+
and, when temporal smoothing is on, pre-smoothed — contour frame is used
|
|
302
|
+
as ``contours_clean`` instead of re-running the per-frame cleanup. The
|
|
303
|
+
widget passes a frame sliced from :func:`clean_and_smooth_contours` so
|
|
304
|
+
the single-frame cost field / labels match the full run exactly (the
|
|
305
|
+
per-frame path cannot, as it lacks the whole-movie percentile and EMA).
|
|
306
|
+
Ignored when ``frame`` is ``None``.
|
|
307
|
+
foreground_mask_override : ndarray of bool, optional
|
|
308
|
+
Precomputed fill mask to segment inside, replacing the internally derived
|
|
309
|
+
``(foreground_clean > fg_threshold) | (nuc > 0)``. The cell widget's
|
|
310
|
+
Foreground stage writes this mask to disk and the Segmentation stage
|
|
311
|
+
feeds it back here, so the territory the walk fills is exactly the one
|
|
312
|
+
tuned in the Foreground stage (not re-derived from the current knobs).
|
|
313
|
+
``(T, Y, X)`` for a full-stack run, ``(Y, X)`` (or a ``(T, Y, X)`` stack
|
|
314
|
+
sliced at ``frame``) for a single-frame run. ``foreground_clean`` is
|
|
315
|
+
still computed for the cost field's foreground score regardless.
|
|
316
|
+
progress_cb : callable, optional
|
|
317
|
+
Receives short status strings.
|
|
318
|
+
|
|
319
|
+
Returns
|
|
320
|
+
-------
|
|
321
|
+
CellDivergenceResult
|
|
322
|
+
"""
|
|
323
|
+
_report = progress_cb or (lambda _msg: None)
|
|
324
|
+
|
|
325
|
+
contours = _to_tyx(contours, np.float32)
|
|
326
|
+
foreground = _to_tyx(foreground, np.float32)
|
|
327
|
+
nuc = _to_tyx(nuc, np.uint32)
|
|
328
|
+
|
|
329
|
+
T = min(len(contours), len(foreground), len(nuc))
|
|
330
|
+
contours, foreground, nuc = contours[:T], foreground[:T], nuc[:T]
|
|
331
|
+
|
|
332
|
+
single = frame is not None
|
|
333
|
+
if single:
|
|
334
|
+
t = max(0, min(int(frame), T - 1))
|
|
335
|
+
contours = contours[t:t + 1]
|
|
336
|
+
foreground = foreground[t:t + 1]
|
|
337
|
+
nuc = nuc[t:t + 1]
|
|
338
|
+
|
|
339
|
+
contours_raw = contours.copy()
|
|
340
|
+
foreground_raw = np.clip(foreground, 0.0, 1.0).astype(np.float32)
|
|
341
|
+
|
|
342
|
+
# ── 1. Map cleanup ──────────────────────────────────────────────────────
|
|
343
|
+
_report("Cleaning maps…")
|
|
344
|
+
foreground_clean = _clean_foreground(foreground, params)
|
|
345
|
+
|
|
346
|
+
if single and contours_clean_override is not None:
|
|
347
|
+
# Caller supplied the already cleaned (+ smoothed) frame — use it
|
|
348
|
+
# verbatim so the single-frame result matches the full run for this
|
|
349
|
+
# frame. Stage 2 is folded into the override and skipped here.
|
|
350
|
+
override = np.asarray(contours_clean_override, dtype=np.float32)
|
|
351
|
+
if override.shape != contours.shape[1:]:
|
|
352
|
+
raise ValueError(
|
|
353
|
+
"contours_clean_override shape "
|
|
354
|
+
f"{override.shape} does not match frame shape {contours.shape[1:]}"
|
|
355
|
+
)
|
|
356
|
+
contours_clean = override[np.newaxis]
|
|
357
|
+
else:
|
|
358
|
+
contours_clean = _clean_contours(contours, params)
|
|
359
|
+
# ── 2. Temporal contour smoothing (full-stack only) ─────────────────
|
|
360
|
+
if not single and params.memory_tau > 0.0 and contours_clean.shape[0] > 1:
|
|
361
|
+
_report(f"Temporal contour smoothing (τ={params.memory_tau})…")
|
|
362
|
+
contours_clean = contour_memory_filter(
|
|
363
|
+
contours_clean, tau=params.memory_tau, floor=params.memory_floor,
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
# ── 3. Foreground mask ──────────────────────────────────────────────────
|
|
367
|
+
if foreground_mask_override is not None:
|
|
368
|
+
# The Foreground stage already produced the fill territory; segment
|
|
369
|
+
# inside exactly that mask rather than re-deriving it from the knobs.
|
|
370
|
+
mask = np.asarray(foreground_mask_override)
|
|
371
|
+
if single and mask.ndim == 3:
|
|
372
|
+
mask = mask[t]
|
|
373
|
+
if mask.ndim == 2:
|
|
374
|
+
mask = mask[np.newaxis]
|
|
375
|
+
mask = mask[:foreground_clean.shape[0]].astype(bool)
|
|
376
|
+
if mask.shape != foreground_clean.shape:
|
|
377
|
+
raise ValueError(
|
|
378
|
+
"foreground_mask_override shape "
|
|
379
|
+
f"{mask.shape} does not match {foreground_clean.shape}"
|
|
380
|
+
)
|
|
381
|
+
foreground_mask = mask
|
|
382
|
+
else:
|
|
383
|
+
foreground_mask = (foreground_clean > params.fg_threshold) | (nuc > 0)
|
|
384
|
+
|
|
385
|
+
# ── Weighted cost field (same construction the solver traverses) ────────
|
|
386
|
+
# Cheap (`1 + α·contour + γ·(1 − fg)`); built first so it is available even
|
|
387
|
+
# when the geodesic label assignment below is skipped.
|
|
388
|
+
alpha, gamma = balance_strength_to_weights(
|
|
389
|
+
params.balance, params.feature_strength
|
|
390
|
+
)
|
|
391
|
+
cost_field = np.stack([
|
|
392
|
+
assemble_cost_field(
|
|
393
|
+
contours_clean[i], foreground_mask[i],
|
|
394
|
+
alpha, foreground_clean[i], gamma,
|
|
395
|
+
)
|
|
396
|
+
for i in range(contours_clean.shape[0])
|
|
397
|
+
]).astype(np.float32)
|
|
398
|
+
|
|
399
|
+
# ── 4. Unary-only segmentation (the slow geodesic walk) ─────────────────
|
|
400
|
+
labels: np.ndarray | None = None
|
|
401
|
+
if with_labels:
|
|
402
|
+
_report("Segmenting (unary geodesic Voronoi)…")
|
|
403
|
+
icm_params = CellLabelICMParams(
|
|
404
|
+
balance=params.balance,
|
|
405
|
+
feature_strength=params.feature_strength,
|
|
406
|
+
n_workers=1 if single else max(1, params.n_workers),
|
|
407
|
+
)
|
|
408
|
+
_state, labels = initialize_icm(
|
|
409
|
+
nuc, foreground_mask, contours_clean, icm_params,
|
|
410
|
+
foreground_scores=foreground_clean,
|
|
411
|
+
progress_cb=lambda m: _report(str(m)),
|
|
412
|
+
)
|
|
413
|
+
labels = labels.astype(np.uint32, copy=False)
|
|
414
|
+
|
|
415
|
+
if single:
|
|
416
|
+
return CellDivergenceResult(
|
|
417
|
+
foreground_raw=foreground_raw[0],
|
|
418
|
+
foreground_clean=foreground_clean[0],
|
|
419
|
+
contours_raw=contours_raw[0],
|
|
420
|
+
contours_clean=contours_clean[0],
|
|
421
|
+
foreground_mask=foreground_mask[0],
|
|
422
|
+
cost_field=cost_field[0],
|
|
423
|
+
labels=None if labels is None else labels[0],
|
|
424
|
+
)
|
|
425
|
+
return CellDivergenceResult(
|
|
426
|
+
foreground_raw=foreground_raw,
|
|
427
|
+
foreground_clean=foreground_clean,
|
|
428
|
+
contours_raw=contours_raw,
|
|
429
|
+
contours_clean=contours_clean,
|
|
430
|
+
foreground_mask=foreground_mask,
|
|
431
|
+
cost_field=cost_field,
|
|
432
|
+
labels=labels,
|
|
433
|
+
)
|