Skip to content

Commit b0fd186

Browse files
authored
Merge pull request #31 from DSD-DBS/annotation-type-in-export
Annotation type in export
2 parents 6449713 + 868b42d commit b0fd186

9 files changed

Lines changed: 203 additions & 235 deletions

File tree

raillabel_providerkit/__main__.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@
1010
from tqdm import tqdm
1111

1212
from raillabel_providerkit import validate
13-
from raillabel_providerkit.validation.issue import Issue
14-
15-
ISSUES_SCHEMA = Path(__file__).parent / "validation" / "issues_schema.json"
13+
from raillabel_providerkit.validation.issue import ISSUES_SCHEMA, Issue
1614

1715

1816
def store_issues_to_json(issues: list[Issue], filepath: Path) -> None:
@@ -36,11 +34,8 @@ def store_issues_to_json(issues: list[Issue], filepath: Path) -> None:
3634
def _adheres_to_issues_schema(
3735
data: list[dict[str, str | dict[str, str | int] | list[str | int]]],
3836
) -> bool:
39-
schema: dict
40-
with ISSUES_SCHEMA.open("r") as file:
41-
schema = json.load(file)
4237
try:
43-
jsonschema.validate(data, schema)
38+
jsonschema.validate(data, ISSUES_SCHEMA)
4439
except jsonschema.ValidationError:
4540
return False
4641

raillabel_providerkit/validation/issue.py

Lines changed: 109 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33

44
from dataclasses import dataclass
55
from enum import Enum
6+
from typing import Literal
67
from uuid import UUID
78

9+
import jsonschema
10+
811

912
class IssueType(Enum):
1013
"""General classification of the issue."""
@@ -26,12 +29,18 @@ class IssueType(Enum):
2629
UNEXPECTED_CLASS = "UnexpectedClassIssue"
2730
URI_FORMAT = "UriFormatIssue"
2831

32+
@classmethod
33+
def names(cls) -> list[str]:
34+
"""Return the string names of all IssueTypes as a list."""
35+
return [type_.value for type_ in cls]
36+
2937

3038
@dataclass
3139
class IssueIdentifiers:
3240
"""Information for locating an issue."""
3341

