Skip to content

Commit f1962a8

Browse files
authored
Merge pull request #301 from superannotateai/validations
Validations
2 parents 2dcee0b + a8ac294 commit f1962a8

34 files changed

+1371
-182
lines changed

src/superannotate/lib/app/annotation_helpers.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,23 @@
44
from superannotate.lib.app.exceptions import AppException
55

66

7-
def fill_in_missing(annotation_json):
7+
def fill_in_missing(annotation_json, image_name: str = ""):
88
for field in ["instances", "comments", "tags"]:
99
if field not in annotation_json:
1010
annotation_json[field] = []
1111
if "metadata" not in annotation_json:
12-
annotation_json["metadata"] = {}
12+
annotation_json["metadata"] = {"name": image_name}
1313

1414

15-
def _preprocess_annotation_json(annotation_json):
15+
def _preprocess_annotation_json(annotation_json, image_name: str = ""):
1616
path = None
1717
if not isinstance(annotation_json, dict) and annotation_json is not None:
1818
path = annotation_json
1919
annotation_json = json.load(open(annotation_json))
2020
elif annotation_json is None:
2121
annotation_json = {}
2222

23-
fill_in_missing(annotation_json)
23+
fill_in_missing(annotation_json, image_name)
2424

2525
return (annotation_json, path)
2626

@@ -60,7 +60,7 @@ def add_annotation_comment_to_json(
6060
"type": "comment",
6161
"x": comment_coords[0],
6262
"y": comment_coords[1],
63-
"correspondence": [{"text": comment_text, "id": comment_author}],
63+
"correspondence": [{"text": comment_text, "email": comment_author}],
6464
"resolved": resolved,
6565
}
6666
annotation_json["comments"].append(annotation)
@@ -74,6 +74,7 @@ def add_annotation_bbox_to_json(
7474
annotation_class_name,
7575
annotation_class_attributes=None,
7676
error=None,
77+
image_name: str = "",
7778
):
7879
"""Add a bounding box annotation to SuperAnnotate format annotation JSON
7980
@@ -94,7 +95,7 @@ def add_annotation_bbox_to_json(
9495
if len(bbox) != 4:
9596
raise AppException("Bounding boxes should have 4 float elements")
9697

97-
annotation_json, path = _preprocess_annotation_json(annotation_json)
98+
annotation_json, path = _preprocess_annotation_json(annotation_json, image_name)
9899

99100
annotation = {
100101
"type": "bbox",

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

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2477,6 +2477,7 @@ def upload_annotations_from_folder_to_project(
24772477
folder_name=folder_name,
24782478
annotation_paths=annotation_paths, # noqa: E203
24792479
client_s3_bucket=from_s3_bucket,
2480+
folder_path=folder_path,
24802481
)
24812482
if response.errors:
24822483
raise AppException(response.errors)
@@ -3070,7 +3071,12 @@ def add_annotation_bbox_to_image(
30703071
"""
30713072
annotations = get_image_annotations(project, image_name)["annotation_json"]
30723073
annotations = add_annotation_bbox_to_json(
3073-
annotations, bbox, annotation_class_name, annotation_class_attributes, error,
3074+
annotations,
3075+
bbox,
3076+
annotation_class_name,
3077+
annotation_class_attributes,
3078+
error,
3079+
image_name,
30743080
)
30753081
upload_image_annotations(project, image_name, annotations, verbose=False)
30763082

