Skip to content

Commit 18a3d07

Browse files
authored
V7 2629 support attributes (#319)
* Added types and docs * adding support for attribute imports * Fixed tests and added check in schema * added attribute import to other types * improving attribute not imported message * Added Typing to file * small readability refactor * linter fixes
1 parent 83a347c commit 18a3d07

File tree

6 files changed

+329
-56
lines changed

6 files changed

+329
-56
lines changed

darwin/datatypes.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -602,7 +602,20 @@ def make_instance_id(value: int) -> SubAnnotation:
602602
return SubAnnotation("instance_id", value)
603603

604604

605-
def make_attributes(attributes: Any) -> SubAnnotation:
605+
def make_attributes(attributes: List[str]) -> SubAnnotation:
606+
"""
607+
Creates and returns an attributes sub-annotation.
608+
609+
Parameters
610+
----------
611+
value: List[str]
612+
A list of attributes. Example: ``["orange", "big"]``.
613+
614+
Returns
615+
-------
616+
SubAnnotation
617+
An attributes ``SubAnnotation``.
618+
"""
606619
return SubAnnotation("attributes", attributes)
607620

608621

darwin/importer/formats/superannotate.py

Lines changed: 83 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
AnnotationFile,
1313
CuboidData,
1414
Point,
15+
SubAnnotation,
16+
make_attributes,
1517
make_bounding_box,
1618
make_cuboid,
1719
make_ellipse,
@@ -24,6 +26,8 @@
2426
superannotate_export,
2527
)
2628

29+
AttributeGroup = Dict[str, Union[str, int]]
30+
2731