3442
annotation: UUID | None = None
43+
annotation_type: Literal["Bbox", "Cuboid", "Num", "Poly2d", "Poly3d", "Seg3d"] | None = None
3544
attribute: str | None = None
3645
frame: int | None = None
3746
object: UUID | None = None
@@ -46,28 +55,25 @@ def serialize(self) -> dict[str, str | int]:
4655
dict[str, str | int]
4756
The serialized IssueIdentifiers as a JSON-compatible dictionary
4857
"""
49-
serialized_issue_identifiers: dict[str, str | int] = {}
50-
if self.annotation is not None:
51-
serialized_issue_identifiers["annotation"] = str(self.annotation)
52-
if self.attribute is not None:
53-
serialized_issue_identifiers["attribute"] = self.attribute
54-
if self.frame is not None:
55-
serialized_issue_identifiers["frame"] = self.frame
56-
if self.object is not None:
57-
serialized_issue_identifiers["object"] = str(self.object)
58-
if self.object_type is not None:
59-
serialized_issue_identifiers["object_type"] = self.object_type
60-
if self.sensor is not None:
61-
serialized_issue_identifiers["sensor"] = self.sensor
62-
return serialized_issue_identifiers
58+
return _clean_dict(
59+
{
60+
"annotation": str(self.annotation),
61+
"annotation_type": self.annotation_type,
62+
"attribute": self.attribute,
63+
"frame": self.frame,
64+
"object": str(self.object),
65+
"object_type": self.object_type,
66+
"sensor": self.sensor,
67+
}
68+
)
6369

6470
@classmethod
65-
def deserialize(cls, serialized_issue_identifiers: dict[str, str | int]) -> "IssueIdentifiers": # noqa: C901
71+
def deserialize(cls, serialized_identifiers: dict[str, str | int]) -> "IssueIdentifiers":
6672
"""Deserialize a JSON-compatible dictionary back into an IssueIdentifiers class instance.
6773
6874
Parameters
6975
----------
70-
serialized_issue_identifiers : dict[str, str | int]
76+
serialized_identifiers : dict[str, str | int]
7177
The serialized IssueIdentifiers as a JSON-compatible dictionary
7278
7379
Returns
@@ -80,45 +86,20 @@ def deserialize(cls, serialized_issue_identifiers: dict[str, str | int]) -> "Iss
8086
TypeError
8187
If any of the fields have an unexpected type
8288
"""
83-
identifiers = IssueIdentifiers()
84-
85-
annotation = serialized_issue_identifiers.get("annotation")
86-
if isinstance(annotation, int):
87-
raise TypeError
88-
if annotation is not None:
89-
identifiers.annotation = UUID(annotation)
90-
91-
attribute = serialized_issue_identifiers.get("attribute")
92-
if isinstance(attribute, int):
93-
raise TypeError
94-
if attribute is not None:
95-
identifiers.attribute = attribute
96-
97-
frame = serialized_issue_identifiers.get("frame")
98-
if isinstance(frame, str):
99-
raise TypeError
100-
if frame is not None:
101-
identifiers.frame = frame
102-
103-
object = serialized_issue_identifiers.get("object") # noqa: A001
104-
if isinstance(object, int):
105-
raise TypeError
106-
if object is not None:
107-
identifiers.object = UUID(object)
108-
109-
object_type = serialized_issue_identifiers.get("object_type")
110-
if isinstance(object_type, int):
111-
raise TypeError
112-
if object_type is not None:
113-
identifiers.object_type = object_type
114-
115-
sensor = serialized_issue_identifiers.get("sensor")
116-
if isinstance(sensor, int):
117-
raise TypeError
118-
if sensor is not None:
119-
identifiers.sensor = sensor
120-
121-
return identifiers
89+
_verify_identifiers_schema(serialized_identifiers)
90+
return IssueIdentifiers(
91+
annotation=UUID(serialized_identifiers.get("annotation"))
92+
if serialized_identifiers.get("annotation") is not None
93+
else None,
94+
annotation_type=serialized_identifiers.get("annotation_type"),
95+
attribute=serialized_identifiers.get("attribute"),
96+
frame=serialized_identifiers.get("frame"),
97+
object=UUID(serialized_identifiers.get("object"))
98+
if serialized_identifiers.get("object") is not None
99+
else None,
100+
object_type=serialized_identifiers.get("object_type"),
101+
sensor=serialized_identifiers.get("sensor"),
102+
)
122103

123104

124105
@dataclass
@@ -137,17 +118,17 @@ def serialize(self) -> dict[str, str | dict[str, str | int] | list[str | int]]:
137118
dict[str, str | dict[str, str | int] | list[str | int]]
138119
The serialized Issue as a JSON-compatible dictionary
139120
"""
140-
serialized_issue = {
141-
"type": str(self.type.value),
142-
"identifiers": (
143-
self.identifiers.serialize()
144-
if isinstance(self.identifiers, IssueIdentifiers)
145-
else self.identifiers
146-
),
147-
}
148-
if self.reason is not None:
149-
serialized_issue["reason"] = self.reason
150-
return serialized_issue
121+
return _clean_dict(
122+
{
123+
"type": str(self.type.value),
124+
"identifiers": (
125+
self.identifiers.serialize()
126+
if isinstance(self.identifiers, IssueIdentifiers)
127+
else self.identifiers
128+
),
129+
"reason": self.reason,
130+
}
131+
)
151132

