Skip to content

Commit 5facad4

Browse files
czaloomntlind
andauthored
Lite Semantic Segmentation (#773)
Co-authored-by: Nick L <[email protected]>
1 parent a9ab7e7 commit 5facad4

21 files changed

+1931
-1
lines changed

.github/workflows/lite-tests-and-coverage.yml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,21 @@ jobs:
3636
- name: run object detection tests and report coverage
3737
run: |
3838
pip install -e ".[test]"
39-
COVERAGE_FILE=.coverage.functional python -m coverage run --omit "tests/*" -m pytest -v tests/detection/
39+
COVERAGE_FILE=.coverage.detection python -m coverage run --omit "tests/*" -m pytest -v tests/detection/
40+
python -m coverage combine
41+
python -m coverage report -m
42+
python -m coverage json
43+
export TOTAL=$(python -c "import json;print(json.load(open('coverage.json'))['totals']['percent_covered_display'])")
44+
echo "total=$TOTAL" >> $GITHUB_ENV
45+
if (( $TOTAL < 90 )); then
46+
echo "Coverage is below 90%"
47+
exit 1
48+
fi
49+
working-directory: ./lite
50+
- name: run semantic segmentation tests and report coverage
51+
run: |
52+
pip install -e ".[test]"
53+
COVERAGE_FILE=.coverage.segmentation python -m coverage run --omit "tests/*" -m pytest -v tests/segmentation/
4054
python -m coverage combine
4155
python -m coverage report -m
4256
python -m coverage json

lite/tests/__init__.py

Whitespace-only changes.

lite/tests/classification/__init__.py

Whitespace-only changes.

lite/tests/segmentation/__init__.py

Whitespace-only changes.

lite/tests/segmentation/conftest.py

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import numpy as np
2+
import pytest
3+
from valor_lite.segmentation import Bitmask, Segmentation
4+
5+
6+
def _generate_boolean_mask(
7+
mask_shape: tuple[int, int],
8+
annotation_shape: tuple[int, int, int, int],
9+
label: str,
10+
) -> Bitmask:
11+
mask = np.zeros(mask_shape, dtype=np.bool_)
12+
xmin = annotation_shape[0]
13+
xmax = annotation_shape[1]
14+
ymin = annotation_shape[2]
15+
ymax = annotation_shape[3]
16+
mask[ymin:ymax, xmin:xmax] = True
17+
return Bitmask(
18+
mask=mask,
19+
label=label,
20+
)
21+
22+
23+
def _generate_random_boolean_mask(
24+
mask_shape: tuple[int, int],
25+
infill: float,
26+
label: str,
27+
) -> Bitmask:
28+
mask_size = mask_shape[0] * mask_shape[1]
29+
mask = np.zeros(mask_size, dtype=np.bool_)
30+
31+
n_positives = int(mask_size * infill)
32+
mask[:n_positives] = True
33+
np.random.shuffle(mask)
34+
mask = mask.reshape(mask_shape)
35+
36+
return Bitmask(
37+
mask=mask,
38+
label=label,
39+
)
40+
41+
42+
@pytest.fixture
43+
def basic_segmentations() -> list[Segmentation]:
44+
45+
gmask1 = Bitmask(
46+
mask=np.array([[True, False], [False, True]]),
47+
label="v1",
48+
)
49+
gmask2 = Bitmask(
50+
mask=np.array([[False, False], [True, False]]),
51+
label="v2",
52+
)
53+
54+
pmask1 = Bitmask(
55+
mask=np.array([[True, False], [False, False]]),
56+
label="v1",
57+
)
58+
pmask2 = Bitmask(
59+
mask=np.array([[False, True], [True, False]]),
60+
label="v2",
61+
)
62+
63+
return [
64+
Segmentation(
65+
uid="uid0",
66+
groundtruths=[gmask1, gmask2],
67+
predictions=[pmask1, pmask2],
68+
)
69+
]
70+
71+
72+
@pytest.fixture
73+
def segmentations_from_boxes() -> list[Segmentation]:
74+
75+
mask_shape = (900, 300)
76+
77+
rect1 = (0, 100, 0, 100)
78+
rect2 = (150, 300, 400, 500)
79+
rect3 = (50, 150, 0, 100) # overlaps 50% with rect1
80+
rect4 = (101, 151, 301, 401) # overlaps 1 pixel with rect2
81+
82+
gmask1 = _generate_boolean_mask(mask_shape, rect1, "v1")
83+
gmask2 = _generate_boolean_mask(mask_shape, rect2, "v2")
84+
85+
pmask1 = _generate_boolean_mask(mask_shape, rect3, "v1")
86+
pmask2 = _generate_boolean_mask(mask_shape, rect4, "v2")
87+
88+
return [
89+
Segmentation(
90+
uid="uid1",
91+
groundtruths=[gmask1],
92+
predictions=[pmask1],
93+
),
94+
Segmentation(
95+
uid="uid2",
96+
groundtruths=[gmask2],
97+
predictions=[pmask2],
98+
),
99+
]
100+
101+
102+
@pytest.fixture
103+
def large_random_segmentations() -> list[Segmentation]:
104+
105+
mask_shape = (1000, 1000)
106+
infills_per_seg = [
107+
(0.9, 0.09, 0.01),
108+
(0.4, 0.4, 0.1),
109+
(0.3, 0.3, 0.3),
110+
]
111+
labels_per_seg = [
112+
("v1", "v2", "v3"),
113+
("v4", "v5", "v6"),
114+
("v7", "v8", "v9"),
115+
]
116+
117+
return [
118+
Segmentation(
119+
uid=f"uid{idx}",
120+
groundtruths=[
121+
_generate_random_boolean_mask(mask_shape, infill, label)
122+
for infill, label in zip(infills, labels)
123+
],
124+
predictions=[
125+
_generate_random_boolean_mask(mask_shape, infill, label)
126+
for infill, label in zip(infills, labels)
127+
],
128+
)
129+
for idx, (infills, labels) in enumerate(
130+
zip(infills_per_seg, labels_per_seg)
131+
)
132+
]
133+
134+
135+
@pytest.fixture
136+
def massive_random_segmentations() -> list[Segmentation]:
137+
"""
138+
A variant of `large_random_segmentations`.
139+
140+
This fixture is not used as it takes 10s to load.
141+
"""
142+
143+
mask_shape = (5000, 5000)
144+
infills_per_seg = [
145+
(0.9, 0.09, 0.01),
146+
(0.4, 0.4, 0.1),
147+
(0.3, 0.3, 0.3),
148+
]
149+
labels_per_seg = [
150+
("v1", "v2", "v3"),
151+
("v4", "v5", "v6"),
152+
("v7", "v8", "v9"),
153+
]
154+
155+
return [
156+
Segmentation(
157+
uid=f"uid{idx}",
158+
groundtruths=[
159+
_generate_random_boolean_mask(mask_shape, infill, label)
160+
for infill, label in zip(infills, labels)
161+
],
162+
predictions=[
163+
_generate_random_boolean_mask(mask_shape, infill, label)
164+
for infill, label in zip(infills, labels)
165+
],
166+
)
167+
for idx, (infills, labels) in enumerate(
168+
zip(infills_per_seg, labels_per_seg)
169+
)
170+
]
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from valor_lite.segmentation import (
2+
Accuracy,
3+
DataLoader,
4+
MetricType,
5+
Segmentation,
6+
)
7+
8+
9+
def test_accuracy_basic_segmentations(basic_segmentations: list[Segmentation]):
10+
loader = DataLoader()
11+
loader.add_data(basic_segmentations)
12+
evaluator = loader.finalize()
13+
14+
metrics = evaluator.evaluate(as_dict=True)
15+
16+
actual_metrics = [m for m in metrics[MetricType.Accuracy]]
17+
expected_metrics = [
18+
{
19+
"type": "Accuracy",
20+
"value": 0.5,
21+
"parameters": {},
22+
},
23+
]
24+
for m in actual_metrics:
25+
assert m in expected_metrics
26+
for m in expected_metrics:
27+
assert m in actual_metrics
28+
29+
30+
def test_accuracy_segmentations_from_boxes(
31+
segmentations_from_boxes: list[Segmentation],
32+
):
33+
loader = DataLoader()
34+
loader.add_data(segmentations_from_boxes)
35+
evaluator = loader.finalize()
36+
37+
metrics = evaluator.evaluate(as_dict=True)
38+
39+
actual_metrics = [m for m in metrics[MetricType.Accuracy]]
40+
expected_metrics = [
41+
{
42+
"type": "Accuracy",
43+
"value": 0.9444481481481481,
44+
"parameters": {},
45+
},
46+
]
47+
for m in actual_metrics:
48+
assert m in expected_metrics
49+
for m in expected_metrics:
50+
assert m in actual_metrics
51+
52+
53+
def test_accuracy_large_random_segmentations(
54+
large_random_segmentations: list[Segmentation],
55+
):
56+
loader = DataLoader()
57+
loader.add_data(large_random_segmentations)
58+
evaluator = loader.finalize()
59+
60+
metrics = evaluator.evaluate()[MetricType.Accuracy]
61+
62+
assert len(metrics) == 1
63+
assert isinstance(metrics[0], Accuracy)
64+
assert round(metrics[0].value, 1) == 0.5 # random choice
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import numpy as np
2+
import pytest
3+
from valor_lite.segmentation import Bitmask, Segmentation
4+
5+
6+
def test_bitmask():
7+
8+
Bitmask(
9+
mask=np.array([True, False]),
10+
label="label",
11+
)
12+
13+
with pytest.raises(ValueError) as e:
14+
Bitmask(mask=np.array([1, 2]), label="label")
15+
assert "int64" in str(e)
16+
17+
18+
def test_segmentation():
19+
20+
s = Segmentation(
21+
uid="uid",
22+
groundtruths=[
23+
Bitmask(
24+
mask=np.array([True, False]),
25+
label="label",
26+
)
27+
],
28+
predictions=[
29+
Bitmask(
30+
mask=np.array([True, False]),
31+
label="label",
32+
)
33+
],
34+
)
35+
assert s.shape == (2,)
36+
assert s.size == 2
37+
38+
with pytest.raises(ValueError) as e:
39+
Segmentation(
40+
uid="uid",
41+
groundtruths=[
42+
Bitmask(
43+
mask=np.array([True, False, False]),
44+
label="label",
45+
)
46+
],
47+
predictions=[
48+
Bitmask(
49+
mask=np.array([True, False]),
50+
label="label",
51+
)
52+
],
53+
)
54+
assert "mismatch" in str(e)
55+
56+
with pytest.raises(ValueError) as e:
57+
Segmentation(
58+
uid="uid",
59+
groundtruths=[],
60+
predictions=[
61+
Bitmask(
62+
mask=np.array([True, False]),
63+
label="label",
64+
)
65+
],
66+
)
67+
assert "missing ground truths" in str(e)
68+
69+
with pytest.raises(ValueError) as e:
70+
Segmentation(
71+
uid="uid",
72+
groundtruths=[
73+
Bitmask(
74+
mask=np.array([True, False]),
75+
label="label",
76+
)
77+
],
78+
predictions=[],
79+
)
80+
assert "missing predictions" in str(e)

0 commit comments

Comments
 (0)