2832
def parse_path(path: Path) -> Optional[AnnotationFile]:
2933
"""
@@ -158,8 +162,13 @@ def _to_keypoint_annotation(point: Dict[str, Any], classes: List[Dict[str, Any]]
158162
y: float = cast(float, point.get("y"))
159163
class_id: int = cast(int, point.get("classId"))
160164

161-
name = _find_class_name(class_id, classes)
162-
return make_keypoint(f"{name}-point", x, y)
165+
instance_class: Dict[str, Any] = _find_class(class_id, classes)
166+
name: str = str(instance_class.get("name"))
167+
attributes: Optional[SubAnnotation] = _get_attributes(point, instance_class)
168+
subannotations: Optional[List[SubAnnotation]] = None
169+
if attributes:
170+
subannotations = [attributes]
171+
return make_keypoint(f"{name}-point", x, y, subannotations)
163172

164173

165174
def _to_bbox_annotation(bbox: Dict[str, Any], classes: List[Dict[str, Any]]) -> Annotation:
@@ -170,8 +179,14 @@ def _to_bbox_annotation(bbox: Dict[str, Any], classes: List[Dict[str, Any]]) ->
170179
h: float = abs(cast(float, points.get("y1")) - cast(float, points.get("y2")))
171180
class_id: int = cast(int, bbox.get("classId"))
172181

173-
name = _find_class_name(class_id, classes)
174-
return make_bounding_box(f"{name}-bbox", x, y, w, h)
182+
instance_class: Dict[str, Any] = _find_class(class_id, classes)
183+
name: str = str(instance_class.get("name"))
184+
attributes: Optional[SubAnnotation] = _get_attributes(bbox, instance_class)
185+
subannotations: Optional[List[SubAnnotation]] = None
186+
if attributes:
187+
subannotations = [attributes]
188+
189+
return make_bounding_box(f"{name}-bbox", x, y, w, h, subannotations)
175190

176191

177192
def _to_ellipse_annotation(ellipse: Dict[str, Any], classes: List[Dict[str, Any]]) -> Annotation:
@@ -181,8 +196,14 @@ def _to_ellipse_annotation(ellipse: Dict[str, Any], classes: List[Dict[str, Any]
181196
ellipse_data: Dict[str, Union[float, Point]] = {"angle": angle, "center": center, "radius": radius}
182197
class_id: int = cast(int, ellipse.get("classId"))
183198

184-
name = _find_class_name(class_id, classes)
185-
return make_ellipse(f"{name}-ellipse", ellipse_data)
199+
instance_class: Dict[str, Any] = _find_class(class_id, classes)
200+
name: str = str(instance_class.get("name"))
201+
attributes: Optional[SubAnnotation] = _get_attributes(ellipse, instance_class)
202+
subannotations: Optional[List[SubAnnotation]] = None
203+
if attributes:
204+
subannotations = [attributes]
205+
206+
return make_ellipse(f"{name}-ellipse", ellipse_data, subannotations)
186207

187208

188209
def _to_cuboid_annotation(cuboid: Dict[str, Any], classes: List[Dict[str, Any]]) -> Annotation:
@@ -208,37 +229,85 @@ def _to_cuboid_annotation(cuboid: Dict[str, Any], classes: List[Dict[str, Any]])
208229
}
209230
class_id: int = cast(int, cuboid.get("classId"))
210231

211-
name = _find_class_name(class_id, classes)
212-
return make_cuboid(f"{name}-cuboid", cuboid_data)
232+
instance_class: Dict[str, Any] = _find_class(class_id, classes)
233+
name: str = str(instance_class.get("name"))
234+
attributes: Optional[SubAnnotation] = _get_attributes(cuboid, instance_class)
235+
subannotations: Optional[List[SubAnnotation]] = None
236+
if attributes:
237+
subannotations = [attributes]
238+
239+
return make_cuboid(f"{name}-cuboid", cuboid_data, subannotations)
213240

214241

215242
def _to_polygon_annotation(polygon: Dict[str, Any], classes: List[Dict[str, Any]]) -> Annotation:
216243
data: List[float] = cast(List[float], polygon.get("points"))
217244
class_id: int = cast(int, polygon.get("classId"))
218-
name: str = _find_class_name(class_id, classes)
245+
instance_class: Dict[str, Any] = _find_class(class_id, classes)
246+
name: str = str(instance_class.get("name"))
219247
points: List[Point] = _map_to_list(_tuple_to_point, _group_to_list(data, 2, 0))
220248

221-
return make_polygon(f"{name}-polygon", points)
249+
attributes: Optional[SubAnnotation] = _get_attributes(polygon, instance_class)
250+
subannotations: Optional[List[SubAnnotation]] = None
251+
if attributes:
252+
subannotations = [attributes]
253+
return make_polygon(f"{name}-polygon", points, None, subannotations)
222254

223255

224256
def _to_line_annotation(line: Dict[str, Any], classes: List[Dict[str, Any]]) -> Annotation:
225257
data: List[float] = cast(List[float], line.get("points"))
226258
class_id: int = cast(int, line.get("classId"))
227-
name: str = _find_class_name(class_id, classes)
259+
instance_class: Dict[str, Any] = _find_class(class_id, classes)
260+
name: str = str(instance_class.get("name"))
228261
points: List[Point] = _map_to_list(_tuple_to_point, _group_to_list(data, 2, 0))
262+
attributes: Optional[SubAnnotation] = _get_attributes(line, instance_class)
263+
subannotations: Optional[List[SubAnnotation]] = None
264+
if attributes:
265+
subannotations = [attributes]
229266

230-
return make_line(f"{name}-polyline", points)
267+
return make_line(f"{name}-polyline", points, subannotations)
231268

232269

233-
def _find_class_name(class_id: int, classes: List[Dict[str, Any]]) -> str:
270+
def _find_class(class_id: int, classes: List[Dict[str, Any]]) -> Dict[str, Any]:
234271
obj: Optional[Dict[str, Any]] = next((class_obj for class_obj in classes if class_obj.get("id") == class_id), None)
235272

236273
if obj is None:
237274
raise ValueError(
238275
f"No class with id '{class_id}' was found in {classes}.\nCannot continue import, pleaase check your 'classes.json' file."
239276
)
240277

241-
return str(obj.get("name"))
278+
return obj
279+
280+
281+
def _get_attributes(instance: Dict[str, Any], instance_class: Dict[str, Any]) -> Optional[SubAnnotation]:
282+
attribute_info: List[Dict[str, int]] = cast(List[Dict[str, int]], instance.get("attributes"))
283+
groups: List[Dict[str, Any]] = cast(List[Dict[str, Any]], instance_class.get("attribute_groups"))
284+
all_attributes: List[str] = []
285+
286+
for info in attribute_info:
287+
info_group_id: int = cast(int, info.get("groupId"))
288+
attribute_id: int = cast(int, info.get("id"))
289+
290+
for group in groups:
291+
group_id: int = cast(int, group.get("id"))
292+
293+
if info_group_id != group_id:
294+
continue
295+
296+
group_attributes: List[AttributeGroup] = cast(List[AttributeGroup], group.get("attributes"))
297+
attribute: Optional[AttributeGroup] = next(
298+
(attribute for attribute in group_attributes if attribute.get("id") == attribute_id), None
299+
)
300+
301+
if attribute is None:
302+
raise ValueError(f"No attribute data found for {info}.")
303+
304+
final_attribute: str = f"{str(group.get('name'))}:{str(attribute.get('name'))}"
305+
all_attributes.append(final_attribute)
306+
307+
if all_attributes == []:
308+
return None
309+
310+
return make_attributes(all_attributes)
242311

243312

244313
def _get_class(annotation: Annotation) -> AnnotationClass:

darwin/importer/formats/superannotate_schemas.py

Lines changed: 66 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,33 @@
1+
attributes = {
2+
"type": "array",
3+
"items": {
4+
"type": "object",
5+
"properties": {"id": {"type": "integer"}, "groupId": {"type": "integer"}},
6+
"required": ["id", "groupId"],
7+
},
8+
}
9+
110
bbox = {
211
"$id": "https://darwin.v7labs.com/schemas/supperannotate/bounding_box",
312
"description": "Schema of a Bounding Box",
413
"title": "Bounding Box",
5-
"default": {"type": "bbox", "points": {"x1": 1223.1, "x2": 1420.2, "y1": 607.3, "y2": 1440,}, "classId": 1,},
6-
"examples": [{"type": "bbox", "points": {"x1": 587.5, "x2": 1420.2, "y1": 607.3, "y2": 1440,}, "classId": 1,}],
14+
"default": {
15+
"type": "bbox",
16+
"points": {"x1": 1223.1, "x2": 1420.2, "y1": 607.3, "y2": 1440,},
17+
"classId": 1,
18+
"attributes": [],
19+
},
20+
"examples": [
21+
{
22+
"type": "bbox",
23+
"points": {"x1": 587.5, "x2": 1420.2, "y1": 607.3, "y2": 1440,},
24+
"classId": 1,
25+
"attributes": [{"id": 1, "groupId": 2}],
26+
}
27+
],
728
"type": "object",
829
"properties": {
30+
"attributes": attributes,
931
"classId": {"type": "integer"},
1032
"type": {"enum": ["bbox"]},
1133
"points": {
@@ -19,7 +41,7 @@
1941
"required": ["x1", "x2", "y1", "y2"],
2042
},
2143
},
22-
"required": ["points", "type", "classId"],
44+
"required": ["points", "type", "classId", "attributes"],
2345
}
2446

2547
polygon = {
@@ -28,16 +50,17 @@
2850
"title": "Polygon",
2951
"default": {"type": "polygon", "points": [1, 2, 3, 4], "classId": 1},
3052
"examples": [
31-
{"type": "polygon", "points": [1, 2, 3, 4], "classId": 1},
32-
{"type": "polygon", "points": [], "classId": 1},
53+
{"type": "polygon", "points": [1, 2, 3, 4], "classId": 1, "attributes": [{"id": 1, "groupId": 2}]},
54+
{"type": "polygon", "points": [], "classId": 1, "attributes": []},
3355
],
3456
"type": "object",
3557
"properties": {
58+
"attributes": attributes,
3659
"classId": {"type": "integer"},
3760
"points": {"type": "array", "items": {"type": "number"}},
3861
"type": {"enum": ["polygon"]},
3962
},
40-
"required": ["points", "type", "classId"],
63+
"required": ["points", "type", "classId", "attributes"],
4164
}
4265

4366
polyline = {
@@ -46,16 +69,17 @@
4669
"title": "Polyline",
4770
"default": {"type": "polyline", "points": [1, 2, 3, 4], "classId": 1},
4871
"examples": [
49-
{"type": "polyline", "points": [1, 2, 3, 4], "classId": 1},
50-
{"type": "polyline", "points": [], "classId": 1},
72+
{"type": "polyline", "points": [1, 2, 3, 4], "classId": 1, "attributes": [{"id": 1, "groupId": 2}]},
73+
{"type": "polyline", "points": [], "classId": 1, "attributes": []},
5174
],
5275
"type": "object",
5376
"properties": {
77+
"attributes": attributes,
5478
"classId": {"type": "integer"},
5579
"points": {"type": "array", "items": {"type": "number"}},
5680
"type": {"enum": ["polyline"]},
5781
},
58-
"required": ["points", "type", "classId"],
82+
"required": ["points", "type", "classId", "attributes"],
5983
}
6084

6185
cuboid = {
@@ -71,6 +95,7 @@
7195
"r2": {"x": 1603.4, "y": 1440},
7296
},
7397
"classId": 1,
98+
"attributes": [{"id": 1, "groupId": 2}],
7499
},
75100
"examples": [
76101
{
@@ -82,10 +107,12 @@
82107
"r2": {"x": 1603.4, "y": 1440},
83108
},
84109
"classId": 1,
110+
"attributes": [],
85111
}
86112
],
87113
"type": "object",
88114
"properties": {
115+
"attributes": attributes,
89116
"classId": {"type": "integer"},
90117
"type": {"enum": ["cuboid"]},
91118
"points": {
@@ -115,19 +142,38 @@
115142
"required": ["f1", "f2", "r1", "r2"],
116143
},
117144
},
118-
"required": ["points", "type", "classId"],
145+
"required": ["points", "type", "classId", "attributes"],
119146
}
120147

121148
ellipse = {
122149
"$id": "https://darwin.v7labs.com/schemas/supperannotate/ellipse",
123150
"description": "Schema of an Ellipse",
124151
"title": "Ellipse",
125-
"default": {"type": "ellipse", "cx": 377.46, "cy": 806.18, "rx": 316.36, "ry": 134.18, "angle": 0, "classId": 1},
152+
"default": {
153+
"type": "ellipse",
154+
"cx": 377.46,
155+
"cy": 806.18,
156+
"rx": 316.36,
157+
"ry": 134.18,
158+
"angle": 0,
159+
"classId": 1,
160+
"attributes": [],
161+
},
126162
"examples": [
127-
{"type": "ellipse", "cx": 377.46, "cy": 806.18, "rx": 316.36, "ry": 134.18, "angle": 14.66, "classId": 1}
163+
{
164+
"type": "ellipse",
165+
"cx": 377.46,
166+
"cy": 806.18,
167+
"rx": 316.36,
168+
"ry": 134.18,
169+
"angle": 14.66,
170+
"classId": 1,
171+
"attributes": [{"id": 1, "groupId": 2}],
172+
}
128173
],
129174
"type": "object",
130175
"properties": {
176+
"attributes": attributes,
131177
"classId": {"type": "integer"},
132178
"cx": {"type": "number"},
133179
"cy": {"type": "number"},
@@ -136,23 +182,27 @@
136182
"angle": {"type": "number"},
137183
"type": {"enum": ["ellipse"]},
138184
},
139-
"required": ["cx", "cy", "rx", "ry", "angle", "type", "classId"],
185+
"required": ["cx", "cy", "rx", "ry", "angle", "type", "classId", "attributes"],
140186
}
141187

142188
point = {
143189
"$id": "https://darwin.v7labs.com/schemas/supperannotate/point",
144190
"description": "Schema of a Point",
145191
"title": "Point",
146-
"default": {"type": "point", "x": 1.2, "y": 2.5, "classId": 1},
147-
"examples": [{"type": "point", "x": 1.2, "y": 2.5, "classId": 1}, {"type": "point", "x": 0, "y": 1, "classId": 2}],
192+
"default": {"type": "point", "x": 1.2, "y": 2.5, "classId": 1, "attributes": []},
193+
"examples": [
194+
{"type": "point", "x": 1.2, "y": 2.5, "classId": 1, "attributes": []},
195+
{"type": "point", "x": 0, "y": 1, "classId": 2, "attributes": [{"id": 1, "groupId": 2}]},
196+
],
148197
"type": "object",
149198
"properties": {
199+
"attributes": attributes,
150200
"classId": {"type": "integer"},
151201
"x": {"type": "number"},
152202
"y": {"type": "number"},
153203
"type": {"enum": ["point"]},
154204
},
155-
"required": ["x", "y", "type", "classId"],
205+
"required": ["x", "y", "type", "classId", "attributes"],
156206
}
157207

158208

darwin/importer/importer.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -273,17 +273,19 @@ def _handle_subs(
273273
if sub.annotation_type == "text":
274274
data["text"] = {"text": sub.data}
275275
elif sub.annotation_type == "attributes":
276-
data["attributes"] = {
277-
"attributes": [
278-
attributes[annotation_class_id][attr]
279-
for attr in sub.data
280-
if annotation_class_id in attributes and attr in attributes[annotation_class_id]
281-
]
282-
}
276+
attributes_with_key = []
277+
for attr in sub.data:
278+
if annotation_class_id in attributes and attr in attributes[annotation_class_id]:
279+
attributes_with_key.append(attributes[annotation_class_id][attr])
280+
else:
281+
print(f"The attribute '{attr}' for class '{annotation.annotation_class.name}' was not imported.")
282+
283+
data["attributes"] = {"attributes": attributes_with_key}
283284
elif sub.annotation_type == "instance_id":
284285
data["instance_id"] = {"value": sub.data}
285286
else:
286287
data[sub.annotation_type] = sub.data
288+
287289
return data
288290

289291

0 commit comments

Comments
 (0)