@@ -3639,12 +3645,23 @@ def attach_document_urls_to_project(
36393645
def validate_annotations(
36403646
project_type: ProjectTypes, annotations_json: Union[NotEmptyStr, Path]
36413647
):
3648+
"""
3649+
Validates given annotation JSON.
3650+
:param project_type: project_type (str) – the project type Vector, Pixel, Video or Document
3651+
:type project_type: str
3652+
:param annotations_json: path to annotation JSON
3653+
:type annotations_json: Path-like (str or Path)
3654+
3655+
:return: The success of the validation
3656+
:rtype: bool
3657+
"""
36423658
with open(annotations_json) as file:
36433659
annotation_data = json.loads(file.read())
3644-
response = controller.validate_annotations(project_type, annotation_data)
3660+
response = controller.validate_annotations(project_type, annotation_data, allow_extra=False)
36453661
if response.errors:
36463662
raise AppException(response.errors)
3647-
if response.data:
3663+
is_valid, _ = response.data
3664+
if is_valid:
36483665
return True
36493666
print(response.report)
36503667
return False
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from lib.core.entities.document import DocumentAnnotation
2+
from lib.core.entities.pixel import PixelAnnotation
3+
from lib.core.entities.project_entities import AnnotationClassEntity
4+
from lib.core.entities.project_entities import BaseEntity
5+
from lib.core.entities.project_entities import ConfigEntity
6+
from lib.core.entities.project_entities import FolderEntity
7+
from lib.core.entities.project_entities import ImageEntity
8+
from lib.core.entities.project_entities import ImageInfoEntity
9+
from lib.core.entities.project_entities import MLModelEntity
10+
from lib.core.entities.project_entities import ProjectEntity
11+
from lib.core.entities.project_entities import ProjectSettingEntity
12+
from lib.core.entities.project_entities import S3FileEntity
13+
from lib.core.entities.project_entities import TeamEntity
14+
from lib.core.entities.project_entities import UserEntity
15+
from lib.core.entities.project_entities import WorkflowEntity
16+
from lib.core.entities.vector import VectorAnnotation
17+
from lib.core.entities.video import VideoAnnotation
18+
from lib.core.entities.video_export import VideoAnnotation as VideoExportAnnotation
19+
20+
21+
__all__ = [
22+
"BaseEntity",
23+
"ProjectEntity",
24+
"ProjectSettingEntity",
25+
"ConfigEntity",
26+
"WorkflowEntity",
27+
"FolderEntity",
28+
"ImageEntity",
29+
"ImageInfoEntity",
30+
"S3FileEntity",
31+
"AnnotationClassEntity",
32+
"UserEntity",
33+
"TeamEntity",
34+
"MLModelEntity",
35+
# annotations
36+
"DocumentAnnotation",
37+
"VideoAnnotation",
38+
"VectorAnnotation",
39+
"PixelAnnotation",
40+
"VideoExportAnnotation",
41+
]
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from typing import List
2+
from typing import Optional
3+
4+
from lib.core.entities.utils import Attribute
5+
from lib.core.entities.utils import BaseInstance
6+
from lib.core.entities.utils import BaseModel
7+
from lib.core.entities.utils import MetadataBase
8+
from lib.core.entities.utils import Tag
9+
from pydantic import Field
10+
11+
12+
class DocumentInstance(BaseInstance):
13+
start: int
14+
end: int
15+
attributes: Optional[List[Attribute]] = Field(list())
16+
17+
18+
class DocumentAnnotation(BaseModel):
19+
metadata: MetadataBase
20+
instances: Optional[List[DocumentInstance]] = Field(list())
21+
tags: Optional[List[Tag]] = Field(list())
22+
free_text: Optional[str] = Field(None, alias="freeText")
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from typing import List
2+
from typing import Optional
3+
4+
from lib.core.entities.utils import BaseImageInstance
5+
from lib.core.entities.utils import BaseModel
6+
from lib.core.entities.utils import Metadata
7+
from lib.core.entities.utils import Tag
8+
from pydantic import Field
9+
from pydantic import validator
10+
from pydantic.color import Color
11+
from pydantic.color import ColorType
12+
13+
14+
class PixelMetaData(Metadata):
15+
is_segmented: Optional[bool] = Field(None, alias="isSegmented")
16+
17+
18+
class PixelAnnotationPart(BaseModel):
19+
color: ColorType
20+
21+
@validator("color")
22+
def validate_color(cls, v):
23+
color = Color(v)
24+
return color.as_hex()
25+
26+
27+
class PixelAnnotationInstance(BaseImageInstance):
28+
parts: List[PixelAnnotationPart]
29+
30+
31+
class PixelAnnotation(BaseModel):
32+
metadata: PixelMetaData
33+
instances: List[PixelAnnotationInstance]
34+
tags: Optional[List[Tag]] = Field(list())
File renamed without changes.
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
from enum import Enum
2+
from typing import Dict
3+
from typing import List
4+
from typing import Optional
5+
6+
from pydantic import BaseModel as PyDanticBaseModel
7+
from pydantic import conlist
8+
from pydantic import constr
9+
from pydantic import EmailStr
10+
from pydantic import Field
11+
from pydantic import Extra
12+
from pydantic import validator
13+
from pydantic.errors import EnumMemberError
14+
15+
16+
def enum_error_handling(self) -> str:
17+
permitted = ", ".join(repr(v.value) for v in self.enum_values)
18+
return f"Invalid value, permitted: {permitted}"
19+
20+
21+
EnumMemberError.__str__ = enum_error_handling
22+
23+
24+
NotEmptyStr = constr(strict=True, min_length=1)
25+
26+
27+
DATE_REGEX = r"\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d(?:\.\d{3})Z"
28+
29+
30+
class BaseModel(PyDanticBaseModel):
31+
32+
class Config:
33+
extra = Extra.allow
34+
use_enum_values = True
35+
error_msg_templates = {
36+
"type_error.integer": "integer type expected",
37+
"type_error.string": "str type expected",
38+
"value_error.missing": "field required",
39+
}
40+
41+
42+
class VectorAnnotationTypeEnum(str, Enum):
43+
BBOX = "bbox"
44+
ELLIPSE = "ellipse"
45+
TEMPLATE = "template"
46+
CUBOID = "cuboid"
47+
POLYLINE = "polyline"
48+
POLYGON = "polygon"
49+
POINT = "point"
50+
RBBOX = "rbbox"
51+
52+
53+
class CreationTypeEnum(str, Enum):
54+
MANUAL = "Manual"
55+
PREDICTION = "Prediction"
56+
PRE_ANNOTATION = "Preannotation"
57+
58+
59+
class AnnotationStatusEnum(str, Enum):
60+
NOT_STARTED = "NotStarted"
61+
IN_PROGRESS = "InProgress"
62+
QUALITY_CHECK = "QualityCheck"
63+
RETURNED = "Returned"
64+
COMPLETED = "Completed"
65+
SKIPPED = "Skipped"
66+
67+
68+
class BaseRoleEnum(str, Enum):
69+
ADMIN = "Admin"
70+
ANNOTATOR = "Annotator"
71+
QA = "QA"
72+
73+
74+
class BaseImageRoleEnum(str, Enum):
75+
CUSTOMER = "Customer"
76+
ADMIN = "Admin"
77+
ANNOTATOR = "Annotator"
78+
QA = "QA"
79+
80+
81+
class Attribute(BaseModel):
82+
id: Optional[int]
83+
group_id: Optional[int] = Field(None, alias="groupId")
84+
name: NotEmptyStr
85+
group_name: NotEmptyStr = Field(None, alias="groupName")
86+
87+
88+
class Tag(BaseModel):
89+
__root__: str
90+
91+
92+
class AttributeGroup(BaseModel):
93+
name: NotEmptyStr
94+
is_multiselect: Optional[int] = False
95+
attributes: List[Attribute]
96+
97+
98+
class BboxPoints(BaseModel):
99+
x1: float
100+
x2: float
101+
y1: float
102+
y2: float
103+
104+
105+
class TimedBaseModel(BaseModel):
106+
created_at: constr(regex=DATE_REGEX) = Field(None, alias="createdAt")
107+
updated_at: constr(regex=DATE_REGEX) = Field(None, alias="updatedAt")
108+
109+
110+
class UserAction(BaseModel):
111+
email: EmailStr
112+
role: BaseImageRoleEnum
113+
114+
115+
class TrackableModel(BaseModel):
116+
created_by: Optional[UserAction] = Field(None, alias="createdBy")
117+
updated_by: Optional[UserAction] = Field(None, alias="updatedBy")
118+
creation_type: Optional[CreationTypeEnum] = Field(
119+
CreationTypeEnum.PRE_ANNOTATION.value, alias="creationType"
120+
)
121+
122+
@validator("creation_type", always=True)
123+
def clean_creation_type(cls, _):
124+
return CreationTypeEnum.PRE_ANNOTATION.value
125+
126+
127+
class LastUserAction(BaseModel):
128+
email: EmailStr
129+
timestamp: float
130+
131+
132+
class BaseInstance(TrackableModel, TimedBaseModel):
133+
class_id: Optional[str] = Field(None, alias="classId")
134+
class_name: Optional[str] = Field(None, alias="className")
135+
136+
137+
class MetadataBase(BaseModel):
138+
name: NotEmptyStr
139+
last_action: Optional[LastUserAction] = Field(None, alias="lastAction")
140+
width: Optional[int]
141+
height: Optional[int]
142+
project_id: Optional[int] = Field(None, alias="projectId")
143+
annotator_email: Optional[EmailStr] = Field(None, alias="annotatorEmail")
144+
qa_email: Optional[EmailStr] = Field(None, alias="qaEmail")
145+
status: Optional[AnnotationStatusEnum]
146+
147+
148+
class PointLabels(BaseModel):
149+
__root__: Dict[constr(regex=r"^[0-9]*$"), str] # noqa: F722 E261
150+
151+
152+
class Correspondence(BaseModel):
153+
text: NotEmptyStr
154+
email: EmailStr
155+
156+
157+
class Comment(TimedBaseModel, TrackableModel):
158+
x: float
159+
y: float
160+
resolved: Optional[bool] = Field(False)
161+
correspondence: conlist(Correspondence, min_items=1)
162+
163+
164+
class BaseImageInstance(BaseInstance):
165+
class_id: Optional[int] = Field(None, alias="classId")
166+
class_name: str = Field(alias="className")
167+
visible: Optional[bool]
168+
locked: Optional[bool]
169+
probability: Optional[int] = Field(100)
170+
attributes: Optional[List[Attribute]] = Field(list())
171+
error: Optional[bool]
172+
173+
class Config:
174+
error_msg_templates = {
175+
"value_error.missing": "field required for annotation",
176+
}
177+
178+
179+
class BaseVectorInstance(BaseImageInstance):
180+
type: VectorAnnotationTypeEnum
181+
point_labels: Optional[PointLabels] = Field(None, alias="pointLabels")
182+
tracking_id: Optional[str] = Field(None, alias="trackingId")
183+
group_id: Optional[int] = Field(None, alias="groupId")
184+
185+
186+
class Metadata(MetadataBase):
187+
pinned: Optional[bool]
188+
is_predicted: Optional[bool] = Field(None, alias="isPredicted")

0 commit comments

Comments
 (0)