File size: 4,107 Bytes
5d31c67
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
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