Source code for itasc.contact_analysis.quantifiers.cell_density
"""Cell-density quantifier — per-frame cell counts per field-of-view area.
Counts cells straight off the tracked **cell labels** (unique non-zero labels per
frame), so it is a cell-labels quantity, not a contacts-derived one. The
field-of-view area is **required** — it comes from the shared params bar and there
is no silent image-area fallback; ``density = n_cells / fov_area_mm2`` in
cells/mm². One ``all`` total row per frame (label-agnostic). A **pooled** quantity:
:meth:`compute_object_table` builds the tidy table in memory (no per-position
artifact).
"""
from __future__ import annotations
from collections.abc import Mapping
from pathlib import Path
import numpy as np
from itasc.contact_analysis.contacts.neighborhood import cell_density
from itasc.contact_analysis.quantifier import PositionInputs, Quantifier
from itasc.contact_analysis.shape.core import read_label_stack
[docs]
class CellDensityQuantifier(Quantifier):
"""Per ``(frame, label)`` cell count and density (cells/mm²)."""
quantity_id = "cell_density"
display_name = "Cell density"
# Cell labels are the only hard input — counts come straight off them. The
# field-of-view area is a required build *param* (validated in
# ``compute_object_table``), not a PositionInputs field.
requires = ("cell_labels_path",)
# Density is keyed per (frame, label); ``label`` is always ``"all"``.
table_keys = ("frame", "label")
wants_build_params = True
# The field-of-view area has no image-area fallback, so it is a hard build
# requirement: the pooling loop skips Cell density (and the UI greys it out)
# until it is set, instead of letting the compute raise.
required_build_params = {"fov_area_mm2": "FOV area (mm²)"}
[docs]
def compute_object_table(
self, inputs: PositionInputs, *, params: dict | None = None
) -> Mapping[str, np.ndarray]:
fov = (params or {}).get("fov_area_mm2")
if not (isinstance(fov, (int, float)) and fov > 0):
raise ValueError(
"Cell density requires a field-of-view area — set 'FOV area (mm²)' "
"in Parameters before building."
)
frame_cells = _frame_cells_from_labels(inputs.cell_labels_path)
return cell_density(frame_cells, fov_area_mm2=float(fov))
def _frame_cells_from_labels(labels_path: Path | None) -> dict[int, list[int]]:
"""``frame -> [cell_id, …]`` from a tracked cell-label TIFF.
The unique non-zero labels of frame *i* are the cells present in it — the same
set the contacts ``cells`` table holds (it is one ``regionprops`` row per
label), so this reproduces the old per-frame counts without the artifact.
"""
stack = read_label_stack(Path(labels_path))
out: dict[int, list[int]] = {}
for frame_idx, frame in enumerate(stack):
ids = np.unique(frame)
out[frame_idx] = [int(i) for i in ids if i != 0]
return out