152133
@classmethod
153134
def deserialize(
@@ -167,21 +148,68 @@ def deserialize(
167148
168149
Raises
169150
------
170-
TypeError
171-
If the reason is not None or a string or if the identifiers are a string
151+
jsonschema.exceptions.ValidationError
152+
If the serialized data does not match the Issue JSONSchema.
172153
"""
173-
serialized_type = serialized_issue["type"]
174-
serialized_identifiers = serialized_issue["identifiers"]
175-
serialized_reason = serialized_issue.get("reason")
176-
if serialized_reason is not None and not isinstance(serialized_reason, str):
177-
raise TypeError
178-
if isinstance(serialized_identifiers, str):
179-
raise TypeError
180-
154+
_verify_issue_schema(serialized_issue)
181155
return Issue(
182-
IssueType(serialized_type),
183-
IssueIdentifiers.deserialize(serialized_identifiers)
184-
if not isinstance(serialized_identifiers, list)
185-
else serialized_identifiers,
186-
serialized_reason,
156+
type=IssueType(serialized_issue["type"]),
157+
identifiers=IssueIdentifiers.deserialize(serialized_issue["identifiers"])
158+
if not isinstance(serialized_issue["identifiers"], list)
159+
else serialized_issue["identifiers"],
160+
reason=serialized_issue.get("reason"),
187161
)
162+
163+
164+
def _clean_dict(d: dict) -> dict:
165+
"""Remove all fields in a dict that are None or 'None'."""
166+
return {k: v for k, v in d.items() if str(v) != "None"}
167+
168+
169+
ISSUES_SCHEMA = {
170+
"type": "array",
171+
"definitions": {
172+
"issue": {
173+
"type": "object",
174+
"properties": {
175+
"type": {"enum": IssueType.names()},
176+
"identifiers": {
177+
"anyOf": [
178+
{
179+
"type": "object",
180+
"properties": {
181+
"annotation": {
182+
"type": "string",
183+
"pattern": "^(-?[0-9]+|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$", # noqa: E501
184+
},
185+
"annotation_type": {
186+
"enum": ["Bbox", "Cuboid", "Num", "Poly2d", "Poly3d", "Seg3d"]
187+
},
188+
"attribute": {"type": "string"},
189+
"frame": {"type": "integer"},
190+
"object": {
191+
"type": "string",
192+
"pattern": "^(-?[0-9]+|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$", # noqa: E501
193+
},
194+
"object_type": {"type": "string"},
195+
"sensor": {"type": "string"},
196+
},
197+
},
198+
{"type": "array", "items": {"type": ["string", "integer"]}},
199+
]
200+
},
201+
"reason": {"type": "string"},
202+
},
203+
"required": ["type", "identifiers"],
204+
},
205+
},
206+
"items": {"$ref": "#/definitions/issue"},
207+
}
208+
209+
210+
def _verify_issue_schema(d: dict) -> None:
211+
jsonschema.validate(d, ISSUES_SCHEMA["definitions"]["issue"])
212+
213+
214+
def _verify_identifiers_schema(d: dict) -> None:
215+
jsonschema.validate(d, ISSUES_SCHEMA["definitions"]["issue"]["properties"]["identifiers"])
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Copyright DB InfraGO AG and contributors
2+
# SPDX-License-Identifier: Apache-2.0

raillabel_providerkit/validation/issues_schema.json

Lines changed: 0 additions & 56 deletions
This file was deleted.

raillabel_providerkit/validation/issues_schema.json.license

Lines changed: 0 additions & 2 deletions
This file was deleted.

raillabel_providerkit/validation/validate_dimensions/validate_dimensions.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ def _get_annotations_with_identifiers(scene: Scene) -> list[tuple[Cuboid, IssueI
4646
annotation,
4747
IssueIdentifiers(
4848
annotation=annotation_id,
49+
annotation_type=annotation.__class__.__name__,
4950
frame=frame_id,
5051
object=annotation.object_id,
5152
object_type=scene.objects[annotation.object_id].type,

raillabel_providerkit/validation/validate_ontology/_ontology_classes/_annotation_with_metadata.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ def sensor_type(self) -> _SensorType | None:
5050
def to_identifiers(self, attribute: str | None = None) -> IssueIdentifiers:
5151
return IssueIdentifiers(
5252
annotation=self.annotation_id,
53+
annotation_type=self.annotation.__class__.__name__,
5354
frame=self.frame_id,
5455
sensor=self.annotation.sensor_id,
5556
object=self.annotation.object_id,

0 commit comments

Comments
 (0)