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,98 @@
|
|
|
1
|
+
"""A wrapping flow layout — lays items left→right and wraps to new rows.
|
|
2
|
+
|
|
3
|
+
Qt ships no wrapping layout out of the box; this is the canonical
|
|
4
|
+
``heightForWidth`` flow layout (per the Qt examples) trimmed to what the
|
|
5
|
+
correction workspace needs. Used to stack candidate thumbnails into rows that
|
|
6
|
+
reflow as the strip is resized.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from qtpy.QtCore import QMargins, QPoint, QRect, QSize, Qt
|
|
11
|
+
from qtpy.QtWidgets import QLayout
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FlowLayout(QLayout):
|
|
15
|
+
"""Left-to-right layout that wraps its items onto new rows as needed."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
parent=None,
|
|
20
|
+
*,
|
|
21
|
+
margin: int = 0,
|
|
22
|
+
h_spacing: int = 4,
|
|
23
|
+
v_spacing: int = 4,
|
|
24
|
+
) -> None:
|
|
25
|
+
super().__init__(parent)
|
|
26
|
+
self._items: list = []
|
|
27
|
+
self._h_spacing = int(h_spacing)
|
|
28
|
+
self._v_spacing = int(v_spacing)
|
|
29
|
+
self.setContentsMargins(QMargins(margin, margin, margin, margin))
|
|
30
|
+
|
|
31
|
+
# -- QLayout plumbing ---------------------------------------------------
|
|
32
|
+
def addItem(self, item) -> None:
|
|
33
|
+
self._items.append(item)
|
|
34
|
+
|
|
35
|
+
def count(self) -> int:
|
|
36
|
+
return len(self._items)
|
|
37
|
+
|
|
38
|
+
def itemAt(self, index: int):
|
|
39
|
+
if 0 <= index < len(self._items):
|
|
40
|
+
return self._items[index]
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
def takeAt(self, index: int):
|
|
44
|
+
if 0 <= index < len(self._items):
|
|
45
|
+
return self._items.pop(index)
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
def expandingDirections(self):
|
|
49
|
+
return Qt.Orientation(0)
|
|
50
|
+
|
|
51
|
+
def hasHeightForWidth(self) -> bool:
|
|
52
|
+
return True
|
|
53
|
+
|
|
54
|
+
def heightForWidth(self, width: int) -> int:
|
|
55
|
+
return self._do_layout(QRect(0, 0, width, 0), test_only=True)
|
|
56
|
+
|
|
57
|
+
def setGeometry(self, rect: QRect) -> None:
|
|
58
|
+
super().setGeometry(rect)
|
|
59
|
+
self._do_layout(rect, test_only=False)
|
|
60
|
+
|
|
61
|
+
def sizeHint(self) -> QSize:
|
|
62
|
+
return self.minimumSize()
|
|
63
|
+
|
|
64
|
+
def minimumSize(self) -> QSize:
|
|
65
|
+
size = QSize()
|
|
66
|
+
for item in self._items:
|
|
67
|
+
size = size.expandedTo(item.minimumSize())
|
|
68
|
+
margins = self.contentsMargins()
|
|
69
|
+
size += QSize(
|
|
70
|
+
margins.left() + margins.right(), margins.top() + margins.bottom()
|
|
71
|
+
)
|
|
72
|
+
return size
|
|
73
|
+
|
|
74
|
+
# -- layout maths -------------------------------------------------------
|
|
75
|
+
def _do_layout(self, rect: QRect, *, test_only: bool) -> int:
|
|
76
|
+
margins = self.contentsMargins()
|
|
77
|
+
effective = rect.adjusted(
|
|
78
|
+
margins.left(), margins.top(), -margins.right(), -margins.bottom()
|
|
79
|
+
)
|
|
80
|
+
x = effective.x()
|
|
81
|
+
y = effective.y()
|
|
82
|
+
line_height = 0
|
|
83
|
+
for item in self._items:
|
|
84
|
+
hint = item.sizeHint()
|
|
85
|
+
next_x = x + hint.width() + self._h_spacing
|
|
86
|
+
if next_x - self._h_spacing > effective.right() and line_height > 0:
|
|
87
|
+
x = effective.x()
|
|
88
|
+
y = y + line_height + self._v_spacing
|
|
89
|
+
next_x = x + hint.width() + self._h_spacing
|
|
90
|
+
line_height = 0
|
|
91
|
+
if not test_only:
|
|
92
|
+
item.setGeometry(QRect(QPoint(x, y), hint))
|
|
93
|
+
x = next_x
|
|
94
|
+
line_height = max(line_height, hint.height())
|
|
95
|
+
return y + line_height - rect.y() + margins.bottom()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
__all__ = ["FlowLayout"]
|
itasc/napari/_icons.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Crisp, accent-tinted vector icons for toolbar / stage-header action buttons.
|
|
2
|
+
|
|
3
|
+
Ported from napariTFM for visual parity across the two tools. Rendered from
|
|
4
|
+
inline SVG via QtSvg so they re-tint with the active theme and stay sharp at any
|
|
5
|
+
DPI — no bundled raster assets. Each body carries a ``{c}`` colour placeholder;
|
|
6
|
+
stroke icons inherit the wrapper stroke, filled icons override ``fill`` on their
|
|
7
|
+
own element.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from qtpy.QtCore import QByteArray, QRectF, Qt
|
|
12
|
+
from qtpy.QtGui import QIcon, QPainter, QPixmap
|
|
13
|
+
from qtpy.QtSvg import QSvgRenderer
|
|
14
|
+
|
|
15
|
+
_ICON_BODIES = {
|
|
16
|
+
# magnifier — "show this stage's data"
|
|
17
|
+
"files": '<circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.5" y2="16.5"/>',
|
|
18
|
+
# sliders — "tune this stage's parameters"
|
|
19
|
+
"params": (
|
|
20
|
+
'<line x1="3" y1="8" x2="21" y2="8"/>'
|
|
21
|
+
'<circle cx="9" cy="8" r="2.4" fill="{c}" stroke="none"/>'
|
|
22
|
+
'<line x1="3" y1="16" x2="21" y2="16"/>'
|
|
23
|
+
'<circle cx="15" cy="16" r="2.4" fill="{c}" stroke="none"/>'
|
|
24
|
+
),
|
|
25
|
+
# eye — "preview this stage"
|
|
26
|
+
"preview": (
|
|
27
|
+
'<path d="M2 12 C5 6 19 6 22 12 C19 18 5 18 2 12 Z"/>'
|
|
28
|
+
'<circle cx="12" cy="12" r="3"/>'
|
|
29
|
+
),
|
|
30
|
+
# play triangle — "run this stage"
|
|
31
|
+
"run": '<path d="M8 5 L19 12 L8 19 Z" fill="{c}" stroke="none"/>',
|
|
32
|
+
# stop square — "cancel the running stage"
|
|
33
|
+
"cancel": '<rect x="6.5" y="6.5" width="11" height="11" rx="1.5" fill="{c}" stroke="none"/>',
|
|
34
|
+
# power — "enable / disable this stage"
|
|
35
|
+
"power": (
|
|
36
|
+
'<path d="M12 3 L12 11"/>'
|
|
37
|
+
'<path d="M6.4 6.8 A8 8 0 1 0 17.6 6.8"/>'
|
|
38
|
+
),
|
|
39
|
+
# plus — "add folders"
|
|
40
|
+
"plus": '<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>',
|
|
41
|
+
# crosshair target — "auto-select the optimal regularization (GCV)"
|
|
42
|
+
"gcv": (
|
|
43
|
+
'<circle cx="12" cy="12" r="6.5"/>'
|
|
44
|
+
'<line x1="12" y1="1.5" x2="12" y2="5"/>'
|
|
45
|
+
'<line x1="12" y1="19" x2="12" y2="22.5"/>'
|
|
46
|
+
'<line x1="1.5" y1="12" x2="5" y2="12"/>'
|
|
47
|
+
'<line x1="19" y1="12" x2="22.5" y2="12"/>'
|
|
48
|
+
),
|
|
49
|
+
# page with a folded corner + plus — "start a new project"
|
|
50
|
+
"new": (
|
|
51
|
+
'<path d="M6 3 H14 L18 7 V21 H6 Z"/>'
|
|
52
|
+
'<path d="M14 3 V7 H18"/>'
|
|
53
|
+
'<path d="M9 15 H15"/>'
|
|
54
|
+
'<path d="M12 12 V18"/>'
|
|
55
|
+
),
|
|
56
|
+
# tabbed folder — "load / open a project or a preset"
|
|
57
|
+
"load": '<path d="M3 6 H9 L11 8 H21 V19 H3 Z"/>',
|
|
58
|
+
# floppy disk — "save a project or a preset"
|
|
59
|
+
"save": (
|
|
60
|
+
'<path d="M5 4 H15.5 L20 8.5 V20 H5 Z"/>'
|
|
61
|
+
'<path d="M8 20 V14 H16 V20"/>'
|
|
62
|
+
'<path d="M9 4 V8 H13.5 V4"/>'
|
|
63
|
+
),
|
|
64
|
+
# counterclockwise rewind — "reset parameters / reload"
|
|
65
|
+
"reset": (
|
|
66
|
+
'<path d="M5.5 6.5 A8.5 8.5 0 1 0 18 6"/>'
|
|
67
|
+
'<path d="M6.06 8.89 L6.80 4.01 L2.94 7.08"/>'
|
|
68
|
+
),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
_SVG_TEMPLATE = (
|
|
72
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" '
|
|
73
|
+
'fill="none" stroke="{c}" stroke-width="{w}" '
|
|
74
|
+
'stroke-linecap="{cap}" stroke-linejoin="{join}">{body}</svg>'
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
ICON_NAMES = tuple(_ICON_BODIES)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _render(svg: str, size: int) -> QPixmap:
|
|
81
|
+
pixmap = QPixmap(size, size)
|
|
82
|
+
pixmap.fill(Qt.transparent)
|
|
83
|
+
renderer = QSvgRenderer(QByteArray(svg.encode("utf-8")))
|
|
84
|
+
painter = QPainter(pixmap)
|
|
85
|
+
renderer.render(painter, QRectF(0, 0, size, size))
|
|
86
|
+
painter.end()
|
|
87
|
+
return pixmap
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def stage_action_pixmap(
|
|
91
|
+
name: str,
|
|
92
|
+
color: str,
|
|
93
|
+
size: int = 18,
|
|
94
|
+
stroke_width: float = 2.0,
|
|
95
|
+
linecap: str = "round",
|
|
96
|
+
linejoin: str = "round",
|
|
97
|
+
) -> QPixmap:
|
|
98
|
+
"""Render a single tinted action icon to a transparent pixmap."""
|
|
99
|
+
body = _ICON_BODIES[name].format(c=color)
|
|
100
|
+
svg = _SVG_TEMPLATE.format(
|
|
101
|
+
c=color, w=stroke_width, cap=linecap, join=linejoin, body=body
|
|
102
|
+
)
|
|
103
|
+
return _render(svg, size)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def stage_action_icon(
|
|
107
|
+
name: str,
|
|
108
|
+
color: str,
|
|
109
|
+
disabled_color: str | None = None,
|
|
110
|
+
size: int = 18,
|
|
111
|
+
stroke_width: float = 2.0,
|
|
112
|
+
linecap: str = "round",
|
|
113
|
+
linejoin: str = "round",
|
|
114
|
+
) -> QIcon:
|
|
115
|
+
"""A QIcon for an action button, optionally carrying a dimmed disabled mode."""
|
|
116
|
+
icon = QIcon(
|
|
117
|
+
stage_action_pixmap(name, color, size, stroke_width, linecap, linejoin)
|
|
118
|
+
)
|
|
119
|
+
if disabled_color is not None:
|
|
120
|
+
icon.addPixmap(
|
|
121
|
+
stage_action_pixmap(
|
|
122
|
+
name, disabled_color, size, stroke_width, linecap, linejoin
|
|
123
|
+
),
|
|
124
|
+
QIcon.Disabled,
|
|
125
|
+
)
|
|
126
|
+
return icon
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Compatibility patches for napari/Qt integration edge cases."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from qtpy.QtCore import Qt
|
|
5
|
+
from qtpy.QtGui import QPixmap
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _index_size_hint(index):
|
|
9
|
+
if index is None or (hasattr(index, "isValid") and not index.isValid()):
|
|
10
|
+
return None
|
|
11
|
+
size_hint = index.data(Qt.ItemDataRole.SizeHintRole)
|
|
12
|
+
if size_hint is None or not hasattr(size_hint, "height"):
|
|
13
|
+
return None
|
|
14
|
+
return size_hint
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def patch_napari_layer_delegate() -> None:
|
|
18
|
+
"""Make napari's layer delegate tolerate transient indexes without size hints."""
|
|
19
|
+
try:
|
|
20
|
+
from napari._qt.containers._layer_delegate import LayerDelegate
|
|
21
|
+
from napari._qt.containers.qt_layer_model import LoadedRole, ThumbnailRole
|
|
22
|
+
except Exception:
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
if getattr(LayerDelegate, "_itasc_missing_size_hint_patch", False):
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
def _paint_loading(self, painter, option, index):
|
|
29
|
+
loaded = index.data(LoadedRole)
|
|
30
|
+
if loaded:
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
size_hint = _index_size_hint(index)
|
|
34
|
+
if size_hint is None:
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
self._load_movie.start()
|
|
38
|
+
load_rect = option.rect.translated(4, 8)
|
|
39
|
+
h = size_hint.height() - 16
|
|
40
|
+
load_rect.setWidth(h)
|
|
41
|
+
load_rect.setHeight(h)
|
|
42
|
+
painter.drawPixmap(load_rect, self._load_movie.currentPixmap())
|
|
43
|
+
|
|
44
|
+
def _paint_thumbnail(self, painter, option, index):
|
|
45
|
+
loaded = index.data(LoadedRole)
|
|
46
|
+
if not loaded:
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
size_hint = _index_size_hint(index)
|
|
50
|
+
if size_hint is None:
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
all_loaded = index.model().sourceModel().all_loaded()
|
|
54
|
+
if all_loaded:
|
|
55
|
+
self._load_movie.setPaused(True)
|
|
56
|
+
|
|
57
|
+
thumb_rect = option.rect.translated(-2, 2)
|
|
58
|
+
h = size_hint.height() - 4
|
|
59
|
+
thumb_rect.setWidth(h)
|
|
60
|
+
thumb_rect.setHeight(h)
|
|
61
|
+
image = index.data(ThumbnailRole)
|
|
62
|
+
painter.drawPixmap(thumb_rect, QPixmap.fromImage(image))
|
|
63
|
+
|
|
64
|
+
LayerDelegate._paint_loading = _paint_loading
|
|
65
|
+
LayerDelegate._paint_thumbnail = _paint_thumbnail
|
|
66
|
+
LayerDelegate._itasc_missing_size_hint_patch = True
|
|
67
|
+
|
|
68
|
+
patch_vispy_units_scale_guard()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def patch_vispy_units_scale_guard() -> None:
|
|
72
|
+
"""Stop a canvas-draw ``IndexError`` from a layer whose per-axis units scale
|
|
73
|
+
is shorter than its displayed dims.
|
|
74
|
+
|
|
75
|
+
napari's ``VispyBaseLayer._on_matrix_change`` indexes
|
|
76
|
+
``self._world_to_layer_units_scale`` by ``self.layer._slice_input.displayed``.
|
|
77
|
+
When physical units (e.g. an OME-TIFF's ``(pixel, pixel)``) reach a layer with
|
|
78
|
+
more dimensions than the units tuple — a calibrated 2D stack shown in a 3D
|
|
79
|
+
viewer — ``_recalculate_units_scale``'s ``strict=False`` zip yields a units
|
|
80
|
+
scale shorter than ``dims_displayed``, and indexing ``[2]`` overruns:
|
|
81
|
+
|
|
82
|
+
IndexError: tuple index out of range (canvas.py → _on_matrix_change)
|
|
83
|
+
|
|
84
|
+
The unresolved axes have no calibration, so their scale is just napari's
|
|
85
|
+
default of ``1.0``. Pad the units scale up to the displayed-dim extent with
|
|
86
|
+
``1.0`` before delegating — semantically the identity, so the only behaviour
|
|
87
|
+
change is that the draw no longer crashes."""
|
|
88
|
+
try:
|
|
89
|
+
from napari._vispy.layers.base import VispyBaseLayer
|
|
90
|
+
except Exception: # pragma: no cover - napari layout changed / unavailable
|
|
91
|
+
return
|
|
92
|
+
if getattr(VispyBaseLayer, "_itasc_units_scale_guard", False):
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
original_on_matrix_change = VispyBaseLayer._on_matrix_change
|
|
96
|
+
|
|
97
|
+
def _guarded_on_matrix_change(self):
|
|
98
|
+
try:
|
|
99
|
+
self._world_to_layer_units_scale = _padded_units_scale(
|
|
100
|
+
self._world_to_layer_units_scale, self.layer._slice_input.displayed
|
|
101
|
+
)
|
|
102
|
+
except Exception: # pragma: no cover - never let the guard itself break draw
|
|
103
|
+
pass
|
|
104
|
+
return original_on_matrix_change(self)
|
|
105
|
+
|
|
106
|
+
VispyBaseLayer._on_matrix_change = _guarded_on_matrix_change
|
|
107
|
+
VispyBaseLayer._itasc_units_scale_guard = True
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _padded_units_scale(scale, displayed) -> tuple:
|
|
111
|
+
"""*scale* extended with ``1.0`` (the no-calibration default) so every index
|
|
112
|
+
in *displayed* is in range; unchanged when it already covers them."""
|
|
113
|
+
scale = tuple(scale)
|
|
114
|
+
needed = (max(displayed) + 1) if len(displayed) else 0
|
|
115
|
+
if len(scale) >= needed:
|
|
116
|
+
return scale
|
|
117
|
+
return scale + (1.0,) * (needed - len(scale))
|
|
118
|
+
|
itasc/napari/_paths.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""On-disk artifact paths for ITASC position directories.
|
|
2
|
+
|
|
3
|
+
Canonical file layout under ``<pos_dir>/``:
|
|
4
|
+
|
|
5
|
+
0_input/ — cell_zavg.tif, nucleus_zavg.tif, NLS_zavg.tif
|
|
6
|
+
1_cellpose/ — nucleus_prob.tif, nucleus_dp.tif,
|
|
7
|
+
cell_prob.tif, cell_dp.tif,
|
|
8
|
+
nucleus_contours.tif, nucleus_foreground.tif,
|
|
9
|
+
cell_contours.tif, cell_foreground.tif
|
|
10
|
+
2_nucleus/ — contour_sources.tif, foreground_sources.tif,
|
|
11
|
+
tracked_labels.tif, ultrack_workdir/data.db
|
|
12
|
+
3_cell/ — filtered_dp.tif, foreground_masks.tif,
|
|
13
|
+
contours.tif, foreground_scores.tif,
|
|
14
|
+
tracked_labels.tif
|
|
15
|
+
nucleus_labels.tif — FINAL output: committed nucleus tracked labels
|
|
16
|
+
cell_labels.tif — FINAL output: committed cell tracked labels
|
|
17
|
+
contact_analysis.h5 — FINAL output: contact graph built from the labels
|
|
18
|
+
|
|
19
|
+
The numbered stage dirs hold re-runnable *working* artifacts; the base-folder
|
|
20
|
+
``nucleus_labels.tif`` / ``cell_labels.tif`` are the committed, downstream-stable
|
|
21
|
+
*final* outputs (see :func:`itasc.core.commit.promote_labels`), and
|
|
22
|
+
``contact_analysis.h5`` — the graph derived from them — sits beside them. Contact
|
|
23
|
+
Analysis discovery defaults to these committed names.
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class NucleusArtifactPaths:
|
|
33
|
+
"""Resolve nucleus-workflow artifact locations under a position directory."""
|
|
34
|
+
|
|
35
|
+
pos_dir: Path
|
|
36
|
+
|
|
37
|
+
# 0_input
|
|
38
|
+
@property
|
|
39
|
+
def cell_zavg(self) -> Path:
|
|
40
|
+
return self.pos_dir / "0_input" / "cell_zavg.tif"
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def nucleus_zavg(self) -> Path:
|
|
44
|
+
return self.pos_dir / "0_input" / "nucleus_zavg.tif"
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def nls_zavg(self) -> Path:
|
|
48
|
+
return self.pos_dir / "0_input" / "NLS_zavg.tif"
|
|
49
|
+
|
|
50
|
+
# 1_cellpose — Cellpose outputs
|
|
51
|
+
@property
|
|
52
|
+
def prob(self) -> Path:
|
|
53
|
+
return self.pos_dir / "1_cellpose" / "nucleus_prob.tif"
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def cell_prob(self) -> Path:
|
|
57
|
+
return self.pos_dir / "1_cellpose" / "cell_prob.tif"
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def dp(self) -> Path:
|
|
61
|
+
return self.pos_dir / "1_cellpose" / "nucleus_dp.tif"
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def cell_dp(self) -> Path:
|
|
65
|
+
return self.pos_dir / "1_cellpose" / "cell_dp.tif"
|
|
66
|
+
|
|
67
|
+
# 1_cellpose — Divergence-map outputs (per channel)
|
|
68
|
+
@property
|
|
69
|
+
def nucleus_contours(self) -> Path:
|
|
70
|
+
return self.pos_dir / "1_cellpose" / "nucleus_contours.tif"
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def nucleus_foreground(self) -> Path:
|
|
74
|
+
return self.pos_dir / "1_cellpose" / "nucleus_foreground.tif"
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def cell_contours(self) -> Path:
|
|
78
|
+
return self.pos_dir / "1_cellpose" / "cell_contours.tif"
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def cell_foreground(self) -> Path:
|
|
82
|
+
return self.pos_dir / "1_cellpose" / "cell_foreground.tif"
|
|
83
|
+
|
|
84
|
+
# Aliases for the nucleus channel — historical names retained for callers
|
|
85
|
+
# that don't need a channel suffix (e.g. tracking-input writers).
|
|
86
|
+
@property
|
|
87
|
+
def contours(self) -> Path:
|
|
88
|
+
return self.nucleus_contours
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def foreground(self) -> Path:
|
|
92
|
+
return self.nucleus_foreground
|
|
93
|
+
|
|
94
|
+
# 2_nucleus
|
|
95
|
+
@property
|
|
96
|
+
def nucleus_dir(self) -> Path:
|
|
97
|
+
return self.pos_dir / "2_nucleus"
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def contour_sources(self) -> Path:
|
|
101
|
+
return self.nucleus_dir / "contour_sources.tif"
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def foreground_sources(self) -> Path:
|
|
105
|
+
return self.nucleus_dir / "foreground_sources.tif"
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def tracked(self) -> Path:
|
|
109
|
+
return self.nucleus_dir / "tracked_labels.tif"
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def ultrack_workdir(self) -> Path:
|
|
113
|
+
return self.nucleus_dir / "ultrack_workdir"
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def ultrack_db(self) -> Path:
|
|
117
|
+
return self.ultrack_workdir / "data.db"
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def nucleus_atoms(self) -> Path:
|
|
121
|
+
return self.nucleus_dir / "atoms.tif"
|
|
122
|
+
|
|
123
|
+
# 3_cell working output (the cell-workflow tracked labels, pre-commit)
|
|
124
|
+
@property
|
|
125
|
+
def cell_tracked(self) -> Path:
|
|
126
|
+
return self.pos_dir / "3_cell" / "tracked_labels.tif"
|
|
127
|
+
|
|
128
|
+
# Final outputs (committed) — base folder, downstream-stable
|
|
129
|
+
@property
|
|
130
|
+
def nucleus_labels(self) -> Path:
|
|
131
|
+
return self.pos_dir / "nucleus_labels.tif"
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def cell_labels(self) -> Path:
|
|
135
|
+
return self.pos_dir / "cell_labels.tif"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@dataclass(frozen=True)
|
|
139
|
+
class NucleusWorkspace:
|
|
140
|
+
"""Single base directory for the nucleus tracking/correction piece.
|
|
141
|
+
|
|
142
|
+
All artifacts (database, tracked labels, atoms, source maps) and the
|
|
143
|
+
annotation store (validation/correction JSONs) live directly under
|
|
144
|
+
``nucleus_dir``; the two foreground/contour *inputs* are explicit paths.
|
|
145
|
+
|
|
146
|
+
Three layouts are supported via the constructors:
|
|
147
|
+
|
|
148
|
+
* :meth:`staged` — the full orchestrator: ``nucleus_dir`` is
|
|
149
|
+
``<pos>/2_nucleus`` and the inputs come from ``<pos>/1_cellpose``.
|
|
150
|
+
* :meth:`flat` — a standalone working directory that holds
|
|
151
|
+
``foreground.tif`` + ``contours.tif`` and receives every output.
|
|
152
|
+
* :meth:`files` — the standalone tracking piece: explicit foreground and
|
|
153
|
+
contour file paths (any name, any location) plus an output directory that
|
|
154
|
+
receives every output.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
nucleus_dir: Path
|
|
158
|
+
foreground: Path
|
|
159
|
+
contours: Path
|
|
160
|
+
# Optional cross-stage cell foreground, used purely as a correction-mode
|
|
161
|
+
# background overlay. Only the staged orchestrator supplies it.
|
|
162
|
+
cell_foreground: Path | None = None
|
|
163
|
+
|
|
164
|
+
@classmethod
|
|
165
|
+
def staged(cls, pos_dir: Path) -> NucleusWorkspace:
|
|
166
|
+
paths = NucleusArtifactPaths(Path(pos_dir))
|
|
167
|
+
return cls(
|
|
168
|
+
nucleus_dir=paths.nucleus_dir,
|
|
169
|
+
foreground=paths.nucleus_foreground,
|
|
170
|
+
contours=paths.nucleus_contours,
|
|
171
|
+
cell_foreground=paths.cell_foreground,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
@classmethod
|
|
175
|
+
def flat(cls, work_dir: Path) -> NucleusWorkspace:
|
|
176
|
+
work_dir = Path(work_dir)
|
|
177
|
+
return cls(
|
|
178
|
+
nucleus_dir=work_dir,
|
|
179
|
+
foreground=work_dir / "foreground.tif",
|
|
180
|
+
contours=work_dir / "contours.tif",
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
@classmethod
|
|
184
|
+
def files(
|
|
185
|
+
cls,
|
|
186
|
+
*,
|
|
187
|
+
foreground: Path | str,
|
|
188
|
+
contours: Path | str,
|
|
189
|
+
output_dir: Path | str | None = None,
|
|
190
|
+
) -> NucleusWorkspace:
|
|
191
|
+
"""Explicit foreground/contour file paths + an output directory.
|
|
192
|
+
|
|
193
|
+
The two inputs may have any name and live anywhere; every output is
|
|
194
|
+
written under ``output_dir`` (defaulting to the foreground file's parent
|
|
195
|
+
when omitted).
|
|
196
|
+
"""
|
|
197
|
+
foreground = Path(foreground)
|
|
198
|
+
contours = Path(contours)
|
|
199
|
+
nucleus_dir = Path(output_dir) if output_dir else foreground.parent
|
|
200
|
+
return cls(
|
|
201
|
+
nucleus_dir=nucleus_dir,
|
|
202
|
+
foreground=foreground,
|
|
203
|
+
contours=contours,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Artifacts (outputs), all directly under nucleus_dir
|
|
207
|
+
@property
|
|
208
|
+
def contour_sources(self) -> Path:
|
|
209
|
+
return self.nucleus_dir / "contour_sources.tif"
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def foreground_sources(self) -> Path:
|
|
213
|
+
return self.nucleus_dir / "foreground_sources.tif"
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def tracked(self) -> Path:
|
|
217
|
+
return self.nucleus_dir / "tracked_labels.tif"
|
|
218
|
+
|
|
219
|
+
@property
|
|
220
|
+
def ultrack_workdir(self) -> Path:
|
|
221
|
+
return self.nucleus_dir / "ultrack_workdir"
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def ultrack_db(self) -> Path:
|
|
225
|
+
return self.ultrack_workdir / "data.db"
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def atoms(self) -> Path:
|
|
229
|
+
return self.nucleus_dir / "atoms.tif"
|
|
230
|
+
|
|
231
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""A per-frame result cache for the live-preview widgets.
|
|
2
|
+
|
|
3
|
+
Every live preview recomputes the current frame off the GUI thread on each param
|
|
4
|
+
edit or time scrub. Without a cache, scrubbing back to a frame that was just
|
|
5
|
+
computed re-runs the (often heavy) pipeline for an identical result. This cache
|
|
6
|
+
holds whatever a compute produced for each visited frame, keyed on a *signature*
|
|
7
|
+
of the parameters that determine the result, so:
|
|
8
|
+
|
|
9
|
+
* scrubbing back to an already-computed frame repaints instantly, and
|
|
10
|
+
* any param edit (a changed signature) drops every cached frame at once.
|
|
11
|
+
|
|
12
|
+
It mirrors the per-frame cache the cell-segmentation preview
|
|
13
|
+
(:mod:`itasc.napari.cell_workflow_widget`) grew first; the other previews
|
|
14
|
+
share this implementation rather than re-deriving it.
|
|
15
|
+
|
|
16
|
+
Stale-write safety: a worker started under an old signature may finish *after* an
|
|
17
|
+
edit has moved the cache to a new one. The owner adopts the current signature up
|
|
18
|
+
front with :meth:`sync`, reads with :meth:`get`, and stores with :meth:`put`
|
|
19
|
+
passing the signature the result was computed under — :meth:`put` ignores the
|
|
20
|
+
write when that signature is no longer current, so a late worker can never poison
|
|
21
|
+
the fresh cache.
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
_UNSET = object()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class FramePreviewCache:
|
|
31
|
+
"""Frame ``t`` → result, valid only while the parameter signature holds."""
|
|
32
|
+
|
|
33
|
+
def __init__(self) -> None:
|
|
34
|
+
self._key: Any = _UNSET
|
|
35
|
+
self._frames: dict[int, Any] = {}
|
|
36
|
+
|
|
37
|
+
def sync(self, key: Any) -> None:
|
|
38
|
+
"""Adopt *key* as the current signature, dropping all frames if it changed."""
|
|
39
|
+
if key != self._key:
|
|
40
|
+
self._key = key
|
|
41
|
+
self._frames = {}
|
|
42
|
+
|
|
43
|
+
def get(self, t: int) -> Any | None:
|
|
44
|
+
"""The cached result for frame *t* under the current signature, or ``None``."""
|
|
45
|
+
return self._frames.get(t)
|
|
46
|
+
|
|
47
|
+
def put(self, key: Any, t: int, value: Any) -> None:
|
|
48
|
+
"""Store *value* for frame *t* — ignored when *key* is no longer current.
|
|
49
|
+
|
|
50
|
+
Call :meth:`sync` with the live signature before reading; pass the
|
|
51
|
+
signature the *value* was computed under here so a worker that finishes
|
|
52
|
+
after an edit cannot overwrite the re-keyed cache.
|
|
53
|
+
"""
|
|
54
|
+
if key == self._key:
|
|
55
|
+
self._frames[t] = value
|
|
56
|
+
|
|
57
|
+
def clear(self) -> None:
|
|
58
|
+
"""Forget every frame and the signature (e.g. when the preview deactivates)."""
|
|
59
|
+
self._key = _UNSET
|
|
60
|
+
self._frames = {}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Shared selection-highlight rendering: turn a boolean cell mask into the RGBA
|
|
2
|
+
overlay the correction studio paints over the labels.
|
|
3
|
+
|
|
4
|
+
Two visual cues, independently toggleable:
|
|
5
|
+
|
|
6
|
+
* ``dim`` — uniformly dim everything *outside* the mask (the "spotlight"), so the
|
|
7
|
+
selected cell stays at full brightness while its surroundings recede.
|
|
8
|
+
* ``border`` — draw an opaque yellow ring just outside the mask.
|
|
9
|
+
|
|
10
|
+
The correction widget uses one cue at a time (its ``spotlight`` / ``border``
|
|
11
|
+
styles); the aggregate-quantification click-to-load uses both at once so a picked
|
|
12
|
+
cell reads clearly against the full labels movie it sits in.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
from scipy.ndimage import binary_dilation
|
|
18
|
+
|
|
19
|
+
#: Alpha applied outside the mask in spotlight (``dim``) mode.
|
|
20
|
+
SPOTLIGHT_OPACITY = 0.7
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def spotlight_rgba(
|
|
24
|
+
mask: np.ndarray,
|
|
25
|
+
*,
|
|
26
|
+
dim: bool = True,
|
|
27
|
+
border: bool = False,
|
|
28
|
+
opacity: float = SPOTLIGHT_OPACITY,
|
|
29
|
+
ring_iterations: int = 2,
|
|
30
|
+
) -> np.ndarray:
|
|
31
|
+
"""RGBA (``mask.shape + (4,)``, float32) overlay highlighting *mask*.
|
|
32
|
+
|
|
33
|
+
With *dim*, everything outside the mask is washed at *opacity* and the mask
|
|
34
|
+
itself left fully transparent. With *border*, an opaque yellow ring of
|
|
35
|
+
*ring_iterations* px is drawn just outside the mask. An all-False mask yields
|
|
36
|
+
an all-zero (fully transparent) overlay.
|
|
37
|
+
"""
|
|
38
|
+
data = np.zeros(mask.shape + (4,), dtype=np.float32)
|
|
39
|
+
if dim:
|
|
40
|
+
alpha = np.full(mask.shape, opacity, dtype=np.float32)
|
|
41
|
+
alpha[mask] = 0.0
|
|
42
|
+
data[..., 3] = alpha
|
|
43
|
+
if border:
|
|
44
|
+
ring = binary_dilation(mask, iterations=ring_iterations) & ~mask
|
|
45
|
+
data[ring] = (1.0, 1.0, 0.0, 1.0) # opaque yellow outline
|
|
46
|
+
return data
|