Skip to content

Commit 57fbe17

Browse files
iurisilvioclaude
andauthored
Fix the crashes in Prediction.save (#560)
Prediction.save() raised before writing the file for most predictions: - Object detection and keypoint: the label background got float corners (x - width / 2, ...), which cv2.rectangle rejects on every supported OpenCV. The corners are now ints, and the label text sits on that background at the top of the box, where PredictionGroup.save puts it. - Classification: "Confidence: " + confidence failed on the float the API returns, so the confidence goes through str() first. - Hosted images: only http:// URLs were downloaded, so an https:// image went to cv2.imread and came back empty. Both are downloaded now. - Semantic segmentation: mask_image decoded the mask with np.fromstring, whose binary mode NumPy 2.3 removed. It uses np.frombuffer now, which fixes PredictionGroup.save for semantic segmentation too. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent b5e42ef commit 57fbe17

3 files changed

Lines changed: 96 additions & 7 deletions

File tree

‎roboflow/util/image_utils.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def mask_image(image, encoded_mask, transparency=60):
5555
import cv2
5656
import numpy as np
5757

58-
np_data = np.fromstring(base64.b64decode(encoded_mask), np.uint8) # type: ignore[no-overload]
58+
np_data = np.frombuffer(base64.b64decode(encoded_mask), np.uint8)
5959
mask = cv2.imdecode(np_data, cv2.IMREAD_UNCHANGED)
6060

6161
# Fallback in case the API returns an incorrectly sized mask

‎roboflow/util/prediction.py‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ def __load_image(self):
126126
import cv2
127127
import numpy as np
128128

129-
if "http://" in self.image_path:
129+
if "http://" in self.image_path or "https://" in self.image_path:
130130
req = urllib.request.urlopen(
131131
urllib.request.Request(self.image_path, headers={"User-Agent": http.USER_AGENT})
132132
)
@@ -184,10 +184,10 @@ def save(self, output_path="predictions.jpg", stroke=2, transparency=60):
184184
# Draw background rectangle for text
185185
cv2.rectangle(
186186
image,
187-
(x - width / 2, y - height / 2 + 1),
187+
(int(x - width / 2), int(y - height / 2 + 1)),
188188
(
189-
x - width / 2 + text_size[0] + 1,
190-
y - height / 2 + int(1.5 * text_size[1]),
189+
int(x - width / 2 + text_size[0] + 1),
190+
int(y - height / 2 + int(1.5 * text_size[1])),
191191
),
192192
stroke_color,
193193
-1,
@@ -196,7 +196,7 @@ def save(self, output_path="predictions.jpg", stroke=2, transparency=60):
196196
cv2.putText(
197197
image,
198198
class_name,
199-
(int(x - width / 2), y + text_size[1]),
199+
(int(x - width / 2), int(y - height / 2 + text_size[1])),
200200
cv2.FONT_HERSHEY_SIMPLEX,
201201
0.4,
202202
(255, 255, 255),
@@ -226,7 +226,7 @@ def save(self, output_path="predictions.jpg", stroke=2, transparency=60):
226226
# Add text and relax
227227
cv2.putText(
228228
image,
229-
(self["top"] + " | " + "Confidence: " + self["confidence"]),
229+
(self["top"] + " | " + "Confidence: " + str(self["confidence"])),
230230
(int(width / 2), 5),
231231
cv2.FONT_HERSHEY_DUPLEX,
232232
0.5,

‎tests/util/test_prediction.py‎

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import os
2+
import tempfile
3+
import unittest
4+
from pathlib import Path
5+
from unittest import mock
6+
7+
from roboflow.config import (
8+
CLASSIFICATION_MODEL,
9+
KEYPOINT_DETECTION_MODEL,
10+
OBJECT_DETECTION_MODEL,
11+
SEMANTIC_SEGMENTATION_MODEL,
12+
)
13+
from roboflow.util.prediction import Prediction
14+
15+
IMAGE_PATH = "tests/images/rabbit.JPG"
16+
BOX = {"x": 300.5, "y": 200.5, "width": 120.0, "height": 90.0, "class": "rabbit", "confidence": 0.91}
17+
18+
19+
class TestPredictionSave(unittest.TestCase):
20+
def _save(self, prediction):
21+
"""Save the prediction as a PNG, so pixels aren't altered by compression, and read it back."""
22+
import cv2
23+
24+
with tempfile.TemporaryDirectory() as tmp:
25+
output_path = os.path.join(tmp, "prediction.png")
26+
prediction.save(output_path)
27+
image = cv2.imread(output_path)
28+
self.assertIsNotNone(image)
29+
return image
30+
31+
def test_object_detection_draws_the_label_on_its_background(self):
32+
import cv2
33+
import numpy as np
34+
35+
image = self._save(Prediction(dict(BOX), IMAGE_PATH, prediction_type=OBJECT_DETECTION_MODEL))
36+
37+
# The label background is filled with the stroke color, so white pixels in it can only be the label text.
38+
text_width, text_height = cv2.getTextSize("rabbit", cv2.FONT_HERSHEY_SIMPLEX, 0.4, 1)[0]
39+
left = int(BOX["x"] - BOX["width"] / 2)
40+
top = int(BOX["y"] - BOX["height"] / 2)
41+
label = image[top + 1 : top + int(1.5 * text_height), left : left + text_width + 1]
42+
self.assertTrue(np.all(label[0, -1] == (255, 0, 0)))
43+
self.assertTrue(np.any(np.all(label == 255, axis=-1)))
44+
45+
def test_keypoint_detection(self):
46+
image = self._save(Prediction(dict(BOX, keypoints=[]), IMAGE_PATH, prediction_type=KEYPOINT_DETECTION_MODEL))
47+
self.assertEqual(image.shape, (992, 1024, 3))
48+
49+
def test_classification(self):
50+
prediction = Prediction(
51+
{"top": "rabbit", "confidence": 0.91, "predictions": [{"class": "rabbit", "confidence": 0.91}]},
52+
IMAGE_PATH,
53+
prediction_type=CLASSIFICATION_MODEL,
54+
)
55+
image = self._save(prediction)
56+
# The label goes on a border around the image.
57+
self.assertEqual(image.shape, (992 + 200, 1024 + 200, 3))
58+
59+
def test_semantic_segmentation(self):
60+
import base64
61+
62+
import cv2
63+
import numpy as np
64+
65+
mask = np.zeros((992, 1024), np.uint8)
66+
mask[100:300, 100:300] = 255
67+
encoded_mask = base64.b64encode(cv2.imencode(".png", mask)[1].tobytes()).decode()
68+
prediction = Prediction(
69+
{"segmentation_mask": encoded_mask, "class_map": {"1": "rabbit"}},
70+
IMAGE_PATH,
71+
prediction_type=SEMANTIC_SEGMENTATION_MODEL,
72+
)
73+
74+
image = self._save(prediction)
75+
76+
self.assertEqual(image.shape, (992, 1024, 3))
77+
78+
@mock.patch("urllib.request.urlopen")
79+
def test_hosted_https_image(self, urlopen):
80+
urlopen.return_value.read.return_value = Path(IMAGE_PATH).read_bytes()
81+
82+
image = self._save(Prediction(dict(BOX), "https://example.com/rabbit.jpg"))
83+
84+
urlopen.assert_called_once()
85+
self.assertEqual(image.shape, (992, 1024, 3))
86+
87+
88+
if __name__ == "__main__":
89+
unittest.main()

0 commit comments

Comments
 (0)