cellquant / tests /test_postprocess.py
stevafernandes's picture
Deploy CellQuant
5d31c67 verified
Raw History Blame Contribute Delete
4.11 kB
import numpy as np
from cellquant.labels import ALL_LABELS
from cellquant.postprocess import PostConfig, analyze, logits_to_probs, render_overlay, segment_cells
def _disc(h, w, cy, cx, r):
yy, xx = np.mgrid[:h, :w]
return (yy - cy) ** 2 + (xx - cx) ** 2 <= r * r
def _probs(h=96, w=96):
"""Two touching discs; the left one antibody+ with a punctum inside; one external punctum."""
d1, d2 = _disc(h, w, 48, 30, 14), _disc(h, w, 48, 56, 14)
fg = d1 | d2
inner = _disc(h, w, 48, 30, 11) | _disc(h, w, 48, 56, 11)
cell = np.zeros((3, h, w), np.float32)
cell[1][inner] = 0.9
cell[2][fg & ~inner] = 0.9
cell[0] = 1 - cell[1] - cell[2]
antibody = np.where(d1, 0.95, 0.05).astype(np.float32)
cellnos = np.full((h, w), 0.1, np.float32)
nos = np.zeros((3, h, w), np.float32)
p_in = _disc(h, w, 48, 30, 2)
p_ext = _disc(h, w, 10, 80, 2)
nos[1][p_in] = 0.9
nos[2][p_ext] = 0.9
nos[0] = 1 - nos[1] - nos[2]
return {"cell": cell, "antibody": antibody, "cellnos": cellnos, "nos": nos}
def test_touching_discs_split_into_two_instances():
inst = segment_cells(_probs()["cell"], PostConfig())
assert inst.max() == 2
areas = np.bincount(inst.ravel())[1:]
assert all(500 <= a <= 700 for a in areas) # pi*14^2 ~ 616
def test_analyze_labels_and_counts():
res = analyze(_probs(), PostConfig())
assert len(res.cells) == 2 and len(res.nostrils) == 2
labels = sorted(c["label"] for c in res.cells)
assert labels == ["Cell, Antibody -", "Cell, Nostril, Antibody +"]
assert sorted(n["label"] for n in res.nostrils) == ["Nostril External of Nucleus", "Nostril in Nucleus"]
assert sum(res.counts.values()) == 4 and set(res.counts) == set(ALL_LABELS)
inside = [n for n in res.nostrils if n["label"] == "Nostril in Nucleus"][0]
assert inside["cell_id"] == [c for c in res.cells if c["label"].startswith("Cell, Nostril")][0]["id"]
def test_cellnos_rules():
p = _probs()
head_only = analyze(p, PostConfig(cellnos_rule="head"))
assert all(not c["label"].startswith("Cell, Nostril") for c in head_only.cells)
contains = analyze(p, PostConfig(cellnos_rule="contains"))
assert sum(c["label"].startswith("Cell, Nostril") for c in contains.cells) == 1
def test_geometric_nostril_rule_overrides_head():
p = _probs()
# make the network call the inside punctum "external"; geometric rule should still say inside
p["nos"][1], p["nos"][2] = p["nos"][2].copy(), p["nos"][1].copy()
head = analyze(p, PostConfig(nostril_rule="head"))
geo = analyze(p, PostConfig(nostril_rule="geometric"))
assert head.counts["Nostril in Nucleus"] == 1 and geo.counts["Nostril in Nucleus"] == 1
# with head rule the punctum inside the disc is now labelled external
inside_disc = [n for n in head.nostrils if n["cell_id"] > 0][0]
assert inside_disc["label"] == "Nostril External of Nucleus"
assert [n for n in geo.nostrils if n["cell_id"] > 0][0]["label"] == "Nostril in Nucleus"
def test_min_area_filters_small_instances():
p = _probs()
assert len(analyze(p, PostConfig(min_cell_area=1000)).cells) == 0
def test_logits_to_probs_and_overlay():
logits = np.random.default_rng(0).normal(size=(8, 40, 40)).astype(np.float32)
pr = logits_to_probs(logits)
assert np.allclose(pr["cell"].sum(0), 1, atol=1e-5) and np.allclose(pr["nos"].sum(0), 1, atol=1e-5)
assert pr["antibody"].min() >= 0 and pr["antibody"].max() <= 1
res = analyze(_probs(), PostConfig())
img = np.zeros((96, 96, 3), np.uint8)
ov = render_overlay(img, res)
assert ov.shape == img.shape and ov.any()
def test_empty_image_gives_no_objects():
h = w = 64
p = {"cell": np.stack([np.ones((h, w)), np.zeros((h, w)), np.zeros((h, w))]).astype(np.float32),
"antibody": np.zeros((h, w), np.float32), "cellnos": np.zeros((h, w), np.float32),
"nos": np.stack([np.ones((h, w)), np.zeros((h, w)), np.zeros((h, w))]).astype(np.float32)}
res = analyze(p, PostConfig())
assert res.cells == [] and res.nostrils == [] and sum(res.counts.values()) == 0