Skip to content

Commit aab6ac3

Browse files
committed
Add vector-pixel validation
1 parent 516c9b1 commit aab6ac3

21 files changed

+7411
-114
lines changed

src/superannotate/lib/app/interface/sdk_interface.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
from lib.app.helpers import get_annotation_paths
3333
from lib.app.helpers import reformat_metrics_json
3434
from lib.app.interface.types import AnnotationType
35-
from lib.app.interface.types import ClassesJson
3635
from lib.app.interface.types import NotEmptyStr
3736
from lib.app.interface.types import Status
3837
from lib.app.interface.types import validate_arguments
@@ -45,9 +44,11 @@
4544
from lib.core.enums import ImageQuality
4645
from lib.core.exceptions import AppException
4746
from lib.core.exceptions import AppValidationException
47+
from lib.core.types import ClassesJson
4848
from lib.infrastructure.controller import Controller
4949
from plotly.subplots import make_subplots
5050
from pydantic import EmailStr
51+
from pydantic import parse_obj_as
5152
from pydantic import StrictBool
5253
from tqdm import tqdm
5354

@@ -1964,12 +1965,18 @@ def create_annotation_classes_from_classes_json(
19641965
from_s3_object = from_s3.Object(from_s3_bucket, classes_json)
19651966
from_s3_object.download_fileobj(file)
19661967
file.seek(0)
1967-
annotation_classes = json.load(file)
1968+
annotation_classes = parse_obj_as(List[ClassesJson], json.load(file))
19681969
else:
1969-
annotation_classes = json.load(open(classes_json))
1970+
annotation_classes = parse_obj_as(
1971+
List[ClassesJson], json.load(open(classes_json))
1972+
)
1973+
19701974
else:
19711975
annotation_classes = classes_json
19721976

1977+
annotation_classes = [
1978+
annotation_class.dict() for annotation_class in annotation_classes
1979+
]
19731980
response = controller.create_annotation_classes(
19741981
project_name=project, annotation_classes=annotation_classes,
19751982
)

src/superannotate/lib/app/interface/types.py

Lines changed: 0 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
11
from functools import wraps
2-
from typing import List
3-
from typing import Optional
42
from typing import Union
53

64
from lib.core.enums import AnnotationStatus
7-
from pydantic import BaseModel
85
from pydantic import constr
96
from pydantic import StrictStr
107
from pydantic import validate_arguments as pydantic_validate_arguments
118
from pydantic import ValidationError
129

13-
1410
NotEmptyStr = constr(strict=True, min_length=1)
1511

1612

@@ -36,100 +32,6 @@ def validate(cls, value: Union[str]) -> Union[str]:
3632
return value
3733

3834

39-
class Attribute(BaseModel):
40-
id: int
41-
group_id: int
42-
name: NotEmptyStr
43-
44-
45-
class AttributeGroup(BaseModel):
46-
name: StrictStr
47-
is_multiselect: Optional[bool]
48-
attributes: List[Attribute]
49-
50-
51-
class ClassesJson(BaseModel):
52-
name: StrictStr
53-
color: StrictStr
54-
attribute_groups: List[AttributeGroup]
55-
56-
57-
class Metadata(BaseModel):
58-
width: int
59-
height: int
60-
61-
62-
class BaseInstance(BaseModel):
63-
metadata: Metadata
64-
type: NotEmptyStr
65-
classId: int
66-
groupId: int
67-
attributes: List[Attribute]
68-
69-
70-
class Point(BaseInstance):
71-
x: float
72-
y: float
73-
74-
75-
class PolyLine(BaseInstance):
76-
points: List[float]
77-
78-
79-
class Polygon(BaseInstance):
80-
points: List[float]
81-
82-
83-
class BboxPoints(BaseModel):
84-
x1: float
85-
x2: float
86-
y1: float
87-
y2: float
88-
89-
90-
class Bbox(BaseInstance):
91-
points: BboxPoints
92-
93-
94-
class Ellipse(BaseInstance):
95-
cx: float
96-
cy: float
97-
rx: float
98-
ry: float
99-
100-
101-
class TemplatePoint(BaseModel):
102-
id: int
103-
x: float
104-
y: float
105-
106-
107-
class TemplateConnection(BaseModel):
108-
id: int
109-
to: int
110-
111-
112-
class Template(BaseInstance):
113-
points: List[TemplatePoint]
114-
connections: List[TemplateConnection]
115-
templateId: int
116-
117-
118-
class CuboidPoint(BaseModel):
119-
f1: Point
120-
f2: Point
121-
r1: Point
122-
r2: Point
123-
124-
125-
class Cuboid(BaseInstance):
126-
points: List[CuboidPoint]
127-
128-
129-
class VectorAnnotation(BaseModel):
130-
metadata: Metadata
131-
instances: Optional[List[Union[Template, Cuboid, Point, PolyLine, Polygon, Bbox, Ellipse]]]
132-
13335
def validate_arguments(func):
13436
@wraps(func)
13537
def wrapped(*args, **kwargs):
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
from typing import List
2+
from typing import Optional
3+
from typing import Union
4+
5+
from pydantic import BaseModel
6+
from pydantic import constr
7+
from pydantic import StrictStr
8+
9+
10+
NotEmptyStr = constr(strict=True, min_length=1)
11+
12+
13+
class Attribute(BaseModel):
14+
name: NotEmptyStr
15+
16+
17+
class AttributeGroup(BaseModel):
18+
name: StrictStr
19+
attributes: List[Attribute]
20+
21+
22+
class ClassesJson(BaseModel):
23+
name: StrictStr
24+
color: StrictStr
25+
attribute_groups: List[AttributeGroup]
26+
27+
28+
class Metadata(BaseModel):
29+
name: Optional[NotEmptyStr]
30+
31+
32+
class BaseInstance(BaseModel):
33+
type: NotEmptyStr
34+
classId: int
35+
groupId: int
36+
attributes: List[Attribute]
37+
38+
39+
class Point(BaseInstance):
40+
x: float
41+
y: float
42+
43+
44+
class PolyLine(BaseInstance):
45+
points: List[float]
46+
47+
48+
class Polygon(BaseInstance):
49+
points: List[float]
50+
51+
52+
class BboxPoints(BaseModel):
53+
x1: float
54+
x2: float
55+
y1: float
56+
y2: float
57+
58+
59+
class Bbox(BaseInstance):
60+
points: BboxPoints
61+
62+
63+
class Ellipse(BaseInstance):
64+
cx: float
65+
cy: float
66+
rx: float
67+
ry: float
68+
69+
70+
class TemplatePoint(BaseModel):
71+
id: int
72+
x: float
73+
y: float
74+
75+
76+
class TemplateConnection(BaseModel):
77+
id: int
78+
to: int
79+
80+
81+
class Template(BaseInstance):
82+
points: List[TemplatePoint]
83+
connections: List[Optional[TemplateConnection]]
84+
templateId: int
85+
86+
87+
class EmptyPoint(BaseModel):
88+
x: float
89+
y: float
90+
91+
92+
class CuboidPoint(BaseModel):
93+
f1: EmptyPoint
94+
f2: EmptyPoint
95+
r1: EmptyPoint
96+
r2: EmptyPoint
97+
98+
99+
class Cuboid(BaseInstance):
100+
points: CuboidPoint
101+
102+
103+
class VectorAnnotation(BaseModel):
104+
metadata: Metadata
105+
instances: List[Union[Template, Cuboid, Point, PolyLine, Polygon, Bbox, Ellipse]]
106+
107+
108+
class PixelAnnotationPart(BaseModel):
109+
color: NotEmptyStr
110+
111+
112+
class PixelAnnotationInstance(BaseModel):
113+
classId: int
114+
groupId: int
115+
parts: List[PixelAnnotationPart]
116+
attributes: List[Attribute]
117+
118+
119+
class PixelAnnotation(BaseModel):
120+
metadata: Metadata
121+
instances: List[PixelAnnotationInstance]

src/superannotate/lib/core/usecases.py

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,10 @@
5151
from lib.core.repositories import BaseReadOnlyRepository
5252
from lib.core.response import Response
5353
from lib.core.serviceproviders import SuerannotateServiceProvider
54+
from lib.core.types import PixelAnnotation
55+
from lib.core.types import VectorAnnotation
5456
from PIL import UnidentifiedImageError
57+
from pydantic import ValidationError
5558

5659
logger = logging.getLogger("root")
5760

@@ -3472,6 +3475,16 @@ def annotations_to_upload(self):
34723475
self._annotations_to_upload = annotations_to_upload
34733476
return self._annotations_to_upload
34743477

3478+
def _is_valid_json(self, json_data: dict):
3479+
try:
3480+
if self._project.project_type == constances.ProjectType.PIXEL.value:
3481+
PixelAnnotation(**json_data)
3482+
else:
3483+
VectorAnnotation(**json_data)
3484+
return True
3485+
except ValidationError as _:
3486+
return False
3487+
34753488
def execute(self):
34763489
uploaded_annotations = []
34773490
missing_annotations = []
@@ -3527,22 +3540,20 @@ def execute(self):
35273540
for image_id, image_info in auth_data["images"].items()
35283541
]
35293542
for future in concurrent.futures.as_completed(results):
3530-
future.result()
3543+
annotation, uploaded = future.result()
3544+
if uploaded:
3545+
uploaded_annotations.append(annotation)
3546+
else:
3547+
failed_annotations.append(annotation)
35313548
yield
35323549

