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,430 @@
|
|
|
1
|
+
"""Persistent validation metadata for the nucleus workflow.
|
|
2
|
+
|
|
3
|
+
All functions take ``nucleus_dir`` — the directory that holds the nucleus
|
|
4
|
+
annotation store (``validated_frames.json``, ``validated_cells.json``,
|
|
5
|
+
``corrections.json``). In the full orchestrator this is ``<pos>/2_nucleus``; the
|
|
6
|
+
standalone tracking piece points it at a flat working directory. The annotation
|
|
7
|
+
files live directly in that directory.
|
|
8
|
+
|
|
9
|
+
Frame-level validation (validated_frames.json):
|
|
10
|
+
A "fully-validated" frame is one where every current (non-zero) cell ID has
|
|
11
|
+
been individually validated. The file acts as a *cache* so UI counters can
|
|
12
|
+
count fully-validated frames without scanning the whole stack.
|
|
13
|
+
|
|
14
|
+
Schema: JSON array of ints, e.g. [0, 3, 7].
|
|
15
|
+
|
|
16
|
+
Track-level validation (validated_cells.json):
|
|
17
|
+
Tracks which frames have been validated for each cell (track) ID.
|
|
18
|
+
|
|
19
|
+
Schema: JSON object keyed by cell ID string, value is a list of frame ints,
|
|
20
|
+
e.g. {"47": [10, 11, 12], "82": [3, 4, 5]}.
|
|
21
|
+
Cell IDs with no validated frames are omitted entirely (sparse).
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from collections.abc import Iterable
|
|
28
|
+
|
|
29
|
+
import numpy as np
|
|
30
|
+
|
|
31
|
+
from itasc.tracking_ultrack.corrections import Correction
|
|
32
|
+
|
|
33
|
+
# Stat-keyed parse caches: hot overlay loops call the readers below repeatedly,
|
|
34
|
+
# and re-parsing unchanged JSON each time is the cost the review flagged. Key on
|
|
35
|
+
# the file's (mtime_ns, size); a writer touching the file changes the signature
|
|
36
|
+
# and invalidates the entry. Callers always receive a fresh copy, so the cached
|
|
37
|
+
# value is never mutated through aliasing.
|
|
38
|
+
_corrections_cache: dict[Path, tuple[tuple[int, int], list[Correction]]] = {}
|
|
39
|
+
_legacy_tracks_cache: dict[Path, tuple[tuple[int, int], dict[int, set[int]]]] = {}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _stat_signature(path: Path) -> tuple[int, int] | None:
|
|
43
|
+
try:
|
|
44
|
+
st = path.stat()
|
|
45
|
+
except OSError:
|
|
46
|
+
return None
|
|
47
|
+
return (st.st_mtime_ns, st.st_size)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _path(nucleus_dir: Path) -> Path:
|
|
51
|
+
return Path(nucleus_dir) / "validated_frames.json"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def read_validated_frames(nucleus_dir: Path) -> set[int]:
|
|
55
|
+
"""Return the set of validated frame indices, or an empty set if none."""
|
|
56
|
+
p = _path(nucleus_dir)
|
|
57
|
+
if not p.exists():
|
|
58
|
+
return set()
|
|
59
|
+
try:
|
|
60
|
+
data = json.loads(p.read_text())
|
|
61
|
+
return set(int(t) for t in data)
|
|
62
|
+
except Exception:
|
|
63
|
+
return set()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def write_validated_frames(nucleus_dir: Path, frames: set[int]) -> None:
|
|
67
|
+
"""Persist the full set of validated frames."""
|
|
68
|
+
p = _path(nucleus_dir)
|
|
69
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
p.write_text(json.dumps(sorted(frames)))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def validate_frame(nucleus_dir: Path, t: int) -> None:
|
|
74
|
+
"""Mark frame t as validated."""
|
|
75
|
+
frames = read_validated_frames(nucleus_dir)
|
|
76
|
+
frames.add(t)
|
|
77
|
+
write_validated_frames(nucleus_dir, frames)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def invalidate_frame(nucleus_dir: Path, t: int) -> None:
|
|
81
|
+
"""Remove the validated mark from frame t."""
|
|
82
|
+
frames = read_validated_frames(nucleus_dir)
|
|
83
|
+
frames.discard(t)
|
|
84
|
+
write_validated_frames(nucleus_dir, frames)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def is_validated(nucleus_dir: Path, t: int) -> bool:
|
|
88
|
+
"""Return True if frame t is in the validated set."""
|
|
89
|
+
return t in read_validated_frames(nucleus_dir)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
# Track-level validation (nucleus workflow)
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
def _cells_path(nucleus_dir: Path) -> Path:
|
|
97
|
+
return Path(nucleus_dir) / "validated_cells.json"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def read_validated_tracks(nucleus_dir: Path) -> dict[int, set[int]]:
|
|
101
|
+
"""Return {cell_id: {frames}} for all validated tracks.
|
|
102
|
+
|
|
103
|
+
Empty dict if the file is missing or corrupt.
|
|
104
|
+
JSON keys are cell ID strings; values are lists of frame ints.
|
|
105
|
+
"""
|
|
106
|
+
corrections = read_corrections(nucleus_dir)
|
|
107
|
+
if corrections:
|
|
108
|
+
data: dict[int, set[int]] = {}
|
|
109
|
+
for correction in corrections:
|
|
110
|
+
if correction.kind == "validated":
|
|
111
|
+
data.setdefault(int(correction.cell_id), set()).add(int(correction.t))
|
|
112
|
+
legacy = _read_legacy_validated_tracks(nucleus_dir)
|
|
113
|
+
for cell_id, frames in legacy.items():
|
|
114
|
+
data.setdefault(cell_id, set()).update(frames)
|
|
115
|
+
return data
|
|
116
|
+
return _read_legacy_validated_tracks(nucleus_dir)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _copy_tracks(data: dict[int, set[int]]) -> dict[int, set[int]]:
|
|
120
|
+
"""Deep-ish copy so a cached map is never mutated through the returned value."""
|
|
121
|
+
return {cell_id: set(frames) for cell_id, frames in data.items()}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _read_legacy_validated_tracks(nucleus_dir: Path) -> dict[int, set[int]]:
|
|
125
|
+
p = _cells_path(nucleus_dir)
|
|
126
|
+
sig = _stat_signature(p)
|
|
127
|
+
if sig is None:
|
|
128
|
+
_legacy_tracks_cache.pop(p, None)
|
|
129
|
+
return {}
|
|
130
|
+
hit = _legacy_tracks_cache.get(p)
|
|
131
|
+
if hit is not None and hit[0] == sig:
|
|
132
|
+
return _copy_tracks(hit[1])
|
|
133
|
+
try:
|
|
134
|
+
raw: dict = json.loads(p.read_text())
|
|
135
|
+
parsed = {int(k): set(int(f) for f in vs) for k, vs in raw.items() if vs}
|
|
136
|
+
except Exception:
|
|
137
|
+
return {}
|
|
138
|
+
_legacy_tracks_cache[p] = (sig, parsed)
|
|
139
|
+
return _copy_tracks(parsed)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _write_validated_tracks(nucleus_dir: Path, data: dict[int, set[int]]) -> None:
|
|
143
|
+
"""Persist the full {cell_id: {frames}} map. Entries with empty sets are dropped."""
|
|
144
|
+
p = _cells_path(nucleus_dir)
|
|
145
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
146
|
+
serialisable = {
|
|
147
|
+
str(cell_id): sorted(frames)
|
|
148
|
+
for cell_id, frames in data.items()
|
|
149
|
+
if frames
|
|
150
|
+
}
|
|
151
|
+
p.write_text(json.dumps(serialisable))
|
|
152
|
+
# Invalidate the parse cache explicitly: two same-size writes within the
|
|
153
|
+
# filesystem's mtime granularity (coarse on Windows/FAT/NFS) leave the
|
|
154
|
+
# (mtime_ns, size) signature unchanged, so a stale entry would survive.
|
|
155
|
+
_legacy_tracks_cache.pop(p, None)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def read_validated_cells_at_frame(nucleus_dir: Path, t: int) -> set[int]:
|
|
159
|
+
"""Return all cell IDs that have frame *t* in their validated set.
|
|
160
|
+
|
|
161
|
+
Derived from the track-keyed store; suitable for overlay rendering.
|
|
162
|
+
"""
|
|
163
|
+
return {cell_id for cell_id, frames in read_validated_tracks(nucleus_dir).items() if t in frames}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def is_track_validated(nucleus_dir: Path, cell_id: int) -> bool:
|
|
167
|
+
"""Return True if *cell_id* has any entry in the validated-tracks store."""
|
|
168
|
+
return cell_id in read_validated_tracks(nucleus_dir)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def validate_track(nucleus_dir: Path, cell_id: int, frames: Iterable[int]) -> None:
|
|
172
|
+
"""Add the given frames to *cell_id*'s validated set (idempotent, accumulates).
|
|
173
|
+
|
|
174
|
+
Creates an entry for *cell_id* if none exists yet. Any existing per-frame
|
|
175
|
+
anchor corrections for the same cell are dropped — whole-track validation
|
|
176
|
+
supersedes them (the post-solve paste-back stamps the validated geometry,
|
|
177
|
+
so anchor pins on the same cell are redundant).
|
|
178
|
+
"""
|
|
179
|
+
frames_set = set(frames)
|
|
180
|
+
if not frames_set:
|
|
181
|
+
return
|
|
182
|
+
data = read_validated_tracks(nucleus_dir)
|
|
183
|
+
existing = data.get(cell_id, set())
|
|
184
|
+
data[cell_id] = existing | frames_set
|
|
185
|
+
_write_validated_tracks(nucleus_dir, data)
|
|
186
|
+
|
|
187
|
+
corrections = read_corrections(nucleus_dir)
|
|
188
|
+
if corrections:
|
|
189
|
+
filtered = [
|
|
190
|
+
c
|
|
191
|
+
for c in corrections
|
|
192
|
+
if not (int(c.cell_id) == int(cell_id) and c.kind == "anchor")
|
|
193
|
+
]
|
|
194
|
+
if len(filtered) != len(corrections):
|
|
195
|
+
write_corrections(nucleus_dir, filtered)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def invalidate_track(nucleus_dir: Path, cell_id: int) -> None:
|
|
200
|
+
"""Remove the entire entry for *cell_id* from the validated-tracks store.
|
|
201
|
+
|
|
202
|
+
No-op if *cell_id* is not present.
|
|
203
|
+
"""
|
|
204
|
+
corrections = read_corrections(nucleus_dir)
|
|
205
|
+
if corrections:
|
|
206
|
+
write_corrections(
|
|
207
|
+
nucleus_dir,
|
|
208
|
+
[
|
|
209
|
+
c
|
|
210
|
+
for c in corrections
|
|
211
|
+
if not (int(c.cell_id) == int(cell_id) and c.kind == "validated")
|
|
212
|
+
],
|
|
213
|
+
)
|
|
214
|
+
data = read_validated_tracks(nucleus_dir)
|
|
215
|
+
if cell_id in data:
|
|
216
|
+
del data[cell_id]
|
|
217
|
+
_write_validated_tracks(nucleus_dir, data)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def remap_validated_tracks(nucleus_dir: Path, old_to_new: dict[int, int]) -> None:
|
|
221
|
+
"""Remap cell IDs in the validated-tracks store using *old_to_new* mapping.
|
|
222
|
+
|
|
223
|
+
IDs not present in the mapping are dropped.
|
|
224
|
+
"""
|
|
225
|
+
corrections = read_corrections(nucleus_dir)
|
|
226
|
+
# Read the legacy store *directly* (not via ``read_validated_tracks``, which
|
|
227
|
+
# merges in the corrections we are about to rewrite) so each store is remapped
|
|
228
|
+
# exactly once. Reading the merged view here would remap the corrections-derived
|
|
229
|
+
# IDs a second time and inject phantom validations whenever the mapping is not
|
|
230
|
+
# the identity — i.e. for any real contiguous compaction with gaps.
|
|
231
|
+
legacy = _read_legacy_validated_tracks(nucleus_dir)
|
|
232
|
+
if corrections:
|
|
233
|
+
write_corrections(
|
|
234
|
+
nucleus_dir,
|
|
235
|
+
[
|
|
236
|
+
Correction(
|
|
237
|
+
cell_id=int(old_to_new[int(c.cell_id)]),
|
|
238
|
+
t=int(c.t),
|
|
239
|
+
kind=c.kind,
|
|
240
|
+
y=float(c.y),
|
|
241
|
+
x=float(c.x),
|
|
242
|
+
)
|
|
243
|
+
for c in corrections
|
|
244
|
+
if int(c.cell_id) in old_to_new
|
|
245
|
+
],
|
|
246
|
+
)
|
|
247
|
+
remapped = {
|
|
248
|
+
old_to_new[cell_id]: frames
|
|
249
|
+
for cell_id, frames in legacy.items()
|
|
250
|
+
if cell_id in old_to_new
|
|
251
|
+
}
|
|
252
|
+
_write_validated_tracks(nucleus_dir, remapped)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
# ---------------------------------------------------------------------------
|
|
256
|
+
# Unified correction list (nucleus workflow)
|
|
257
|
+
# ---------------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
def _corrections_path(nucleus_dir: Path) -> Path:
|
|
260
|
+
return Path(nucleus_dir) / "corrections.json"
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def read_corrections(nucleus_dir: Path) -> list[Correction]:
|
|
264
|
+
"""Return persisted per-frame corrections, or an empty list if unavailable."""
|
|
265
|
+
p = _corrections_path(nucleus_dir)
|
|
266
|
+
sig = _stat_signature(p)
|
|
267
|
+
if sig is None:
|
|
268
|
+
_corrections_cache.pop(p, None)
|
|
269
|
+
return []
|
|
270
|
+
hit = _corrections_cache.get(p)
|
|
271
|
+
if hit is not None and hit[0] == sig:
|
|
272
|
+
return list(hit[1]) # fresh list of immutable Corrections
|
|
273
|
+
try:
|
|
274
|
+
raw = json.loads(p.read_text())
|
|
275
|
+
corrections = [
|
|
276
|
+
Correction(
|
|
277
|
+
cell_id=int(item["cell_id"]),
|
|
278
|
+
t=int(item["t"]),
|
|
279
|
+
kind=item["kind"],
|
|
280
|
+
y=float(item["y"]),
|
|
281
|
+
x=float(item["x"]),
|
|
282
|
+
)
|
|
283
|
+
for item in raw
|
|
284
|
+
]
|
|
285
|
+
except Exception:
|
|
286
|
+
return []
|
|
287
|
+
result = sorted(corrections, key=lambda c: (c.t, c.cell_id, c.kind))
|
|
288
|
+
_corrections_cache[p] = (sig, result)
|
|
289
|
+
return list(result)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def write_corrections(nucleus_dir: Path, corrections: Iterable[Correction]) -> None:
|
|
293
|
+
"""Persist the full flat correction list to ``corrections.json``."""
|
|
294
|
+
p = _corrections_path(nucleus_dir)
|
|
295
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
296
|
+
serialisable = [
|
|
297
|
+
{
|
|
298
|
+
"cell_id": int(c.cell_id),
|
|
299
|
+
"t": int(c.t),
|
|
300
|
+
"kind": c.kind,
|
|
301
|
+
"y": float(c.y),
|
|
302
|
+
"x": float(c.x),
|
|
303
|
+
}
|
|
304
|
+
for c in sorted(corrections, key=lambda item: (item.t, item.cell_id, item.kind))
|
|
305
|
+
]
|
|
306
|
+
p.write_text(json.dumps(serialisable))
|
|
307
|
+
# See _write_validated_tracks: same-size writes can share a (mtime_ns, size)
|
|
308
|
+
# signature on coarse-mtime filesystems, so invalidate the cache explicitly.
|
|
309
|
+
_corrections_cache.pop(p, None)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def add_correction(nucleus_dir: Path, correction: Correction) -> None:
|
|
313
|
+
"""Add or replace a correction for the same cell, frame, and kind.
|
|
314
|
+
|
|
315
|
+
Writing a ``validated`` correction for a cell additionally drops every
|
|
316
|
+
``anchor`` correction for that cell — validation is whole-track and
|
|
317
|
+
supersedes any anchor pinning on the same cell.
|
|
318
|
+
"""
|
|
319
|
+
add_corrections(nucleus_dir, [correction])
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def add_corrections(nucleus_dir: Path, corrections: Iterable[Correction]) -> None:
|
|
323
|
+
"""Add or replace several corrections with a single read+write.
|
|
324
|
+
|
|
325
|
+
Each correction replaces any existing one for the same cell, frame, and
|
|
326
|
+
kind (later entries in *corrections* win over earlier ones). As with
|
|
327
|
+
:func:`add_correction`, a ``validated`` correction for a cell additionally
|
|
328
|
+
drops every ``anchor`` correction for that cell.
|
|
329
|
+
|
|
330
|
+
Unlike calling :func:`add_correction` in a loop, this reads and writes
|
|
331
|
+
``corrections.json`` exactly once, so validating a long track stays cheap
|
|
332
|
+
instead of re-parsing and re-serialising the whole file per frame.
|
|
333
|
+
"""
|
|
334
|
+
new = list(corrections)
|
|
335
|
+
if not new:
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
# Keys (cell_id, t, kind) being written; cell_ids whose anchors validation drops.
|
|
339
|
+
replaced_keys = {(int(c.cell_id), int(c.t), c.kind) for c in new}
|
|
340
|
+
validated_cells = {int(c.cell_id) for c in new if c.kind == "validated"}
|
|
341
|
+
|
|
342
|
+
def _keep(c: Correction) -> bool:
|
|
343
|
+
if (int(c.cell_id), int(c.t), c.kind) in replaced_keys:
|
|
344
|
+
return False
|
|
345
|
+
if c.kind == "anchor" and int(c.cell_id) in validated_cells:
|
|
346
|
+
return False
|
|
347
|
+
return True
|
|
348
|
+
|
|
349
|
+
existing = [c for c in read_corrections(nucleus_dir) if _keep(c)]
|
|
350
|
+
|
|
351
|
+
# Dedupe within the batch itself (last wins for a repeated key).
|
|
352
|
+
deduped: dict[tuple[int, int, str], Correction] = {}
|
|
353
|
+
for c in new:
|
|
354
|
+
deduped[(int(c.cell_id), int(c.t), c.kind)] = c
|
|
355
|
+
|
|
356
|
+
write_corrections(nucleus_dir, existing + list(deduped.values()))
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _centroid_of(mask: np.ndarray) -> tuple[float, float] | None:
|
|
360
|
+
if mask.ndim == 3:
|
|
361
|
+
mask = mask.any(axis=0)
|
|
362
|
+
ys, xs = np.nonzero(mask)
|
|
363
|
+
if ys.size == 0:
|
|
364
|
+
return None
|
|
365
|
+
return float(ys.mean()), float(xs.mean())
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def add_anchor(
|
|
369
|
+
nucleus_dir: Path,
|
|
370
|
+
cell_id: int,
|
|
371
|
+
t: int,
|
|
372
|
+
y: float,
|
|
373
|
+
x: float,
|
|
374
|
+
tracked_labels: np.ndarray,
|
|
375
|
+
) -> int:
|
|
376
|
+
"""Add an anchor for *cell_id* at frame *t* and back-fill intermediate frames.
|
|
377
|
+
|
|
378
|
+
If another anchor for the same cell already exists at some frame ``A``,
|
|
379
|
+
find the nearest such ``A`` (smallest ``|A - t|``). If ``cell_id`` is
|
|
380
|
+
present in ``tracked_labels`` at *every* frame in ``[min(A, t), max(A, t)]``,
|
|
381
|
+
add an anchor correction for each intermediate frame using the cell's
|
|
382
|
+
centroid in ``tracked_labels`` at that frame.
|
|
383
|
+
|
|
384
|
+
Returns the number of intermediate anchors added (0 if no neighbor exists
|
|
385
|
+
or the track is not consecutive).
|
|
386
|
+
|
|
387
|
+
The strictness rule guarantees we never anchor onto a frame where the cell
|
|
388
|
+
isn't actually present in the corrected layer.
|
|
389
|
+
"""
|
|
390
|
+
cell_id = int(cell_id)
|
|
391
|
+
t = int(t)
|
|
392
|
+
|
|
393
|
+
existing = read_corrections(nucleus_dir)
|
|
394
|
+
other_anchor_frames = sorted(
|
|
395
|
+
int(c.t)
|
|
396
|
+
for c in existing
|
|
397
|
+
if int(c.cell_id) == cell_id and c.kind == "anchor" and int(c.t) != t
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
add_correction(
|
|
401
|
+
nucleus_dir,
|
|
402
|
+
Correction(cell_id=cell_id, t=t, kind="anchor", y=float(y), x=float(x)),
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
if not other_anchor_frames:
|
|
406
|
+
return 0
|
|
407
|
+
|
|
408
|
+
neighbor = min(other_anchor_frames, key=lambda f: (abs(f - t), f))
|
|
409
|
+
lo, hi = (neighbor, t) if neighbor < t else (t, neighbor)
|
|
410
|
+
interior = list(range(lo + 1, hi))
|
|
411
|
+
if not interior:
|
|
412
|
+
return 0
|
|
413
|
+
|
|
414
|
+
n_frames = int(tracked_labels.shape[0])
|
|
415
|
+
centroids: dict[int, tuple[float, float]] = {}
|
|
416
|
+
for f in interior:
|
|
417
|
+
if f < 0 or f >= n_frames:
|
|
418
|
+
return 0
|
|
419
|
+
mask = np.asarray(tracked_labels[f] == cell_id)
|
|
420
|
+
c = _centroid_of(mask)
|
|
421
|
+
if c is None:
|
|
422
|
+
return 0
|
|
423
|
+
centroids[f] = c
|
|
424
|
+
|
|
425
|
+
for f, (cy, cx) in centroids.items():
|
|
426
|
+
add_correction(
|
|
427
|
+
nucleus_dir,
|
|
428
|
+
Correction(cell_id=cell_id, t=f, kind="anchor", y=cy, x=cx),
|
|
429
|
+
)
|
|
430
|
+
return len(centroids)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: itasc
|
|
2
|
+
display_name: ITASC
|
|
3
|
+
contributions:
|
|
4
|
+
commands:
|
|
5
|
+
- id: itasc.main_widget
|
|
6
|
+
title: "ITASC"
|
|
7
|
+
python_name: itasc.napari:ITASCWidget
|
|
8
|
+
# The full install folds the extracted tools' widgets into this single
|
|
9
|
+
# manifest. npe2 requires a manifest's name to match the providing
|
|
10
|
+
# distribution, so the standalone itasc-cellpose / itasc-tracking /
|
|
11
|
+
# itasc-aggregate manifests cannot be re-registered from the itasc
|
|
12
|
+
# distribution; their contributions are mirrored here instead. The integrated
|
|
13
|
+
# ITASC app's own per-stage widgets (Cellpose map-building, Cell
|
|
14
|
+
# Segmentation) live inside the ITASC widget above and are not exposed as
|
|
15
|
+
# separate menu entries.
|
|
16
|
+
#
|
|
17
|
+
# The itasc-cellpose distribution's *standalone* surface — native-mask
|
|
18
|
+
# segment + laptrack track + embedded basic correction. It is not part of the
|
|
19
|
+
# integrated pipeline (the pipeline uses the Cellpose widget above); it is
|
|
20
|
+
# mirrored here only so the full install can also reach the standalone tool.
|
|
21
|
+
- id: itasc.cellpose_segment_track_widget
|
|
22
|
+
title: "Cellpose Segment + Track"
|
|
23
|
+
python_name: itasc.napari.cellpose_segment_track_widget:make_cellpose_segment_track_widget
|
|
24
|
+
- id: itasc.nucleus_tracking_widget
|
|
25
|
+
title: "Ultrack Segment + Track"
|
|
26
|
+
python_name: itasc.napari.nucleus_workflow_widget:make_nucleus_tracking_widget
|
|
27
|
+
# The ITASC Aggregate app: the full catalog UI restricted to the
|
|
28
|
+
# Contact Analysis stage + Aggregate capstone (three-dot rail: cell labels,
|
|
29
|
+
# nucleus labels, contact analysis). Both the full install and the standalone
|
|
30
|
+
# itasc-aggregate wheel resolve this same factory; the standalone simply
|
|
31
|
+
# lacks the upstream segmentation/tracking distributions, so it always comes
|
|
32
|
+
# up contact-only. In the full install this is the slim aggregate-focused
|
|
33
|
+
# entry alongside the complete ITASC widget above.
|
|
34
|
+
- id: itasc.contact_analysis_widget
|
|
35
|
+
title: "ITASC Aggregate"
|
|
36
|
+
python_name: itasc.napari.main_widget:make_aggregate_app_widget
|
|
37
|
+
widgets:
|
|
38
|
+
- command: itasc.main_widget
|
|
39
|
+
display_name: "ITASC"
|
|
40
|
+
- command: itasc.cellpose_segment_track_widget
|
|
41
|
+
display_name: "Cellpose Segment + Track"
|
|
42
|
+
- command: itasc.nucleus_tracking_widget
|
|
43
|
+
display_name: "Ultrack Segment + Track"
|
|
44
|
+
- command: itasc.contact_analysis_widget
|
|
45
|
+
display_name: "ITASC Aggregate"
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: itasc
|
|
3
|
+
Version: 0.2.0.dev0
|
|
4
|
+
Summary: Fast, interactive cell tracking
|
|
5
|
+
Project-URL: Homepage, https://github.com/ArturRuppel/ITASC
|
|
6
|
+
Project-URL: Repository, https://github.com/ArturRuppel/ITASC
|
|
7
|
+
Project-URL: Issues, https://github.com/ArturRuppel/ITASC/issues
|
|
8
|
+
Author-email: Artur Ruppel <artur@ruppel.pro>
|
|
9
|
+
License: AGPL-3.0
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: cell-tracking,microscopy,napari,segmentation
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Framework :: napari
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: License :: OSI Approved :: GNU Affero General Public License v3
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
24
|
+
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Requires-Dist: h5py
|
|
27
|
+
Requires-Dist: matplotlib>=3.9.4
|
|
28
|
+
Requires-Dist: napari[all]>=0.4.18
|
|
29
|
+
Requires-Dist: numba
|
|
30
|
+
Requires-Dist: numpy
|
|
31
|
+
Requires-Dist: pandas
|
|
32
|
+
Requires-Dist: pyarrow
|
|
33
|
+
Requires-Dist: pydantic
|
|
34
|
+
Requires-Dist: pymaxflow>=1.3.2
|
|
35
|
+
Requires-Dist: pyqtgraph>=0.13
|
|
36
|
+
Requires-Dist: qtpy>=2.3.0
|
|
37
|
+
Requires-Dist: scikit-image
|
|
38
|
+
Requires-Dist: scipy
|
|
39
|
+
Requires-Dist: seaborn>=0.13
|
|
40
|
+
Requires-Dist: sqlalchemy
|
|
41
|
+
Requires-Dist: tifffile
|
|
42
|
+
Provides-Extra: all
|
|
43
|
+
Requires-Dist: cellpose>=4.0; extra == 'all'
|
|
44
|
+
Requires-Dist: torch; extra == 'all'
|
|
45
|
+
Requires-Dist: torchvision; extra == 'all'
|
|
46
|
+
Requires-Dist: ultrack; extra == 'all'
|
|
47
|
+
Provides-Extra: cellpose
|
|
48
|
+
Requires-Dist: cellpose>=4.0; extra == 'cellpose'
|
|
49
|
+
Requires-Dist: torch; extra == 'cellpose'
|
|
50
|
+
Requires-Dist: torchvision; extra == 'cellpose'
|
|
51
|
+
Provides-Extra: dev
|
|
52
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
53
|
+
Requires-Dist: ruff>=0.11; extra == 'dev'
|
|
54
|
+
Requires-Dist: tomli>=2.0; (python_version < '3.11') and extra == 'dev'
|
|
55
|
+
Provides-Extra: docs
|
|
56
|
+
Requires-Dist: furo>=2024.1; extra == 'docs'
|
|
57
|
+
Requires-Dist: myst-parser>=2; extra == 'docs'
|
|
58
|
+
Requires-Dist: sphinx-autobuild>=2024.2; extra == 'docs'
|
|
59
|
+
Requires-Dist: sphinx-autodoc-typehints>=1.25; extra == 'docs'
|
|
60
|
+
Requires-Dist: sphinx-design>=0.5; extra == 'docs'
|
|
61
|
+
Requires-Dist: sphinx>=7; extra == 'docs'
|
|
62
|
+
Requires-Dist: sphinxcontrib-mermaid>=0.9; extra == 'docs'
|
|
63
|
+
Provides-Extra: test
|
|
64
|
+
Requires-Dist: pytest>=8; extra == 'test'
|
|
65
|
+
Requires-Dist: tomli>=2.0; (python_version < '3.11') and extra == 'test'
|
|
66
|
+
Provides-Extra: tracking
|
|
67
|
+
Requires-Dist: ultrack; extra == 'tracking'
|
|
68
|
+
Description-Content-Type: text/markdown
|
|
69
|
+
|
|
70
|
+
# ITASC
|
|
71
|
+
|
|
72
|
+
Segment, track, correct, and quantify cells in time-lapse microscopy, inside napari.
|
|
73
|
+
|
|
74
|
+
ITASC (Interactive Tracking And Segmentation of Cells) takes raw time-lapse
|
|
75
|
+
stacks to tracked, quantified cells. It segments each frame, links cells across
|
|
76
|
+
time, lets you correct the result by hand where the automatics miss, and
|
|
77
|
+
measures what the tracked cells do. It is a [napari](https://napari.org) plugin
|
|
78
|
+
built for dense, motile monolayers, where segmentation and tracking are the hard
|
|
79
|
+
part.
|
|
80
|
+
|
|
81
|
+
<!-- hero-start -->
|
|
82
|
+
<p align="center">
|
|
83
|
+
<img src="docs/_static/napari_timelapse_last.png"
|
|
84
|
+
alt="ITASC tracking a monolayer in napari" width="100%">
|
|
85
|
+
</p>
|
|
86
|
+
<!-- hero-end -->
|
|
87
|
+
|
|
88
|
+
## What it does
|
|
89
|
+
|
|
90
|
+
An ITASC run moves through four stages, each usable on its own:
|
|
91
|
+
|
|
92
|
+
- **Segment** each frame with [Cellpose-SAM](https://github.com/MouseLand/cellpose).
|
|
93
|
+
For sparse, well-separated cells its masks are the result; for a dense
|
|
94
|
+
monolayer its probability and flow output becomes divergence images that
|
|
95
|
+
separate crowded, variable-shape cells and form the input the tracker runs on.
|
|
96
|
+
- **Track** across time: [Ultrack](https://github.com/royerlab/ultrack) for
|
|
97
|
+
dense monolayers, [LapTrack](https://github.com/yfukai/laptrack) for sparse,
|
|
98
|
+
well-separated cells.
|
|
99
|
+
- **Correct** tracks and labels interactively, with editing tools adapted from
|
|
100
|
+
[EpiCure](https://github.com/Image-Analysis-Hub/Epicure), where the automatic
|
|
101
|
+
result is wrong.
|
|
102
|
+
- **Quantify** what the tracked cells do, in two outputs:
|
|
103
|
+
- cell-cell contacts and edges, identified and tracked through T1 transitions,
|
|
104
|
+
written to a self-describing HDF5 (`.h5`) file.
|
|
105
|
+
- aggregate analysis of tracked nuclei, cell bodies, and cell-cell contacts
|
|
106
|
+
(shape and dynamics over time), exported to `.csv` tables.
|
|
107
|
+
|
|
108
|
+
## How it is organized
|
|
109
|
+
|
|
110
|
+
ITASC comes in two forms.
|
|
111
|
+
|
|
112
|
+
The **full app** is the integrated pipeline. A project is a directory with one
|
|
113
|
+
subfolder per position, and the four stages hand off through the `.tif` and HDF5
|
|
114
|
+
files on disk, so you can enter wherever your data already sits or pick up a
|
|
115
|
+
project someone else started.
|
|
116
|
+
|
|
117
|
+
The **solo distributions** are separate, smaller napari tools, each built for one
|
|
118
|
+
job with its own interface: `itasc-cellpose` segments and tracks channels
|
|
119
|
+
straight into viewer layers, `itasc-tracking` turns foreground and contour maps
|
|
120
|
+
into corrected Ultrack tracks, and `itasc-aggregate` quantifies contacts and T1
|
|
121
|
+
events from finished labels. They are not the full app with stages switched off:
|
|
122
|
+
each ships only the code its job needs and a leaner interface to match.
|
|
123
|
+
|
|
124
|
+
Pick the one that matches the data you have. Each row links to that tool's guide,
|
|
125
|
+
which covers how to install it and how to use it.
|
|
126
|
+
|
|
127
|
+
| If you have… | Reach for | It gives you |
|
|
128
|
+
| --- | --- | --- |
|
|
129
|
+
| **Dense, motile cells of varying shape** (a confluent monolayer), from raw stacks to quantified contacts | [itasc\[all\]](docs/manual/full-app.md) | The unified `ITASC` workflow widget, every stage end to end. |
|
|
130
|
+
| **Sparse, well-separated cells** with a cell and/or nucleus marker, to segment and track one or both channels | [itasc-cellpose](docs/manual/cellpose.md) | A local Cellpose-SAM runner for segmentation, then `laptrack` linking across time, plus manual correction of tracks and masks (adapted from [EpiCure](https://github.com/Image-Analysis-Hub/Epicure)). One channel or two. |
|
|
131
|
+
| **Foreground and contour maps already**, to skip the cellpose step | [itasc-tracking](docs/manual/tracking.md) | Ultrack candidate database, solving, browsing, and interactive segmentation and tracking correction. |
|
|
132
|
+
| **Tracked cell labels already**, and you want the aggregate quantification | [itasc-aggregate](docs/manual/aggregate.md) | Cell-cell edges, border edges, and T1 events to HDF5, aggregate quantification to `.csv`. |
|
|
133
|
+
| **Code to build on** | [itasc-core](docs/manual/core.md) | TIFF/path/label-IO helpers, the lineage model, and napari UI primitives. |
|
|
134
|
+
|
|
135
|
+
## Built on
|
|
136
|
+
|
|
137
|
+
ITASC reuses the published methods of four tools. If you use the stage that
|
|
138
|
+
depends on one, please cite it:
|
|
139
|
+
|
|
140
|
+
- **Cellpose-SAM** (segmentation): Pachitariu M, Rariden M, Stringer C.
|
|
141
|
+
*Cellpose-SAM: superhuman generalization for cellular segmentation.* bioRxiv
|
|
142
|
+
(2025). [doi:10.1101/2025.04.28.651001](https://doi.org/10.1101/2025.04.28.651001)
|
|
143
|
+
· [MouseLand/cellpose](https://github.com/MouseLand/cellpose)
|
|
144
|
+
- **Ultrack** (dense tracking): Bragantini J, et al. *Ultrack: pushing the
|
|
145
|
+
limits of cell tracking across biological scales.* Nature Methods (2025).
|
|
146
|
+
[doi:10.1038/s41592-025-02778-0](https://doi.org/10.1038/s41592-025-02778-0)
|
|
147
|
+
· [royerlab/ultrack](https://github.com/royerlab/ultrack)
|
|
148
|
+
- **LapTrack** (sparse tracking): Fukai YT, Kawaguchi K. *LapTrack: linear
|
|
149
|
+
assignment particle tracking with tunable metrics.* Bioinformatics 39(1),
|
|
150
|
+
btac799 (2023). [doi:10.1093/bioinformatics/btac799](https://doi.org/10.1093/bioinformatics/btac799)
|
|
151
|
+
· [yfukai/laptrack](https://github.com/yfukai/laptrack)
|
|
152
|
+
- **EpiCure** (correction tools): Letort G. *EpiCure: a versatile and handy tool
|
|
153
|
+
for curation of epithelial segmentation.* bioRxiv (2026).
|
|
154
|
+
[doi:10.64898/2026.03.27.714683](https://doi.org/10.64898/2026.03.27.714683)
|
|
155
|
+
· [Image-Analysis-Hub/Epicure](https://github.com/Image-Analysis-Hub/Epicure)
|
|
156
|
+
|
|
157
|
+
## Documentation
|
|
158
|
+
|
|
159
|
+
- [User guide](docs/index.md): install, the staged workflow, and driving the
|
|
160
|
+
plugin.
|
|
161
|
+
- [API reference](docs/api/index.md): the programmatic API, generated
|
|
162
|
+
from the source.
|
|
163
|
+
|
|
164
|
+
## Status
|
|
165
|
+
|
|
166
|
+
ITASC is approaching its first public release and JOSS submission. The four
|
|
167
|
+
stages and the file-based project layout are settled and in active research use.
|
|
168
|
+
Installation and the public API are close to final: expect small changes before
|
|
169
|
+
the release and its accompanying manuscript.
|
|
170
|
+
|
|
171
|
+
## Citing ITASC
|
|
172
|
+
|
|
173
|
+
Cite the software using the metadata in [`CITATION.cff`](CITATION.cff). A DOI
|
|
174
|
+
and manuscript citation will be added with the public release. For
|
|
175
|
+
pre-publication citation questions, contact Artur Ruppel at `artur@ruppel.pro`.
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
AGPL-3.0. See [`LICENSE`](LICENSE).
|
|
180
|
+
|
|
181
|
+
## AI usage
|
|
182
|
+
|
|
183
|
+
Generative AI tools (OpenAI GPT and Anthropic Claude) assisted with code
|
|
184
|
+
drafting, refactoring, tests, debugging, and documentation. Human authors made
|
|
185
|
+
the scientific, architectural, and design decisions, and are fully responsible
|
|
186
|
+
for all code and other content in the repository.
|