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
itasc/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Top-level package metadata for ITASC.
|
|
2
|
+
|
|
3
|
+
The supported top-level API is intentionally small while ITASC is under
|
|
4
|
+
active research development. Import concrete workflow functions from their
|
|
5
|
+
subpackage modules.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
__version__ = version("itasc")
|
|
13
|
+
except PackageNotFoundError:
|
|
14
|
+
__version__ = "0.2.0"
|
|
15
|
+
|
|
16
|
+
__all__ = ["__version__"]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Cellpose stage + standalone segment/track tool.
|
|
2
|
+
|
|
3
|
+
Two roles share this package:
|
|
4
|
+
|
|
5
|
+
* **Integrated app** — the local Cellpose-SAM runner (``cellpose_runner``) and
|
|
6
|
+
the divergence-derived foreground/contour maps (``build_divergence_maps``) that
|
|
7
|
+
the orchestrator's in-app stage produces for nucleus tracking
|
|
8
|
+
(``itasc-tracking``) and cell segmentation.
|
|
9
|
+
* **Standalone ``itasc-cellpose`` tool** — ``native_masks`` captures the
|
|
10
|
+
Cellpose native masks the runner otherwise discards, and ``track_laptrack``
|
|
11
|
+
links them across time, giving a self-contained "segment then track" product.
|
|
12
|
+
|
|
13
|
+
The Cellpose model is the optional ``[cellpose]`` extra and ``laptrack`` is the
|
|
14
|
+
``[laptrack]`` extra; both are imported lazily, so importing this package does
|
|
15
|
+
not require either.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from itasc.cellpose import cellpose_runner, native_masks, track_laptrack
|
|
20
|
+
from itasc.cellpose.divergence_maps import (
|
|
21
|
+
DivergenceMapsReport,
|
|
22
|
+
build_divergence_maps,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"DivergenceMapsReport",
|
|
27
|
+
"build_divergence_maps",
|
|
28
|
+
"cellpose_runner",
|
|
29
|
+
"native_masks",
|
|
30
|
+
"track_laptrack",
|
|
31
|
+
]
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""Local Cellpose-SAM runner, Qt-free, used by the napari Cellpose widget."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Literal
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from itasc.core.tiff import imwrite_grayscale
|
|
12
|
+
|
|
13
|
+
_NORMALIZE = {"tile_norm_blocksize": 128}
|
|
14
|
+
|
|
15
|
+
InputLayout = Literal["2D", "2D+t", "3D", "3D+t"]
|
|
16
|
+
|
|
17
|
+
# layout -> (has_time, has_z); canonical processing shape is always (T, Z, Y, X).
|
|
18
|
+
_LAYOUT_AXES: dict[str, tuple[bool, bool]] = {
|
|
19
|
+
"2D": (False, False),
|
|
20
|
+
"2D+t": (True, False),
|
|
21
|
+
"3D": (False, True),
|
|
22
|
+
"3D+t": (True, True),
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def layout_has_time(layout: str) -> bool:
|
|
27
|
+
return _LAYOUT_AXES[layout][0]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def layout_has_z(layout: str) -> bool:
|
|
31
|
+
return _LAYOUT_AXES[layout][1]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def infer_layout_from_ndim(ndim: int) -> str | None:
|
|
35
|
+
"""Best-effort layout from array ndim.
|
|
36
|
+
|
|
37
|
+
``2 -> "2D"`` and ``4 -> "3D+t"`` are unambiguous; ``3`` is ambiguous
|
|
38
|
+
(``2D+t`` vs ``3D``) so returns ``None`` and the caller keeps the user's
|
|
39
|
+
explicit choice.
|
|
40
|
+
"""
|
|
41
|
+
if ndim == 2:
|
|
42
|
+
return "2D"
|
|
43
|
+
if ndim == 4:
|
|
44
|
+
return "3D+t"
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def to_tzyx(arr: np.ndarray, layout: str) -> np.ndarray:
|
|
49
|
+
"""Normalize an input array to canonical ``(T, Z, Y, X)`` for its layout.
|
|
50
|
+
|
|
51
|
+
Singleton ``T`` and/or ``Z`` axes are inserted so 2D/2D+t/3D/3D+t inputs all
|
|
52
|
+
become 4-D; the runner then iterates uniformly over frames and z-slices.
|
|
53
|
+
"""
|
|
54
|
+
if layout not in _LAYOUT_AXES:
|
|
55
|
+
raise ValueError(f"unknown input layout {layout!r}")
|
|
56
|
+
has_time, has_z = _LAYOUT_AXES[layout]
|
|
57
|
+
arr = np.asarray(arr)
|
|
58
|
+
expected_ndim = 2 + int(has_time) + int(has_z)
|
|
59
|
+
if arr.ndim != expected_ndim:
|
|
60
|
+
raise ValueError(
|
|
61
|
+
f"{layout} input must be {expected_ndim}-D, got shape {arr.shape}"
|
|
62
|
+
)
|
|
63
|
+
if not has_time:
|
|
64
|
+
arr = arr[np.newaxis] # add T at axis 0
|
|
65
|
+
if not has_z:
|
|
66
|
+
arr = arr[:, np.newaxis] # add Z at axis 1 (after T)
|
|
67
|
+
return arr
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class NucleusParams:
|
|
72
|
+
do_3d: bool
|
|
73
|
+
anisotropy: float
|
|
74
|
+
diameter: float # 0 means "let cpsam decide" (None passed to model)
|
|
75
|
+
min_size: int
|
|
76
|
+
gamma: float
|
|
77
|
+
# Cellpose cellprob (logit) threshold; 0.0 is Cellpose's default. Defaulted so
|
|
78
|
+
# the app (which never sets it) and existing callers are byte-for-byte unchanged.
|
|
79
|
+
cellprob_threshold: float = 0.0
|
|
80
|
+
# Cellpose flow-error QC: masks whose flow error exceeds this are removed; 0.4
|
|
81
|
+
# is Cellpose's default (0 disables QC, higher keeps more masks).
|
|
82
|
+
flow_threshold: float = 0.4
|
|
83
|
+
# Euler-integration steps for the flow dynamics; 0 -> auto (None passed to
|
|
84
|
+
# Cellpose, which derives it from the diameter). Both defaulted, so the app and
|
|
85
|
+
# existing callers stay byte-for-byte unchanged.
|
|
86
|
+
niter: int = 0
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass(frozen=True)
|
|
90
|
+
class CellParams:
|
|
91
|
+
diameter: float
|
|
92
|
+
min_size: int
|
|
93
|
+
gamma: float
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _apply_gamma(img: np.ndarray, gamma: float) -> np.ndarray:
|
|
97
|
+
"""Min/max-normalized gamma correction matching cellpose_full.py."""
|
|
98
|
+
if gamma == 1.0:
|
|
99
|
+
return np.asarray(img)
|
|
100
|
+
img = np.asarray(img, dtype=np.float32)
|
|
101
|
+
img_min = float(np.min(img))
|
|
102
|
+
img_max = float(np.max(img))
|
|
103
|
+
if img_max <= img_min:
|
|
104
|
+
return img
|
|
105
|
+
scaled = (img - img_min) / (img_max - img_min)
|
|
106
|
+
return (scaled ** gamma) * (img_max - img_min) + img_min
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _diameter_kwarg(diameter: float) -> float | None:
|
|
110
|
+
return None if diameter == 0 else float(diameter)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
_MODEL = None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _cuda_available() -> bool:
|
|
117
|
+
try:
|
|
118
|
+
import torch
|
|
119
|
+
except ImportError:
|
|
120
|
+
return False
|
|
121
|
+
return bool(torch.cuda.is_available())
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def device_label() -> str:
|
|
125
|
+
return "cuda:0" if _cuda_available() else "cpu"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def is_model_loaded() -> bool:
|
|
129
|
+
return _MODEL is not None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def get_model():
|
|
133
|
+
"""Lazy-load the cpsam model once per process; cached at module level."""
|
|
134
|
+
global _MODEL
|
|
135
|
+
if _MODEL is not None:
|
|
136
|
+
return _MODEL
|
|
137
|
+
from cellpose.models import CellposeModel
|
|
138
|
+
|
|
139
|
+
use_gpu = _cuda_available()
|
|
140
|
+
_MODEL = CellposeModel(
|
|
141
|
+
gpu=use_gpu,
|
|
142
|
+
pretrained_model="cpsam",
|
|
143
|
+
use_bfloat16=use_gpu,
|
|
144
|
+
)
|
|
145
|
+
return _MODEL
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def run_nucleus_frame(
|
|
149
|
+
frame: np.ndarray,
|
|
150
|
+
z: int | None,
|
|
151
|
+
params: NucleusParams,
|
|
152
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
153
|
+
"""Single-frame nucleus inference.
|
|
154
|
+
|
|
155
|
+
If ``z`` is None, runs full 3D over (Z, Y, X) and returns
|
|
156
|
+
prob with shape (Z, Y, X) and dp with shape (3, Z, Y, X).
|
|
157
|
+
If ``z`` is an integer, runs 2D on frame[z] and returns
|
|
158
|
+
prob with shape (Y, X) and dp with shape (2, Y, X).
|
|
159
|
+
"""
|
|
160
|
+
model = get_model()
|
|
161
|
+
diameter = _diameter_kwarg(params.diameter)
|
|
162
|
+
if z is None:
|
|
163
|
+
volume = _apply_gamma(frame, params.gamma)
|
|
164
|
+
_, flows, _ = model.eval(
|
|
165
|
+
volume,
|
|
166
|
+
do_3D=True,
|
|
167
|
+
z_axis=0,
|
|
168
|
+
diameter=diameter,
|
|
169
|
+
anisotropy=params.anisotropy,
|
|
170
|
+
min_size=params.min_size,
|
|
171
|
+
normalize=_NORMALIZE,
|
|
172
|
+
)
|
|
173
|
+
else:
|
|
174
|
+
slice_2d = _apply_gamma(frame[z], params.gamma)
|
|
175
|
+
_, flows, _ = model.eval(
|
|
176
|
+
slice_2d,
|
|
177
|
+
diameter=diameter,
|
|
178
|
+
min_size=params.min_size,
|
|
179
|
+
normalize=_NORMALIZE,
|
|
180
|
+
)
|
|
181
|
+
dp = np.asarray(flows[1], dtype=np.float32)
|
|
182
|
+
prob = np.asarray(flows[2], dtype=np.float32)
|
|
183
|
+
return prob, dp
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class CancelledError(RuntimeError):
|
|
187
|
+
"""Raised by run_*_stack when cancel_cb returns True between frames."""
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _check_cancel(cancel_cb: Callable[[], bool] | None) -> None:
|
|
191
|
+
if cancel_cb is not None and cancel_cb():
|
|
192
|
+
raise CancelledError("cellpose run cancelled")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def run_cell_frame(
|
|
196
|
+
frame: np.ndarray,
|
|
197
|
+
z: int,
|
|
198
|
+
params: CellParams,
|
|
199
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
200
|
+
"""Single 2D-slice cell inference. Returns (prob (Y,X), dp (2,Y,X))."""
|
|
201
|
+
model = get_model()
|
|
202
|
+
diameter = _diameter_kwarg(params.diameter)
|
|
203
|
+
slice_2d = _apply_gamma(frame[z], params.gamma)
|
|
204
|
+
_, flows, _ = model.eval(
|
|
205
|
+
slice_2d,
|
|
206
|
+
diameter=diameter,
|
|
207
|
+
min_size=params.min_size,
|
|
208
|
+
normalize=_NORMALIZE,
|
|
209
|
+
)
|
|
210
|
+
dp = np.asarray(flows[1], dtype=np.float32)
|
|
211
|
+
prob = np.asarray(flows[2], dtype=np.float32)
|
|
212
|
+
return prob, dp
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def run_nucleus_stack(
|
|
216
|
+
stack: np.ndarray,
|
|
217
|
+
params: NucleusParams,
|
|
218
|
+
*,
|
|
219
|
+
progress_cb: Callable[[int, int, str], None] | None = None,
|
|
220
|
+
cancel_cb: Callable[[], bool] | None = None,
|
|
221
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
222
|
+
"""Process a (T, Z, Y, X) stack frame-by-frame.
|
|
223
|
+
|
|
224
|
+
Returns (prob_3dt, dp_3dt). For do_3d=True dp has shape (T, 3, Z, Y, X);
|
|
225
|
+
for do_3d=False dp has shape (T, Z, 2, Y, X).
|
|
226
|
+
"""
|
|
227
|
+
if stack.ndim != 4:
|
|
228
|
+
raise ValueError(f"expected (T, Z, Y, X), got shape {stack.shape}")
|
|
229
|
+
T = stack.shape[0]
|
|
230
|
+
prob_frames: list[np.ndarray] = []
|
|
231
|
+
dp_frames: list[np.ndarray] = []
|
|
232
|
+
for t in range(T):
|
|
233
|
+
_check_cancel(cancel_cb)
|
|
234
|
+
if progress_cb is not None:
|
|
235
|
+
progress_cb(t, T, f"Nucleus: frame {t + 1}/{T}...")
|
|
236
|
+
if params.do_3d:
|
|
237
|
+
prob, dp = run_nucleus_frame(stack[t], z=None, params=params)
|
|
238
|
+
else:
|
|
239
|
+
Z = stack.shape[1]
|
|
240
|
+
slice_probs: list[np.ndarray] = []
|
|
241
|
+
slice_dps: list[np.ndarray] = []
|
|
242
|
+
for z in range(Z):
|
|
243
|
+
if progress_cb is not None:
|
|
244
|
+
progress_cb(
|
|
245
|
+
t,
|
|
246
|
+
T,
|
|
247
|
+
f"Nucleus: frame {t + 1}/{T}, z {z + 1}/{Z}...",
|
|
248
|
+
)
|
|
249
|
+
p, d = run_nucleus_frame(stack[t], z=z, params=params)
|
|
250
|
+
slice_probs.append(p)
|
|
251
|
+
slice_dps.append(d)
|
|
252
|
+
prob = np.stack(slice_probs, axis=0)
|
|
253
|
+
dp = np.stack(slice_dps, axis=0)
|
|
254
|
+
prob_frames.append(prob)
|
|
255
|
+
dp_frames.append(dp)
|
|
256
|
+
if progress_cb is not None:
|
|
257
|
+
progress_cb(t + 1, T, f"Nucleus: frame {t + 1}/{T}...")
|
|
258
|
+
return np.stack(prob_frames, axis=0), np.stack(dp_frames, axis=0)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def run_cell_stack(
|
|
262
|
+
stack: np.ndarray,
|
|
263
|
+
params: CellParams,
|
|
264
|
+
*,
|
|
265
|
+
progress_cb: Callable[[int, int, str], None] | None = None,
|
|
266
|
+
cancel_cb: Callable[[], bool] | None = None,
|
|
267
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
268
|
+
"""Process a (T, Z, Y, X) stack slice-by-slice in 2D.
|
|
269
|
+
|
|
270
|
+
Returns (prob_3dt (T, Z, Y, X), dp_3dt (T, Z, 2, Y, X)).
|
|
271
|
+
"""
|
|
272
|
+
if stack.ndim != 4:
|
|
273
|
+
raise ValueError(f"expected (T, Z, Y, X), got shape {stack.shape}")
|
|
274
|
+
T, Z = stack.shape[:2]
|
|
275
|
+
prob_frames: list[np.ndarray] = []
|
|
276
|
+
dp_frames: list[np.ndarray] = []
|
|
277
|
+
for t in range(T):
|
|
278
|
+
_check_cancel(cancel_cb)
|
|
279
|
+
if progress_cb is not None:
|
|
280
|
+
progress_cb(t, T, f"Cell: frame {t + 1}/{T}...")
|
|
281
|
+
slice_probs: list[np.ndarray] = []
|
|
282
|
+
slice_dps: list[np.ndarray] = []
|
|
283
|
+
for z in range(Z):
|
|
284
|
+
p, d = run_cell_frame(stack[t], z=z, params=params)
|
|
285
|
+
slice_probs.append(p)
|
|
286
|
+
slice_dps.append(d)
|
|
287
|
+
prob_frames.append(np.stack(slice_probs, axis=0))
|
|
288
|
+
dp_frames.append(np.stack(slice_dps, axis=0))
|
|
289
|
+
if progress_cb is not None:
|
|
290
|
+
progress_cb(t + 1, T, f"Cell: frame {t + 1}/{T}...")
|
|
291
|
+
return np.stack(prob_frames, axis=0), np.stack(dp_frames, axis=0)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _dp_axes(dp_3dt: np.ndarray) -> str | None:
|
|
295
|
+
"""Axis labels for the flow stack so singleton T/Z survive the TIFF round-trip.
|
|
296
|
+
|
|
297
|
+
2D-per-slice flow is ``(T, Z, 2, Y, X)``; true-3D flow is ``(T, 3, Z, Y, X)``.
|
|
298
|
+
"""
|
|
299
|
+
if dp_3dt.ndim == 5 and dp_3dt.shape[2] == 2:
|
|
300
|
+
return "TZCYX"
|
|
301
|
+
if dp_3dt.ndim == 5 and dp_3dt.shape[1] == 3:
|
|
302
|
+
return "TCZYX"
|
|
303
|
+
return None
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def write_outputs(
|
|
307
|
+
prob_3dt: np.ndarray,
|
|
308
|
+
dp_3dt: np.ndarray,
|
|
309
|
+
output_dir: Path,
|
|
310
|
+
channel: Literal["nucleus", "cell"],
|
|
311
|
+
) -> None:
|
|
312
|
+
"""Write the two canonical TIFFs under output_dir.
|
|
313
|
+
|
|
314
|
+
Writes ``{channel}_prob.tif`` and ``{channel}_dp.tif``. Axis labels
|
|
315
|
+
are recorded as metadata so singleton ``T``/``Z`` axes (2D / 2D+t / single
|
|
316
|
+
3D-stack inputs) survive the TIFF round-trip and are not misread downstream.
|
|
317
|
+
"""
|
|
318
|
+
if channel not in ("nucleus", "cell"):
|
|
319
|
+
raise ValueError(f"channel must be 'nucleus' or 'cell', got {channel!r}")
|
|
320
|
+
if prob_3dt.ndim != 4:
|
|
321
|
+
raise ValueError(f"prob_3dt must be (T, Z, Y, X), got {prob_3dt.shape}")
|
|
322
|
+
output_dir = Path(output_dir)
|
|
323
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
324
|
+
prob_path = output_dir / f"{channel}_prob.tif"
|
|
325
|
+
dp_path = output_dir / f"{channel}_dp.tif"
|
|
326
|
+
imwrite_grayscale(
|
|
327
|
+
prob_path, prob_3dt.astype(np.float32),
|
|
328
|
+
compression="zlib", metadata={"axes": "TZYX"},
|
|
329
|
+
)
|
|
330
|
+
dp_kwargs: dict = {"compression": "zlib"}
|
|
331
|
+
dp_axes = _dp_axes(dp_3dt)
|
|
332
|
+
if dp_axes is not None:
|
|
333
|
+
dp_kwargs["metadata"] = {"axes": dp_axes}
|
|
334
|
+
imwrite_grayscale(dp_path, dp_3dt.astype(np.float32), **dp_kwargs)
|