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,7 @@
|
|
|
1
|
+
"""Quantifier plugins.
|
|
2
|
+
|
|
3
|
+
Dropping a ``*.py`` module here whose class subclasses
|
|
4
|
+
:class:`itasc.contact_analysis.quantifier.Quantifier` (with a non-empty
|
|
5
|
+
``quantity_id``) registers a new per-position quantity. ``available_quantifiers``
|
|
6
|
+
imports every module here so they self-register.
|
|
7
|
+
"""
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Shared plumbing for the contacts-*derived* quantifiers.
|
|
2
|
+
|
|
3
|
+
The neighbor count / density / signed-contact-length quantities are all computed
|
|
4
|
+
from a position's already-built ``contact_analysis.h5``. They are **pooled**
|
|
5
|
+
quantities: each runs its compute function over the contacts artifact at aggregate
|
|
6
|
+
time (in :meth:`Quantifier.compute_object_table`) and the tidy table goes straight
|
|
7
|
+
into the pooled output — no per-position artifact is persisted.
|
|
8
|
+
|
|
9
|
+
This module factors out the one thing those quantifiers share: loading the
|
|
10
|
+
contacts artifact for a position. All of it is **label-agnostic**.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from itasc.contact_analysis.contacts.reader import (
|
|
17
|
+
PositionContactAnalysis,
|
|
18
|
+
read_position_contacts,
|
|
19
|
+
)
|
|
20
|
+
from itasc.contact_analysis.quantifier import PositionInputs
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"PositionContactAnalysis",
|
|
24
|
+
"load_analysis",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load_analysis(inputs: PositionInputs) -> PositionContactAnalysis:
|
|
29
|
+
"""Read the position's contacts artifact, or raise a clear error.
|
|
30
|
+
|
|
31
|
+
The derived quantifiers ``require`` ``contact_analysis_path``, so a missing /
|
|
32
|
+
not-yet-built file is a real precondition failure (a position lacking its
|
|
33
|
+
``contact_analysis.h5`` is scoped out before pooling) rather than a silent
|
|
34
|
+
skip.
|
|
35
|
+
"""
|
|
36
|
+
path = inputs.contact_analysis_path
|
|
37
|
+
if path is None or not Path(path).is_file():
|
|
38
|
+
raise FileNotFoundError(
|
|
39
|
+
"contact_analysis.h5 not found — build 'Cell–cell contacts' for this "
|
|
40
|
+
f"position first (looked for {path!r})."
|
|
41
|
+
)
|
|
42
|
+
return read_position_contacts(path)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Cell-density quantifier — per-frame cell counts per field-of-view area.
|
|
2
|
+
|
|
3
|
+
Counts cells straight off the tracked **cell labels** (unique non-zero labels per
|
|
4
|
+
frame), so it is a cell-labels quantity, not a contacts-derived one. The
|
|
5
|
+
field-of-view area is **required** — it comes from the shared params bar and there
|
|
6
|
+
is no silent image-area fallback; ``density = n_cells / fov_area_mm2`` in
|
|
7
|
+
cells/mm². One ``all`` total row per frame (label-agnostic). A **pooled** quantity:
|
|
8
|
+
:meth:`compute_object_table` builds the tidy table in memory (no per-position
|
|
9
|
+
artifact).
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from collections.abc import Mapping
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
from itasc.contact_analysis.contacts.neighborhood import cell_density
|
|
19
|
+
from itasc.contact_analysis.quantifier import PositionInputs, Quantifier
|
|
20
|
+
from itasc.contact_analysis.shape.core import read_label_stack
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CellDensityQuantifier(Quantifier):
|
|
24
|
+
"""Per ``(frame, label)`` cell count and density (cells/mm²)."""
|
|
25
|
+
|
|
26
|
+
quantity_id = "cell_density"
|
|
27
|
+
display_name = "Cell density"
|
|
28
|
+
# Cell labels are the only hard input — counts come straight off them. The
|
|
29
|
+
# field-of-view area is a required build *param* (validated in
|
|
30
|
+
# ``compute_object_table``), not a PositionInputs field.
|
|
31
|
+
requires = ("cell_labels_path",)
|
|
32
|
+
# Density is keyed per (frame, label); ``label`` is always ``"all"``.
|
|
33
|
+
table_keys = ("frame", "label")
|
|
34
|
+
wants_build_params = True
|
|
35
|
+
# The field-of-view area has no image-area fallback, so it is a hard build
|
|
36
|
+
# requirement: the pooling loop skips Cell density (and the UI greys it out)
|
|
37
|
+
# until it is set, instead of letting the compute raise.
|
|
38
|
+
required_build_params = {"fov_area_mm2": "FOV area (mm²)"}
|
|
39
|
+
|
|
40
|
+
def compute_object_table(
|
|
41
|
+
self, inputs: PositionInputs, *, params: dict | None = None
|
|
42
|
+
) -> Mapping[str, np.ndarray]:
|
|
43
|
+
fov = (params or {}).get("fov_area_mm2")
|
|
44
|
+
if not (isinstance(fov, (int, float)) and fov > 0):
|
|
45
|
+
raise ValueError(
|
|
46
|
+
"Cell density requires a field-of-view area — set 'FOV area (mm²)' "
|
|
47
|
+
"in Parameters before building."
|
|
48
|
+
)
|
|
49
|
+
frame_cells = _frame_cells_from_labels(inputs.cell_labels_path)
|
|
50
|
+
return cell_density(frame_cells, fov_area_mm2=float(fov))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _frame_cells_from_labels(labels_path: Path | None) -> dict[int, list[int]]:
|
|
54
|
+
"""``frame -> [cell_id, …]`` from a tracked cell-label TIFF.
|
|
55
|
+
|
|
56
|
+
The unique non-zero labels of frame *i* are the cells present in it — the same
|
|
57
|
+
set the contacts ``cells`` table holds (it is one ``regionprops`` row per
|
|
58
|
+
label), so this reproduces the old per-frame counts without the artifact.
|
|
59
|
+
"""
|
|
60
|
+
stack = read_label_stack(Path(labels_path))
|
|
61
|
+
out: dict[int, list[int]] = {}
|
|
62
|
+
for frame_idx, frame in enumerate(stack):
|
|
63
|
+
ids = np.unique(frame)
|
|
64
|
+
out[frame_idx] = [int(i) for i in ids if i != 0]
|
|
65
|
+
return out
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Cell-dynamics quantifier — the registry adapter over the dynamics core.
|
|
2
|
+
|
|
3
|
+
Wraps :mod:`itasc.contact_analysis.dynamics` so the studio can build
|
|
4
|
+
and read per-cell motion through the generic :class:`Quantifier` interface. Its
|
|
5
|
+
persistence is a multi-table ``4_contact_analysis/cell_dynamics.h5``;
|
|
6
|
+
:meth:`object_table` exposes the per-frame instantaneous table (the only
|
|
7
|
+
``(frame, cell_id)`` table) to the plotting backend. The cell twin of
|
|
8
|
+
:mod:`.nucleus_dynamics` — they share the label-agnostic core and differ only by
|
|
9
|
+
which label field they read and their output filename.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from collections.abc import Callable, Mapping
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
from itasc.contact_analysis.dynamics import (
|
|
19
|
+
TrackDynamics,
|
|
20
|
+
build_track_dynamics,
|
|
21
|
+
read_instantaneous_table,
|
|
22
|
+
read_track_dynamics,
|
|
23
|
+
)
|
|
24
|
+
from itasc.contact_analysis.dynamics.kinematics import instantaneous_table
|
|
25
|
+
from itasc.contact_analysis.dynamics.trajectories import extract_trajectories
|
|
26
|
+
from itasc.contact_analysis.quantifier import PositionInputs, Quantifier
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CellDynamicsQuantifier(Quantifier):
|
|
30
|
+
"""Quantifies per-cell motion (speed, persistence, MSD, collective) from cell labels."""
|
|
31
|
+
|
|
32
|
+
quantity_id = "cell_dynamics"
|
|
33
|
+
display_name = "Cell dynamics"
|
|
34
|
+
requires = ("cell_labels_path",)
|
|
35
|
+
# Pixel size (µm/px) + frame interval (s/frame) are global build params, set
|
|
36
|
+
# once in the Parameters panel and applied to every position, so each output
|
|
37
|
+
# lands in physical units. They gate the build (unset ⇒ not buildable) and
|
|
38
|
+
# reach :meth:`build` via the stamped ``PositionInputs``, not the params dict.
|
|
39
|
+
required_build_params = {
|
|
40
|
+
"pixel_size_um": "pixel size (µm/px)",
|
|
41
|
+
"time_interval_s": "frame interval (s)",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
default_output_name = "cell_dynamics.h5"
|
|
45
|
+
# The object_table is the per-(frame, cell_id) instantaneous motion table. The
|
|
46
|
+
# per-track / per-tissue / curve sub-tables are separate views (tracks / frames
|
|
47
|
+
# / dac_curves) and are not this quantifier's object_table.
|
|
48
|
+
table_keys = ("frame", "cell_id")
|
|
49
|
+
|
|
50
|
+
def build(
|
|
51
|
+
self,
|
|
52
|
+
inputs: PositionInputs,
|
|
53
|
+
output_path: Path,
|
|
54
|
+
*,
|
|
55
|
+
params: dict | None = None,
|
|
56
|
+
progress_cb: Callable[[int, int, str], None] | None = None,
|
|
57
|
+
) -> Path:
|
|
58
|
+
return build_track_dynamics(
|
|
59
|
+
inputs.cell_labels_path,
|
|
60
|
+
output_path,
|
|
61
|
+
pixel_size_um=inputs.pixel_size_um,
|
|
62
|
+
time_interval_s=inputs.time_interval_s,
|
|
63
|
+
source_path=inputs.position_dir,
|
|
64
|
+
params=params,
|
|
65
|
+
quantity_id=self.quantity_id,
|
|
66
|
+
progress_cb=progress_cb,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def read(self, output_path: Path) -> TrackDynamics:
|
|
70
|
+
return read_track_dynamics(output_path)
|
|
71
|
+
|
|
72
|
+
def object_table(self, output_path: Path) -> Mapping[str, np.ndarray]:
|
|
73
|
+
return read_instantaneous_table(output_path)
|
|
74
|
+
|
|
75
|
+
def compute_object_table(
|
|
76
|
+
self, inputs: PositionInputs, *, params: dict | None = None
|
|
77
|
+
) -> Mapping[str, np.ndarray]:
|
|
78
|
+
trajectories = extract_trajectories(
|
|
79
|
+
inputs.cell_labels_path, pixel_size_um=inputs.pixel_size_um
|
|
80
|
+
)
|
|
81
|
+
return instantaneous_table(trajectories, time_interval_s=inputs.time_interval_s)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Cell-shape quantifier — the registry adapter over the shape core.
|
|
2
|
+
|
|
3
|
+
Exposes per-cell morphology through the generic :class:`Quantifier` interface as
|
|
4
|
+
a **pooled** quantity: :meth:`compute_object_table` runs the shape core over the
|
|
5
|
+
cell label stack in memory and hands the tidy table straight to the pooling layer
|
|
6
|
+
(no per-position artifact is persisted). The object-key column is ``cell_id`` (the
|
|
7
|
+
shared track id).
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from itasc.contact_analysis.quantifier import Quantifier
|
|
12
|
+
from itasc.contact_analysis.shape import compute_object_shape
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CellShapeQuantifier(Quantifier):
|
|
16
|
+
"""Quantifies per-cell, per-frame shape descriptors from cell labels."""
|
|
17
|
+
|
|
18
|
+
quantity_id = "cell_shape"
|
|
19
|
+
display_name = "Cell shape"
|
|
20
|
+
# Cell labels only; no nucleus / contacts dependency. Pixel size (to emit
|
|
21
|
+
# physical µm / µm²) is a global build param set in the Parameters panel.
|
|
22
|
+
requires = ("cell_labels_path",)
|
|
23
|
+
required_build_params = {"pixel_size_um": "pixel size (µm/px)"}
|
|
24
|
+
#: Per-cell, per-frame shape descriptors, keyed (frame, cell_id).
|
|
25
|
+
table_keys = ("frame", "cell_id")
|
|
26
|
+
|
|
27
|
+
def compute_object_table(self, inputs, *, params=None):
|
|
28
|
+
return compute_object_shape(
|
|
29
|
+
inputs.cell_labels_path,
|
|
30
|
+
pixel_size_um=inputs.pixel_size_um,
|
|
31
|
+
object_key="cell_id",
|
|
32
|
+
)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Cell-cell contacts quantifier — the registry adapter over the contacts code.
|
|
2
|
+
|
|
3
|
+
Wraps :mod:`itasc.contact_analysis.contacts` so the studio can build
|
|
4
|
+
and read contacts through the generic :class:`Quantifier` interface. Its chosen
|
|
5
|
+
persistence is the ``contact_analysis.h5`` schema, unchanged from before the
|
|
6
|
+
rename.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Callable, Mapping
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
from itasc.contact_analysis.contacts.build import build_contacts
|
|
17
|
+
from itasc.contact_analysis.contacts.reader import (
|
|
18
|
+
read_position_contacts,
|
|
19
|
+
)
|
|
20
|
+
from itasc.contact_analysis.quantifier import PositionInputs, Quantifier
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ContactsQuantifier(Quantifier):
|
|
24
|
+
"""Quantifies cell-cell contacts: edges, T1 events, NLS classes."""
|
|
25
|
+
|
|
26
|
+
quantity_id = "contacts"
|
|
27
|
+
display_name = "Cell–cell contacts"
|
|
28
|
+
# Cell labels are mandatory; nucleus labels are optional (when present they
|
|
29
|
+
# enable the cell_id == nucleus_id invariant check and NLS classification).
|
|
30
|
+
requires = ("cell_labels_path",)
|
|
31
|
+
#: The contacts artifact is the input the contacts-derived quantifiers
|
|
32
|
+
#: (neighbor count / enrichment / z-score / density / signed contact length) consume.
|
|
33
|
+
produces = "contact_analysis_path"
|
|
34
|
+
|
|
35
|
+
#: Default artifact name when a position does not dictate one.
|
|
36
|
+
default_output_name = "contact_analysis.h5"
|
|
37
|
+
|
|
38
|
+
def build(
|
|
39
|
+
self,
|
|
40
|
+
inputs: PositionInputs,
|
|
41
|
+
output_path: Path,
|
|
42
|
+
*,
|
|
43
|
+
params: dict | None = None,
|
|
44
|
+
progress_cb: Callable[[int, int, str], None] | None = None,
|
|
45
|
+
) -> Path:
|
|
46
|
+
return build_contacts(
|
|
47
|
+
cell_labels_path=inputs.cell_labels_path,
|
|
48
|
+
nucleus_labels_path=inputs.nucleus_labels_path,
|
|
49
|
+
output_path=output_path,
|
|
50
|
+
source_path=inputs.position_dir,
|
|
51
|
+
edge_extraction_params=params,
|
|
52
|
+
progress_cb=progress_cb,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def default_output(self, inputs: PositionInputs) -> Path:
|
|
56
|
+
"""The contacts ``.h5`` lives in the position base folder, beside the
|
|
57
|
+
committed ``cell_labels.tif`` / ``nucleus_labels.tif`` — not under the
|
|
58
|
+
shared :data:`~itasc.contact_analysis.quantifier.OUTPUT_SUBDIR` that the
|
|
59
|
+
other (dynamics) quantifiers persist into. One homogeneous per-position
|
|
60
|
+
layout for the downstream-stable inputs and this derived-from-them graph.
|
|
61
|
+
"""
|
|
62
|
+
return inputs.position_dir / self.default_output_name
|
|
63
|
+
|
|
64
|
+
def read(self, output_path: Path) -> Any:
|
|
65
|
+
return read_position_contacts(output_path)
|
|
66
|
+
|
|
67
|
+
def object_table(self, output_path: Path) -> Mapping[str, np.ndarray]:
|
|
68
|
+
"""The per-cell ``cells`` table (``frame`` · ``cell_id`` · morphometry).
|
|
69
|
+
|
|
70
|
+
The subpopulation label is no longer carried here — it lives in the NLS
|
|
71
|
+
sidecar CSV and is joined by ``cell_id`` at pool time."""
|
|
72
|
+
return read_position_contacts(output_path).cells
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Neighbor-count quantifier — per-cell adjacency degree from the contact graph.
|
|
2
|
+
|
|
3
|
+
A contacts-derived **pooled** quantity: :meth:`compute_object_table` reads the
|
|
4
|
+
position's ``contact_analysis.h5`` and returns the per ``(frame, cell_id)``
|
|
5
|
+
neighbor count in memory (no per-position artifact). The NLS subpopulation label
|
|
6
|
+
is *not* stored here — it is left-joined by ``cell_id`` at pool time, exactly as
|
|
7
|
+
the shape family does.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from itasc.contact_analysis.contacts.neighborhood import cell_neighbor_counts
|
|
12
|
+
from itasc.contact_analysis.quantifier import Quantifier
|
|
13
|
+
from itasc.contact_analysis.quantifiers import _contacts_derived as derived
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NeighborCountQuantifier(Quantifier):
|
|
17
|
+
"""Per ``(frame, cell_id)`` number of distinct cell-cell neighbors."""
|
|
18
|
+
|
|
19
|
+
quantity_id = "neighbor_count"
|
|
20
|
+
display_name = "Neighbor count"
|
|
21
|
+
requires = ("contact_analysis_path",)
|
|
22
|
+
# Per-cell adjacency degree, keyed (frame, cell_id).
|
|
23
|
+
table_keys = ("frame", "cell_id")
|
|
24
|
+
|
|
25
|
+
def compute_object_table(self, inputs, *, params=None):
|
|
26
|
+
return dict(cell_neighbor_counts(derived.load_analysis(inputs)))
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Nucleus-dynamics quantifier — the registry adapter over the dynamics core.
|
|
2
|
+
|
|
3
|
+
The nucleus twin of :mod:`.cell_dynamics`: it runs the same label-agnostic
|
|
4
|
+
:func:`build_track_dynamics` over the **nucleus** label stack instead of the cell
|
|
5
|
+
one, persisting ``4_contact_analysis/nucleus_dynamics.h5``. Nuclei are
|
|
6
|
+
compact, point-like centroids — the robust default for motility / MSD. The
|
|
7
|
+
object-key column stays ``cell_id`` (a nucleus is nucleus-seeded so it carries
|
|
8
|
+
its cell's shared track id), so the pooling / plotting layer treats it no
|
|
9
|
+
differently.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from collections.abc import Callable, Mapping
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
from itasc.contact_analysis.dynamics import (
|
|
19
|
+
TrackDynamics,
|
|
20
|
+
build_track_dynamics,
|
|
21
|
+
read_instantaneous_table,
|
|
22
|
+
read_track_dynamics,
|
|
23
|
+
)
|
|
24
|
+
from itasc.contact_analysis.dynamics.kinematics import instantaneous_table
|
|
25
|
+
from itasc.contact_analysis.dynamics.trajectories import extract_trajectories
|
|
26
|
+
from itasc.contact_analysis.quantifier import PositionInputs, Quantifier
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class NucleusDynamicsQuantifier(Quantifier):
|
|
30
|
+
"""Quantifies per-nucleus motion (speed, persistence, MSD, collective) from nucleus labels."""
|
|
31
|
+
|
|
32
|
+
quantity_id = "nucleus_dynamics"
|
|
33
|
+
display_name = "Nucleus dynamics"
|
|
34
|
+
requires = ("nucleus_labels_path",)
|
|
35
|
+
# Pixel size + frame interval are global build params (see CellDynamics).
|
|
36
|
+
required_build_params = {
|
|
37
|
+
"pixel_size_um": "pixel size (µm/px)",
|
|
38
|
+
"time_interval_s": "frame interval (s)",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
default_output_name = "nucleus_dynamics.h5"
|
|
42
|
+
# Instantaneous nucleus motion is keyed on the shared cell track id
|
|
43
|
+
# (frame, cell_id).
|
|
44
|
+
table_keys = ("frame", "cell_id")
|
|
45
|
+
|
|
46
|
+
def build(
|
|
47
|
+
self,
|
|
48
|
+
inputs: PositionInputs,
|
|
49
|
+
output_path: Path,
|
|
50
|
+
*,
|
|
51
|
+
params: dict | None = None,
|
|
52
|
+
progress_cb: Callable[[int, int, str], None] | None = None,
|
|
53
|
+
) -> Path:
|
|
54
|
+
return build_track_dynamics(
|
|
55
|
+
inputs.nucleus_labels_path,
|
|
56
|
+
output_path,
|
|
57
|
+
pixel_size_um=inputs.pixel_size_um,
|
|
58
|
+
time_interval_s=inputs.time_interval_s,
|
|
59
|
+
source_path=inputs.position_dir,
|
|
60
|
+
params=params,
|
|
61
|
+
quantity_id=self.quantity_id,
|
|
62
|
+
progress_cb=progress_cb,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def read(self, output_path: Path) -> TrackDynamics:
|
|
66
|
+
return read_track_dynamics(output_path)
|
|
67
|
+
|
|
68
|
+
def object_table(self, output_path: Path) -> Mapping[str, np.ndarray]:
|
|
69
|
+
return read_instantaneous_table(output_path)
|
|
70
|
+
|
|
71
|
+
def compute_object_table(
|
|
72
|
+
self, inputs: PositionInputs, *, params: dict | None = None
|
|
73
|
+
) -> Mapping[str, np.ndarray]:
|
|
74
|
+
trajectories = extract_trajectories(
|
|
75
|
+
inputs.nucleus_labels_path, pixel_size_um=inputs.pixel_size_um
|
|
76
|
+
)
|
|
77
|
+
return instantaneous_table(trajectories, time_interval_s=inputs.time_interval_s)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Nucleus-shape quantifier — the registry adapter over the shape core.
|
|
2
|
+
|
|
3
|
+
The nucleus twin of :mod:`.cell_shape`: :meth:`compute_object_table` runs the same
|
|
4
|
+
label-agnostic :func:`compute_object_shape` over the **nucleus** label stack
|
|
5
|
+
instead of the cell one and pools the tidy table in memory (no per-position
|
|
6
|
+
artifact). The object-key column stays ``cell_id`` — a nucleus is nucleus-seeded
|
|
7
|
+
so it carries its cell's shared track id — so nothing in the pooling layer treats
|
|
8
|
+
it differently.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from itasc.contact_analysis.quantifier import Quantifier
|
|
13
|
+
from itasc.contact_analysis.shape import compute_object_shape
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NucleusShapeQuantifier(Quantifier):
|
|
17
|
+
"""Quantifies per-nucleus, per-frame shape descriptors from nucleus labels."""
|
|
18
|
+
|
|
19
|
+
quantity_id = "nucleus_shape"
|
|
20
|
+
display_name = "Nucleus shape"
|
|
21
|
+
# Nucleus labels only; pixel size (to emit physical µm / µm²) is a global
|
|
22
|
+
# build param set in the Parameters panel.
|
|
23
|
+
requires = ("nucleus_labels_path",)
|
|
24
|
+
required_build_params = {"pixel_size_um": "pixel size (µm/px)"}
|
|
25
|
+
# Nucleus shape is keyed on its cell's shared track id (frame, cell_id); its
|
|
26
|
+
# descriptors are namespaced by quantity_id (so the nucleus ``area`` never
|
|
27
|
+
# collides with the cell ``area`` in a joined view).
|
|
28
|
+
table_keys = ("frame", "cell_id")
|
|
29
|
+
|
|
30
|
+
def compute_object_table(self, inputs, *, params=None):
|
|
31
|
+
return compute_object_shape(
|
|
32
|
+
inputs.nucleus_labels_path,
|
|
33
|
+
pixel_size_um=inputs.pixel_size_um,
|
|
34
|
+
object_key="cell_id",
|
|
35
|
+
)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Relational shape quantifier — nucleus-vs-cell paired morphology.
|
|
2
|
+
|
|
3
|
+
Wraps :func:`itasc.contact_analysis.shape.compute_relational_table`, which
|
|
4
|
+
pairs each nucleus with its cell on the shared ``(frame, id)`` key and emits
|
|
5
|
+
relational quantities (nuclear:cell area ratio, centroid offset, orientation
|
|
6
|
+
delta, …). Needs **both** label stacks plus a pixel size. A **pooled** quantity:
|
|
7
|
+
:meth:`compute_object_table` builds the tidy table in memory (no per-position
|
|
8
|
+
artifact). The object-key column is ``cell_id``, so the relational table pools
|
|
9
|
+
through the same path as the per-source shape tables.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from itasc.contact_analysis.quantifier import Quantifier
|
|
14
|
+
from itasc.contact_analysis.shape import compute_relational_table
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ShapeRelationalQuantifier(Quantifier):
|
|
18
|
+
"""Quantifies per-(frame, cell) relational nucleus-vs-cell shape."""
|
|
19
|
+
|
|
20
|
+
quantity_id = "shape_relational"
|
|
21
|
+
display_name = "Nucleus–cell shape"
|
|
22
|
+
# Both label stacks (to pair); pixel size (to emit physical units) is a
|
|
23
|
+
# global build param set in the Parameters panel.
|
|
24
|
+
requires = ("cell_labels_path", "nucleus_labels_path")
|
|
25
|
+
required_build_params = {"pixel_size_um": "pixel size (µm/px)"}
|
|
26
|
+
# Per-cell nucleus↔cell relational descriptors, keyed (frame, cell_id).
|
|
27
|
+
table_keys = ("frame", "cell_id")
|
|
28
|
+
|
|
29
|
+
def compute_object_table(self, inputs, *, params=None):
|
|
30
|
+
return compute_relational_table(
|
|
31
|
+
inputs.cell_labels_path,
|
|
32
|
+
inputs.nucleus_labels_path,
|
|
33
|
+
pixel_size_um=inputs.pixel_size_um,
|
|
34
|
+
)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Signed-contact-length quantifier — signed T1 junction lengths for the potential.
|
|
2
|
+
|
|
3
|
+
A contacts-derived **pooled** quantity: :meth:`compute_object_table` returns the
|
|
4
|
+
signed central-junction length of each T1 event (negative on the losing side,
|
|
5
|
+
positive on the gaining side) in memory (no per-position artifact). Pooled and
|
|
6
|
+
Boltzmann-inverted downstream these reproduce the double-well potential. Lengths
|
|
7
|
+
are in µm when the position's pixel size resolves, else pixels. The
|
|
8
|
+
``contact_type`` transition label is normalized here (blank → ``"unlabelled"``) so
|
|
9
|
+
a group axis never sees an empty string.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
from itasc.contact_analysis.contacts.signed_contact_length import (
|
|
16
|
+
signed_central_junction_lengths,
|
|
17
|
+
)
|
|
18
|
+
from itasc.contact_analysis.quantifier import Quantifier
|
|
19
|
+
from itasc.contact_analysis.quantifiers import _contacts_derived as derived
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SignedContactLengthQuantifier(Quantifier):
|
|
23
|
+
"""Per-T1-event signed central junction length (the potential's samples)."""
|
|
24
|
+
|
|
25
|
+
quantity_id = "signed_contact_length"
|
|
26
|
+
display_name = "Signed contact length"
|
|
27
|
+
requires = ("contact_analysis_path",)
|
|
28
|
+
# One row per T1 event; ``contact_type`` / ``role`` are categorical axes that
|
|
29
|
+
# key the table alongside the event id (only ``signed_length`` is a value
|
|
30
|
+
# column).
|
|
31
|
+
table_keys = ("frame", "t1_event_id", "role", "contact_type")
|
|
32
|
+
|
|
33
|
+
def compute_object_table(self, inputs, *, params=None):
|
|
34
|
+
analysis = derived.load_analysis(inputs)
|
|
35
|
+
table = dict(
|
|
36
|
+
signed_central_junction_lengths(
|
|
37
|
+
analysis, pixel_size_um=inputs.pixel_size_um
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
if "contact_type" in table:
|
|
41
|
+
ct = np.asarray(table["contact_type"], dtype=object)
|
|
42
|
+
ct[ct == ""] = "unlabelled"
|
|
43
|
+
table["contact_type"] = ct
|
|
44
|
+
return table
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Qt-free bridge from catalogue *records* to a quantifier's per-position files.
|
|
2
|
+
|
|
3
|
+
The studio carries positions as normalized catalogue ``record`` dicts (see
|
|
4
|
+
:mod:`itasc.contact_analysis.catalog`). Turning one into the
|
|
5
|
+
:class:`~itasc.contact_analysis.quantifier.PositionInputs` a quantifier
|
|
6
|
+
builds from — and resolving where that quantifier's artifact lives — is pure path
|
|
7
|
+
logic with no Qt. It lives here (rather than in the napari studio layer) so the
|
|
8
|
+
headless aggregation backend (:mod:`.shape_tables`) and the standalone
|
|
9
|
+
``itasc-aggregate`` wheel can resolve the same paths the pipeline does.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from collections.abc import Mapping, Sequence
|
|
14
|
+
from dataclasses import fields as _dataclass_fields
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from .quantifier import PositionInputs, Quantifier, available_quantifiers
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"position_inputs_from_record",
|
|
21
|
+
"output_for_record",
|
|
22
|
+
"available_fields",
|
|
23
|
+
"record_build_params",
|
|
24
|
+
"supported_quantities",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def position_inputs_from_record(record: dict) -> PositionInputs:
|
|
29
|
+
"""Build :class:`PositionInputs` from a normalized catalogue record.
|
|
30
|
+
|
|
31
|
+
``pixel_size_um`` / ``time_interval_s`` are **global** build params: they are
|
|
32
|
+
taken only from the value stamped on the record (the Parameters panel's
|
|
33
|
+
px/Δt), never auto-resolved from a position's config or label TIFF tags.
|
|
34
|
+
Absent ⇒ ``None``.
|
|
35
|
+
"""
|
|
36
|
+
out = record.get("contact_analysis_path")
|
|
37
|
+
cell = record.get("cell_tracked_labels_path")
|
|
38
|
+
nucleus = record.get("nucleus_tracked_labels_path")
|
|
39
|
+
position_dir = record.get("position_path") or (Path(out).parent if out else Path("."))
|
|
40
|
+
cell_path = Path(cell) if cell else None
|
|
41
|
+
nucleus_path = Path(nucleus) if nucleus else None
|
|
42
|
+
return PositionInputs(
|
|
43
|
+
position_dir=Path(position_dir),
|
|
44
|
+
cell_labels_path=cell_path,
|
|
45
|
+
nucleus_labels_path=nucleus_path,
|
|
46
|
+
pixel_size_um=_positive_float(record.get("pixel_size_um")),
|
|
47
|
+
time_interval_s=_positive_float(record.get("time_interval_s")),
|
|
48
|
+
# The contacts artifact is a *produced* input — the contacts quantifier
|
|
49
|
+
# writes it. The catalogue stamps its expected path on every position
|
|
50
|
+
# whether or not it has been built yet, so gate on the file actually
|
|
51
|
+
# existing: an unbuilt contacts product is not an available input.
|
|
52
|
+
contact_analysis_path=Path(out) if out and Path(out).is_file() else None,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _positive_float(value: object) -> float | None:
|
|
57
|
+
try:
|
|
58
|
+
result = float(value) # type: ignore[arg-type]
|
|
59
|
+
except (TypeError, ValueError):
|
|
60
|
+
return None
|
|
61
|
+
return result if result > 0 else None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def output_for_record(quantifier: Quantifier, record: dict) -> Path:
|
|
65
|
+
"""Where *quantifier*'s artifact lives for a catalogue *record*.
|
|
66
|
+
|
|
67
|
+
Contacts keeps using the catalogue's explicit ``contact_analysis_path``
|
|
68
|
+
column — it predates the quantifier seam and may hold a custom *nested* path
|
|
69
|
+
that the per-position read/visualize paths also rely on, so building must
|
|
70
|
+
target the same file. Every other quantifier derives its destination from
|
|
71
|
+
:meth:`Quantifier.default_output`.
|
|
72
|
+
"""
|
|
73
|
+
if quantifier.quantity_id == "contacts":
|
|
74
|
+
explicit = record.get("contact_analysis_path")
|
|
75
|
+
if explicit:
|
|
76
|
+
return Path(explicit)
|
|
77
|
+
return quantifier.default_output(position_inputs_from_record(record))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def available_fields(inputs: PositionInputs) -> set[str]:
|
|
81
|
+
"""The populated (non-``None``) ``PositionInputs`` field names — the satisfied
|
|
82
|
+
prerequisites a quantifier's ``requires`` is checked against."""
|
|
83
|
+
return {f.name for f in _dataclass_fields(inputs) if getattr(inputs, f.name) is not None}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def record_build_params(
|
|
87
|
+
quantifier: Quantifier, record: dict, params: Mapping[str, object] | None
|
|
88
|
+
) -> dict:
|
|
89
|
+
"""Shared *params* overlaid with the record's own required-build-param values.
|
|
90
|
+
|
|
91
|
+
A param like pixel size can be set per-position on the record (the value the
|
|
92
|
+
build actually reads via ``PositionInputs``) instead of in the shared bar, so
|
|
93
|
+
the build-param gate must see both. The record's own value wins where present,
|
|
94
|
+
mirroring ``run()``'s per-record stamping.
|
|
95
|
+
"""
|
|
96
|
+
merged = dict(params or {})
|
|
97
|
+
for key in quantifier.required_build_params:
|
|
98
|
+
value = record.get(key)
|
|
99
|
+
if value is not None:
|
|
100
|
+
merged[key] = value
|
|
101
|
+
return merged
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def supported_quantities(
|
|
105
|
+
records: Sequence[dict], *, params: Mapping[str, object] | None = None
|
|
106
|
+
) -> set[str]:
|
|
107
|
+
"""``quantity_id``\\ s of the pooled quantifiers *records* can actually produce.
|
|
108
|
+
|
|
109
|
+
A pooled quantifier (one declaring ``table_keys``) is *supported* when at least
|
|
110
|
+
one record satisfies **both** gates :func:`shape_tables.build_table` applies per
|
|
111
|
+
position: its ``requires`` inputs are present (:meth:`Quantifier.can_build`) and
|
|
112
|
+
its ``required_build_params`` are satisfied (pixel size, FOV area, …). That makes
|
|
113
|
+
"supported" ⟺ "this table would pool at least one non-empty row" — the exact
|
|
114
|
+
predicate the Aggregate UI greys its checkboxes against, so an enabled box never
|
|
115
|
+
promises a table the run would silently skip. Producers (no ``table_keys``, e.g.
|
|
116
|
+
``contacts``) are never pooled and so never reported.
|
|
117
|
+
"""
|
|
118
|
+
supported: set[str] = set()
|
|
119
|
+
for quantifier in (cls() for cls in available_quantifiers()):
|
|
120
|
+
if not quantifier.table_keys:
|
|
121
|
+
continue
|
|
122
|
+
for record in records:
|
|
123
|
+
inputs = position_inputs_from_record(record)
|
|
124
|
+
if quantifier.can_build(inputs) and not quantifier.missing_build_params(
|
|
125
|
+
record_build_params(quantifier, record, params)
|
|
126
|
+
):
|
|
127
|
+
supported.add(quantifier.quantity_id)
|
|
128
|
+
break
|
|
129
|
+
return supported
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Shape compute core: per-object morphology + relational nucleus-vs-cell shape.
|
|
2
|
+
|
|
3
|
+
The headless backend for the shape quantifiers. :func:`compute_object_shape` runs
|
|
4
|
+
:func:`skimage.measure.regionprops` over any tracked-label stack (cell *or*
|
|
5
|
+
nucleus) and returns a tidy column-major table; :func:`compute_relational_table`
|
|
6
|
+
pairs each nucleus with its cell and emits relational quantities. Both compute in
|
|
7
|
+
memory — the aggregate stage pools the tables, nothing is persisted per position.
|
|
8
|
+
No Qt / napari import, so scripts and the standalone wheel can use it.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .core import DESCRIPTOR_COLUMNS, compute_object_shape
|
|
12
|
+
from .relational import RELATIONAL_COLUMNS, compute_relational_table
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"DESCRIPTOR_COLUMNS",
|
|
16
|
+
"RELATIONAL_COLUMNS",
|
|
17
|
+
"compute_object_shape",
|
|
18
|
+
"compute_relational_table",
|
|
19
|
+
]
|