3533-
uploaded_annotations.extend(
3534-
[annotation.path for annotation in self.annotations_to_upload]
3535-
)
3550+
uploaded_annotations = [
3551+
annotation.path for annotation in uploaded_annotations
3552+
]
35363553
missing_annotations.extend(
35373554
[annotation.path for annotation in self._missing_annotations]
35383555
)
3539-
failed_annotations.extend(
3540-
[
3541-
annotation
3542-
for annotation in self._annotation_paths
3543-
if annotation not in uploaded_annotations + missing_annotations
3544-
]
3545-
)
3556+
failed_annotations = [annotation.path for annotation in failed_annotations]
35463557
self._response.data = (
35473558
uploaded_annotations,
35483559
failed_annotations,
@@ -3565,6 +3576,9 @@ def upload_to_s3(
35653576
annotation_json = json.load(open(image_id_name_map[image_id].path))
35663577

35673578
self.fill_classes_data(annotation_json)
3579+
3580+
if not self._is_valid_json(annotation_json):
3581+
return image_id_name_map[image_id], False
35683582
bucket.put_object(
35693583
Key=image_info["annotation_json_path"], Body=json.dumps(annotation_json),
35703584
)
@@ -3584,6 +3598,7 @@ def upload_to_s3(
35843598
file = io.BytesIO(mask_file.read())
35853599

35863600
bucket.put_object(Key=image_info["annotation_bluemap_path"], Body=file)
3601+
return image_id_name_map[image_id], True
35873602

35883603

35893604
class CreateModelUseCase(BaseUseCase):

0 commit comments

Comments
 (0)