Skip to content

Commit 43157d5

Browse files
committed
update: added a comprehensive unit testing suite for torch.py and image.py
1 parent c868409 commit 43157d5

2 files changed

Lines changed: 175 additions & 0 deletions

File tree

tests/test_image.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import numpy as np
2+
from PIL import Image
3+
from unittest.mock import MagicMock, patch
4+
import pytest
5+
import supervision as sv
6+
from perceptionmetrics.utils.image import draw_detections
7+
8+
9+
def test_draw_detections_calls_supervision():
10+
# Setup
11+
img = Image.new("RGB", (100, 100))
12+
boxes = np.array([[0, 0, 10, 10]])
13+
class_ids = np.array([0])
14+
class_names = ["cat"]
15+
scores = np.array([0.9])
16+
17+
mock_box_annotator = MagicMock()
18+
mock_box_annotator.annotate.return_value = np.zeros((100, 100, 3), dtype=np.uint8)
19+
20+
with patch("perceptionmetrics.utils.image.sv.Detections") as mock_detections, patch(
21+
"perceptionmetrics.utils.image.sv.BoxAnnotator", return_value=mock_box_annotator
22+
), patch("perceptionmetrics.utils.image.sv.Color"), patch(
23+
"perceptionmetrics.utils.image.sv.ColorPalette"
24+
):
25+
26+
draw_detections(img, boxes, class_ids, class_names, scores)
27+
28+
assert mock_detections.called
29+
args, kwargs = mock_detections.call_args
30+
assert np.array_equal(kwargs["xyxy"], boxes)
31+
assert np.array_equal(kwargs["class_id"], class_ids)
32+
assert np.array_equal(kwargs["confidence"], scores)
33+
34+
assert mock_box_annotator.annotate.called
35+
36+
37+
def test_draw_detections_label_construction():
38+
# Setup
39+
img = Image.new("RGB", (100, 100))
40+
boxes = np.array([[0, 0, 10, 10], [10, 10, 20, 20]])
41+
class_ids = np.array([0, 1])
42+
class_names = ["cat", "dog"]
43+
scores = np.array([0.9, 0.8])
44+
45+
# First BoxAnnotator (older style) will fail on annotate
46+
mock_box_annotator_old = MagicMock()
47+
mock_box_annotator_old.annotate.side_effect = TypeError(
48+
"Simulation of mismatching arguments"
49+
)
50+
51+
# Second BoxAnnotator (modern style) will succeed
52+
mock_box_annotator_new = MagicMock()
53+
mock_box_annotator_new.annotate.return_value = np.zeros(
54+
(100, 100, 3), dtype=np.uint8
55+
)
56+
57+
mock_label_annotator = MagicMock()
58+
mock_label_annotator.annotate.return_value = np.zeros((100, 100, 3), dtype=np.uint8)
59+
60+
with patch(
61+
"perceptionmetrics.utils.image.sv.BoxAnnotator",
62+
side_effect=[mock_box_annotator_old, mock_box_annotator_new],
63+
), patch(
64+
"perceptionmetrics.utils.image.sv.LabelAnnotator",
65+
return_value=mock_label_annotator,
66+
), patch(
67+
"perceptionmetrics.utils.image.sv.Color"
68+
), patch(
69+
"perceptionmetrics.utils.image.sv.ColorPalette"
70+
):
71+
72+
draw_detections(img, boxes, class_ids, class_names, scores)
73+
74+
# Check labels passed to LabelAnnotator
75+
assert mock_label_annotator.annotate.called
76+
args, kwargs = mock_label_annotator.annotate.call_args
77+
labels = kwargs.get("labels")
78+
assert labels == ["cat: 0.90", "dog: 0.80"]
79+
80+
81+
def test_draw_detections_missing_names():
82+
img = Image.new("RGB", (100, 100))
83+
boxes = np.array([[0, 0, 10, 10]])
84+
class_ids = np.array([5])
85+
class_names = [] # No name for ID 5
86+
87+
mock_box_annotator_old = MagicMock()
88+
mock_box_annotator_old.annotate.side_effect = TypeError()
89+
mock_box_annotator_new = MagicMock()
90+
mock_box_annotator_new.annotate.return_value = np.zeros(
91+
(100, 100, 3), dtype=np.uint8
92+
)
93+
94+
mock_label_annotator = MagicMock()
95+
mock_label_annotator.annotate.return_value = np.zeros((100, 100, 3), dtype=np.uint8)
96+
97+
with patch(
98+
"perceptionmetrics.utils.image.sv.BoxAnnotator",
99+
side_effect=[mock_box_annotator_old, mock_box_annotator_new],
100+
), patch(
101+
"perceptionmetrics.utils.image.sv.LabelAnnotator",
102+
return_value=mock_label_annotator,
103+
), patch(
104+
"perceptionmetrics.utils.image.sv.Color"
105+
), patch(
106+
"perceptionmetrics.utils.image.sv.ColorPalette"
107+
):
108+
109+
draw_detections(img, boxes, class_ids, class_names)
110+
111+
assert mock_label_annotator.annotate.called
112+
args, kwargs = mock_label_annotator.annotate.call_args
113+
labels = kwargs.get("labels")
114+
assert labels == ["5"]

tests/test_torch.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import torch
2+
import pytest
3+
from perceptionmetrics.utils.torch import data_to_device, get_data_shape, unsqueeze_data
4+
5+
6+
def test_data_to_device():
7+
# Setup
8+
device = torch.device("cpu")
9+
t1 = torch.randn(2, 2)
10+
t2 = torch.randn(3, 3)
11+
data = [t1, (t2, "not_a_tensor")]
12+
13+
# Execute
14+
moved_data = data_to_device(data, device)
15+
16+
# Verify
17+
assert torch.equal(moved_data[0], t1.to(device))
18+
assert torch.equal(moved_data[1][0], t2.to(device))
19+
assert moved_data[1][1] == "not_a_tensor"
20+
assert isinstance(moved_data, list)
21+
assert isinstance(moved_data[1], tuple)
22+
23+
24+
def test_get_data_shape():
25+
# Setup
26+
t1 = torch.randn(2, 3)
27+
t2 = torch.randn(4, 5, 6)
28+
data = (t1, [t2, "label"])
29+
30+
# Execute
31+
shapes = get_data_shape(data)
32+
33+
# Verify
34+
assert shapes == ((2, 3), [(4, 5, 6), "label"])
35+
assert isinstance(shapes, tuple)
36+
assert isinstance(shapes[1], list)
37+
38+
39+
def test_unsqueeze_data():
40+
# Setup
41+
t1 = torch.randn(2, 2)
42+
t2 = torch.randn(3, 3)
43+
data = [t1, (t2, "text")]
44+
45+
# Execute
46+
unsqueezed = unsqueeze_data(data, dim=0)
47+
48+
# Verify
49+
assert unsqueezed[0].shape == (1, 2, 2)
50+
assert unsqueezed[1][0].shape == (1, 3, 3)
51+
assert unsqueezed[1][1] == "text"
52+
assert isinstance(unsqueezed, list)
53+
assert isinstance(unsqueezed[1], tuple)
54+
55+
56+
def test_torch_non_tensor_passthrough():
57+
# Ensure non-tensors are passed through correctly
58+
data = "string"
59+
assert data_to_device(data, torch.device("cpu")) == "string"
60+
assert get_data_shape(data) == "string"
61+
assert unsqueeze_data(data) == "string"

0 commit comments

Comments
 (0)