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/core/commit.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""The final-output commit contract.
|
|
2
|
+
|
|
3
|
+
The numbered stage dirs (``2_nucleus/``, ``3_cell/``) hold re-runnable *working*
|
|
4
|
+
label files; :func:`promote_labels` copies one up to a stable *committed* name in
|
|
5
|
+
the position base folder (``nucleus_labels.tif`` / ``cell_labels.tif``), the
|
|
6
|
+
downstream-stable output that discovery defaults to. :func:`commit_state` reports
|
|
7
|
+
where a position sits in that working-vs-committed split — the signal the
|
|
8
|
+
workflow widgets surface and the catalog status rail reads.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import shutil
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
import tifffile
|
|
17
|
+
|
|
18
|
+
CommitState = str # one of: "missing" | "uncommitted" | "committed" | "stale"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def promote_labels(src: Path | str, dst: Path | str) -> Path:
|
|
22
|
+
"""Promote a working label file *src* to the committed output *dst*.
|
|
23
|
+
|
|
24
|
+
Copies bytes verbatim (the working file is already a dtype-correct label
|
|
25
|
+
image), creating ``dst``'s parent and overwriting any existing ``dst``.
|
|
26
|
+
Raises ``FileNotFoundError`` if *src* is missing and ``ValueError`` if it is
|
|
27
|
+
not an integer-typed label image.
|
|
28
|
+
"""
|
|
29
|
+
src = Path(src)
|
|
30
|
+
dst = Path(dst)
|
|
31
|
+
if not src.is_file():
|
|
32
|
+
raise FileNotFoundError(f"No working label file to commit: {src}")
|
|
33
|
+
with tifffile.TiffFile(str(src)) as tf:
|
|
34
|
+
dtype = tf.series[0].dtype
|
|
35
|
+
if not np.issubdtype(dtype, np.integer):
|
|
36
|
+
raise ValueError(
|
|
37
|
+
f"Not a label image (dtype {dtype}); refusing to commit {src}"
|
|
38
|
+
)
|
|
39
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
shutil.copy2(src, dst)
|
|
41
|
+
return dst
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def commit_state(working: Path | str, committed: Path | str) -> CommitState:
|
|
45
|
+
"""Return the working-vs-committed state for one stage output.
|
|
46
|
+
|
|
47
|
+
* ``"missing"`` — no working file yet.
|
|
48
|
+
* ``"uncommitted"`` — working file exists but has not been committed.
|
|
49
|
+
* ``"committed"`` — committed copy is at least as new as the working file.
|
|
50
|
+
* ``"stale"`` — the working file was re-run after the last commit (strictly
|
|
51
|
+
newer), so the committed copy no longer reflects it.
|
|
52
|
+
"""
|
|
53
|
+
working = Path(working)
|
|
54
|
+
committed = Path(committed)
|
|
55
|
+
if not working.is_file():
|
|
56
|
+
return "missing"
|
|
57
|
+
if not committed.is_file():
|
|
58
|
+
return "uncommitted"
|
|
59
|
+
if working.stat().st_mtime > committed.stat().st_mtime:
|
|
60
|
+
return "stale"
|
|
61
|
+
return "committed"
|
itasc/core/imageops.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# src/itasc/core/imageops.py
|
|
2
|
+
"""Generic, deterministic image operations shared across ITASC pieces.
|
|
3
|
+
|
|
4
|
+
Pure functions with no ITASC dependencies, usable by segmentation, tracking,
|
|
5
|
+
and any other consumer without pulling in a heavier subpackage.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
from skimage.filters import threshold_local
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def residual(frame: np.ndarray, window: int, strength: float = 1.0) -> np.ndarray:
|
|
14
|
+
"""Local-mean-subtracted residual: ``clip(frame - strength*localmean(frame), 0)``.
|
|
15
|
+
|
|
16
|
+
Flattens each map's per-nucleus offset so a single global threshold works
|
|
17
|
+
everywhere while staying ~0 in flat background. ``window`` is forced odd.
|
|
18
|
+
|
|
19
|
+
``strength`` blends between the raw map and the fully-flattened residual:
|
|
20
|
+
``1.0`` subtracts the whole local background (default), ``0.0`` subtracts
|
|
21
|
+
nothing so the result is the raw (non-negative) map, and values in between
|
|
22
|
+
partially flatten. Lowering it trades uniform-threshold behaviour for
|
|
23
|
+
keeping more of the original signal where the background is already flat.
|
|
24
|
+
"""
|
|
25
|
+
window = int(window) | 1
|
|
26
|
+
frame = np.asarray(frame, dtype=np.float32)
|
|
27
|
+
if strength == 0.0:
|
|
28
|
+
# No background subtraction requested (the default): the result is just
|
|
29
|
+
# the non-negative raw map, so skip the O(Y·X) gaussian entirely. This
|
|
30
|
+
# matters because ``CellDivergenceParams.fg_strength`` defaults to 0.0,
|
|
31
|
+
# so every default-config frame — including live previews — hit this.
|
|
32
|
+
return np.clip(frame, 0.0, None)
|
|
33
|
+
local_mean = threshold_local(frame, block_size=window, method="gaussian")
|
|
34
|
+
return np.clip(frame - strength * local_mean, 0.0, None).astype(np.float32)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""TIFF storage for tracked nucleus label volumes.
|
|
2
|
+
|
|
3
|
+
Schema: single multipage TIFF — shape (T, Y, X), dtype uint32.
|
|
4
|
+
Frames that have not yet been tracked are stored as all-zeros.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import tifffile
|
|
12
|
+
|
|
13
|
+
from itasc.core.tiff import imwrite_grayscale
|
|
14
|
+
|
|
15
|
+
_LABEL_DTYPE = np.uint32
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _load_stack(path: Path) -> np.ndarray:
|
|
19
|
+
"""Load the TIFF as (T, Y, X). Returns empty array if file does not exist.
|
|
20
|
+
|
|
21
|
+
Tolerates legacy files written with a singleton Z axis: ``(T, 1, Y, X)``
|
|
22
|
+
is squeezed to ``(T, Y, X)`` so the in-memory shape always matches the
|
|
23
|
+
documented schema.
|
|
24
|
+
"""
|
|
25
|
+
if not path.exists():
|
|
26
|
+
return np.empty((0, 0, 0), dtype=_LABEL_DTYPE)
|
|
27
|
+
stack = np.asarray(tifffile.imread(str(path)), dtype=_LABEL_DTYPE)
|
|
28
|
+
if stack.ndim == 2:
|
|
29
|
+
stack = stack[np.newaxis]
|
|
30
|
+
elif stack.ndim == 4 and stack.shape[1] == 1:
|
|
31
|
+
stack = stack[:, 0]
|
|
32
|
+
return stack
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def write_full_tracked_stack(path: str | Path, stack: np.ndarray) -> None:
|
|
36
|
+
"""Write the entire ``(T, Y, X)`` tracked stack in one encode.
|
|
37
|
+
|
|
38
|
+
A multipage zlib TIFF cannot be updated in place, so a per-frame writer
|
|
39
|
+
would decode and re-encode the whole file on every frame (O(T²) over a
|
|
40
|
+
full-stack save). This encodes once (O(T)). A legacy singleton-Z axis
|
|
41
|
+
``(T, 1, Y, X)`` is squeezed to match the documented schema.
|
|
42
|
+
"""
|
|
43
|
+
path = Path(path)
|
|
44
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
stack = np.asarray(stack, dtype=_LABEL_DTYPE)
|
|
46
|
+
if stack.ndim == 4 and stack.shape[1] == 1:
|
|
47
|
+
stack = stack[:, 0]
|
|
48
|
+
imwrite_grayscale(path, stack, compression="zlib")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def read_full_tracked_stack(path: str | Path) -> np.ndarray:
|
|
52
|
+
"""Read all tracked frames as a (T, Y, X) uint32 array."""
|
|
53
|
+
return _load_stack(Path(path))
|
itasc/core/lineage.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Per-track temporal presence ("swimlane") model from a tracked label stack.
|
|
2
|
+
|
|
3
|
+
This is the data half of the correction *lineage graph*: for each track id it
|
|
4
|
+
records the frame ranges where the cell is present, collapsed into contiguous
|
|
5
|
+
segments so a gap (track vanishes then returns — a likely ID swap or missed
|
|
6
|
+
link) reads as a break between segments. It is deliberately array-only — true
|
|
7
|
+
parent/daughter division edges live in the Ultrack database and are a separate,
|
|
8
|
+
heavier concern layered on top later.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=True)
|
|
18
|
+
class TrackSegment:
|
|
19
|
+
"""A contiguous run of frames ``[start, end]`` (inclusive) for one track."""
|
|
20
|
+
|
|
21
|
+
start: int
|
|
22
|
+
end: int
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def length(self) -> int:
|
|
26
|
+
return self.end - self.start + 1
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True, slots=True)
|
|
30
|
+
class TrackLane:
|
|
31
|
+
"""One track id and the frame segments where it is present."""
|
|
32
|
+
|
|
33
|
+
cell_id: int
|
|
34
|
+
segments: tuple[TrackSegment, ...]
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def first_frame(self) -> int:
|
|
38
|
+
return self.segments[0].start
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def last_frame(self) -> int:
|
|
42
|
+
return self.segments[-1].end
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def n_frames(self) -> int:
|
|
46
|
+
return sum(seg.length for seg in self.segments)
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def has_gap(self) -> bool:
|
|
50
|
+
return len(self.segments) > 1
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True, slots=True)
|
|
54
|
+
class LineageModel:
|
|
55
|
+
"""All track lanes plus the total frame count, for the lineage panel."""
|
|
56
|
+
|
|
57
|
+
n_frames: int
|
|
58
|
+
lanes: tuple[TrackLane, ...]
|
|
59
|
+
|
|
60
|
+
def lane_for(self, cell_id: int) -> TrackLane | None:
|
|
61
|
+
for lane in self.lanes:
|
|
62
|
+
if lane.cell_id == cell_id:
|
|
63
|
+
return lane
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _segments_from_frames(frames: list[int]) -> tuple[TrackSegment, ...]:
|
|
68
|
+
"""Collapse a sorted frame list into contiguous inclusive segments."""
|
|
69
|
+
segments: list[TrackSegment] = []
|
|
70
|
+
start = prev = frames[0]
|
|
71
|
+
for t in frames[1:]:
|
|
72
|
+
if t == prev + 1:
|
|
73
|
+
prev = t
|
|
74
|
+
continue
|
|
75
|
+
segments.append(TrackSegment(start=start, end=prev))
|
|
76
|
+
start = prev = t
|
|
77
|
+
segments.append(TrackSegment(start=start, end=prev))
|
|
78
|
+
return tuple(segments)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def build_lineage(tracked: np.ndarray) -> LineageModel:
|
|
82
|
+
"""Build a :class:`LineageModel` from a ``(T, Y, X)`` tracked label stack.
|
|
83
|
+
|
|
84
|
+
A singleton Z axis (``(T, 1, Y, X)``) is squeezed; a single 2D frame is
|
|
85
|
+
treated as one timepoint. Lanes are sorted by track id so a cell keeps its
|
|
86
|
+
row across refreshes; correction actions (retrack, extend, relabel, …) that
|
|
87
|
+
change a track's id move it to its new sorted position on the next refresh.
|
|
88
|
+
"""
|
|
89
|
+
arr = np.asarray(tracked)
|
|
90
|
+
if arr.ndim == 4 and arr.shape[1] == 1:
|
|
91
|
+
arr = arr[:, 0]
|
|
92
|
+
if arr.ndim == 2:
|
|
93
|
+
arr = arr[np.newaxis]
|
|
94
|
+
if arr.ndim != 3:
|
|
95
|
+
raise ValueError(f"tracked must be (T, Y, X); got shape {arr.shape}")
|
|
96
|
+
|
|
97
|
+
n_t = arr.shape[0]
|
|
98
|
+
frames_of: dict[int, list[int]] = {}
|
|
99
|
+
for t in range(n_t):
|
|
100
|
+
for cell_id in np.unique(arr[t]).tolist():
|
|
101
|
+
if cell_id == 0:
|
|
102
|
+
continue
|
|
103
|
+
frames_of.setdefault(int(cell_id), []).append(t)
|
|
104
|
+
|
|
105
|
+
lanes = [
|
|
106
|
+
TrackLane(cell_id=cell_id, segments=_segments_from_frames(frames))
|
|
107
|
+
for cell_id, frames in frames_of.items()
|
|
108
|
+
]
|
|
109
|
+
lanes.sort(key=lambda lane: lane.cell_id)
|
|
110
|
+
return LineageModel(n_frames=n_t, lanes=tuple(lanes))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
__all__ = ["LineageModel", "TrackLane", "TrackSegment", "build_lineage"]
|
itasc/core/logging.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""StageLogger — JSON-lines logger writing to per-position pipeline.log."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class StageLogger:
|
|
10
|
+
"""Context manager that appends JSON-lines entries to pipeline.log."""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
pipeline_log: Path,
|
|
15
|
+
stage_name: str,
|
|
16
|
+
) -> None:
|
|
17
|
+
self._pipeline_log = pipeline_log
|
|
18
|
+
self._stage_name = stage_name
|
|
19
|
+
|
|
20
|
+
def _write(self, level: str, message: str) -> None:
|
|
21
|
+
entry = json.dumps(
|
|
22
|
+
{
|
|
23
|
+
"ts": datetime.now(timezone.utc).isoformat(),
|
|
24
|
+
"stage": self._stage_name,
|
|
25
|
+
"level": level,
|
|
26
|
+
"message": message,
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
self._pipeline_log.parent.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
with self._pipeline_log.open("a", encoding="utf-8") as fh:
|
|
31
|
+
fh.write(entry + "\n")
|
|
32
|
+
|
|
33
|
+
def info(self, message: str) -> None:
|
|
34
|
+
self._write("INFO", message)
|
|
35
|
+
|
|
36
|
+
def warning(self, message: str) -> None:
|
|
37
|
+
self._write("WARNING", message)
|
|
38
|
+
|
|
39
|
+
def error(self, message: str) -> None:
|
|
40
|
+
self._write("ERROR", message)
|
|
41
|
+
|
|
42
|
+
def __enter__(self) -> StageLogger:
|
|
43
|
+
self.info("Stage started")
|
|
44
|
+
return self
|
|
45
|
+
|
|
46
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
|
|
47
|
+
if exc_type is None:
|
|
48
|
+
self.info("Stage completed successfully")
|
|
49
|
+
else:
|
|
50
|
+
self.error(f"Stage failed: {exc_val}")
|
|
51
|
+
return False
|
itasc/core/paths.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Unified path resolution for the ITASC pipeline directory layout."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
#: Filename ITASC writes a position's metadata/config into.
|
|
7
|
+
CONFIG_NAME = "itasc_config.json"
|
|
8
|
+
|
|
9
|
+
#: Pre-rename filename, written by the old CellFlow app. Read-only fallback.
|
|
10
|
+
_LEGACY_CONFIG_NAME = "cellflow_config.json"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def pos_dir(root: Path | str, pos: int) -> Path:
|
|
14
|
+
"""Return <root>/pos<pos:02d>."""
|
|
15
|
+
return Path(root) / f"pos{pos:02d}"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def position_config_path(position_dir: Path | str) -> Path:
|
|
19
|
+
"""Path to a position's ITASC config file.
|
|
20
|
+
|
|
21
|
+
Prefers the current ``itasc_config.json``. When it is absent but a legacy
|
|
22
|
+
``cellflow_config.json`` (written before the rename) is present, returns the
|
|
23
|
+
legacy path so old data keeps loading. When neither exists, returns the
|
|
24
|
+
current name, so new writes always use it.
|
|
25
|
+
"""
|
|
26
|
+
base = Path(position_dir)
|
|
27
|
+
current = base / CONFIG_NAME
|
|
28
|
+
if current.exists():
|
|
29
|
+
return current
|
|
30
|
+
legacy = base / _LEGACY_CONFIG_NAME
|
|
31
|
+
if legacy.exists():
|
|
32
|
+
return legacy
|
|
33
|
+
return current
|
|
34
|
+
|
itasc/core/tiff.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""TIFF writing helpers for ITASC grayscale stacks."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import tifffile
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def imwrite_grayscale(path: str | Path, data: Any, **kwargs: Any) -> None:
|
|
11
|
+
"""Write a grayscale TIFF stack without tifffile RGB-shape ambiguity."""
|
|
12
|
+
kwargs.setdefault("photometric", "minisblack")
|
|
13
|
+
tifffile.imwrite(str(path), data, **kwargs)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from itasc.correction.labels import (
|
|
2
|
+
clean_stranded_pixels,
|
|
3
|
+
draw_cell_path,
|
|
4
|
+
erase_cell,
|
|
5
|
+
expand_label_to_foreground,
|
|
6
|
+
fill_label_holes,
|
|
7
|
+
fix_label_semiholes,
|
|
8
|
+
merge_cells,
|
|
9
|
+
split_across,
|
|
10
|
+
split_draw,
|
|
11
|
+
swap_labels,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"clean_stranded_pixels",
|
|
16
|
+
"draw_cell_path",
|
|
17
|
+
"erase_cell",
|
|
18
|
+
"expand_label_to_foreground",
|
|
19
|
+
"fill_label_holes",
|
|
20
|
+
"fix_label_semiholes",
|
|
21
|
+
"merge_cells",
|
|
22
|
+
"split_across",
|
|
23
|
+
"split_draw",
|
|
24
|
+
"swap_labels",
|
|
25
|
+
]
|