Skip to content

Commit 134fecf

Browse files
author
Matt Sokoloff
committed
uses png if no signed url provided
1 parent 3bf282d commit 134fecf

File tree

3 files changed

+93
-10
lines changed

3 files changed

+93
-10
lines changed

labelbox/data/serialization/ndjson/label.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ def _generate_annotations(
4242
NDConfusionMatrixMetric,
4343
NDScalarMetric]]]
4444
) -> Generator[Label, None, None]:
45-
4645
for data_row_id, annotations in grouped_annotations.items():
4746
annots = []
4847
for annotation in annotations:

labelbox/data/serialization/ndjson/objects.py

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1+
from ast import Bytes
2+
from io import BytesIO
13
from typing import Any, Dict, List, Tuple, Union
4+
import base64
5+
import numpy as np
26

37
from pydantic import BaseModel
8+
from PIL import Image
49

510
from ...annotation_types.data import ImageData, TextData, MaskData
611
from ...annotation_types.ner import TextEntity
@@ -113,28 +118,48 @@ def from_common(cls, rectangle: Rectangle,
113118
classifications=classifications)
114119

115120

116-
class _Mask(BaseModel):
121+
class _URIMask(BaseModel):
117122
instanceURI: str
118123
colorRGB: Tuple[int, int, int]
119124

120125

126+
class _PNGMask(BaseModel):
127+
png: str
128+
129+
121130
class NDMask(NDBaseObject):
122-
mask: _Mask
131+
mask: Union[_URIMask, _PNGMask]
123132

124133
def to_common(self) -> Mask:
125-
return Mask(mask=MaskData(url=self.mask.instanceURI),
126-
color=self.mask.colorRGB)
134+
if isinstance(self.mask, _URIMask):
135+
return Mask(mask=MaskData(url=self.mask.instanceURI),
136+
color=self.mask.colorRGB)
137+
else:
138+
encoded_image_bytes = self.mask.png.encode('utf-8')
139+
image_bytes = base64.b64decode(encoded_image_bytes)
140+
image = np.array(Image.open(BytesIO(image_bytes)))
141+
if np.max(image) > 1:
142+
raise ValueError(
143+
f"Expected binary mask. Found max value of {np.max(image)}")
144+
# Color is 1,1,1 because it is a binary array and we are just stacking it into 3 channels
145+
return Mask(mask=MaskData.from_2D_arr(image), color=(1, 1, 1))
127146

128147
@classmethod
129148
def from_common(cls, mask: Mask,
130149
classifications: List[ClassificationAnnotation],
131150
feature_schema_id: Cuid, extra: Dict[str, Any],
132151
data: Union[ImageData, TextData]) -> "NDMask":
133-
if mask.mask.url is None:
134-
raise ValueError(
135-
"Mask does not have a url. Use `LabelGenerator.add_url_to_masks`, `LabelList.add_url_to_masks`, or `Label.add_url_to_masks`."
136-
)
137-
return cls(mask=_Mask(instanceURI=mask.mask.url, colorRGB=mask.color),
152+
153+
if mask.mask.url is not None:
154+
lbv1_mask = _URIMask(instanceURI=mask.mask.url, colorRGB=mask.color)
155+
else:
156+
binary = np.all(mask.mask.value == mask.color, axis=-1)
157+
im_bytes = BytesIO()
158+
Image.fromarray(binary, 'L').save(im_bytes, format="PNG")
159+
lbv1_mask = _PNGMask(
160+
png=base64.b64encode(im_bytes.getvalue()).decode('utf-8'))
161+
162+
return cls(mask=lbv1_mask,
138163
dataRow=DataRow(id=data.uid),
139164
schema_id=feature_schema_id,
140165
uuid=extra.get('uuid'),

tests/data/serialization/ndjson/test_image.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import json
2+
import numpy as np
3+
import cv2
24

35
from labelbox.data.serialization.ndjson.converter import NDJsonConverter
6+
from labelbox.data.annotation_types import Mask, Label, ObjectAnnotation, ImageData, MaskData
47

58

69
def round_dict(data):
@@ -25,3 +28,59 @@ def test_image():
2528
for r in res:
2629
r.pop('classifications', None)
2730
assert [round_dict(x) for x in res] == [round_dict(x) for x in data]
31+
32+
33+
def test_mask():
34+
data = [{
35+
"uuid": "b862c586-8614-483c-b5e6-82810f70cac0",
36+
"schemaId": "ckrazcueb16og0z6609jj7y3y",
37+
"dataRow": {
38+
"id": "ckrazctum0z8a0ybc0b0o0g0v"
39+
},
40+
"mask": {
41+
"png":
42+
"iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAAAAACoWZBhAAAAMklEQVR4nD3MuQ3AQADDMOqQ/Vd2ijytaSiZLAcYuyLEYYYl9cvrlGftTHvsYl+u/3EDv0QLI8Z7FlwAAAAASUVORK5CYII="
43+
}
44+
}, {
45+
"uuid": "751fc725-f7b6-48ed-89b0-dd7d94d08af6",
46+
"schemaId": "ckrazcuec16ok0z66f956apb7",
47+
"dataRow": {
48+
"id": "ckrazctum0z8a0ybc0b0o0g0v"
49+
},
50+
"mask": {
51+
"instanceURI":
52+
"https://storage.labelbox.com/ckqcx1czn06830y61gh9v02cs%2F3e729327-f038-f66c-186e-45e921ef9717-1?Expires=1626806874672&KeyName=labelbox-assets-key-3&Signature=YsUOGKrsqmAZ68vT9BlPJOaRyLY",
53+
"colorRGB": [255, 0, 0]
54+
}
55+
}]
56+
res = NDJsonConverter.deserialize(data).as_list()
57+
res = list(NDJsonConverter.serialize(res))
58+
for r in res:
59+
r.pop('classifications', None)
60+
61+
assert [round_dict(x) for x in res] == [round_dict(x) for x in data]
62+
63+
64+
def test_mask_from_arr():
65+
mask_arr = np.round(np.zeros((32, 32))).astype(np.uint8)
66+
mask_arr = cv2.rectangle(mask_arr, (5, 5), (10, 10), (1, 1), -1)
67+
68+
label = Label(annotations=[
69+
ObjectAnnotation(feature_schema_id="1" * 25,
70+
value=Mask(mask=MaskData.from_2D_arr(arr=mask_arr),
71+
color=(1, 1, 1)))
72+
],
73+
data=ImageData(uid="0" * 25))
74+
res = next(NDJsonConverter.serialize([label]))
75+
res.pop('uuid')
76+
assert res == {
77+
"classifications": [],
78+
"schemaId": "1" * 25,
79+
"dataRow": {
80+
"id": "0" * 25
81+
},
82+
"mask": {
83+
"png":
84+
"iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAAAAABWESUoAAAAHklEQVR4nGNgGAKAEYn8j00BEyETBoOCUTAKhhwAAJW+AQwvpePVAAAAAElFTkSuQmCC"
85+
}
86+
}

0 commit comments

Comments
 (0)