Skip to content

Commit b65ca74

Browse files
authored
Merge pull request #262 from superannotateai/friday
Friday
2 parents 0dde146 + 7f664a8 commit b65ca74

File tree

10 files changed

+430
-64
lines changed

10 files changed

+430
-64
lines changed

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

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2447,22 +2447,23 @@ def upload_annotations_from_folder_to_project(
24472447
project_name, folder_name = extract_project_folder(project)
24482448
project_folder_name = project_name + (f"/{folder_name}" if folder_name else "")
24492449

2450-
24512450
if recursive_subfolders:
24522451
logger.info(
24532452
"When using recursive subfolder parsing same name annotations in different "
24542453
"subfolders will overwrite each other.",
24552454
)
2456-
logger.info("The JSON files should follow a specific naming convention, matching file names already present "
2457-
"on the platform. Existing annotations will be overwritten")
2455+
logger.info(
2456+
"The JSON files should follow a specific naming convention, matching file names already present "
2457+
"on the platform. Existing annotations will be overwritten"
2458+
)
24582459

24592460
annotation_paths = get_annotation_paths(
24602461
folder_path, from_s3_bucket, recursive_subfolders
24612462
)
2462-
if not annotation_paths:
2463-
raise AppException("Could not find annotations matching existing items on the platform.")
24642463

2465-
logger.info(f"Uploading {len(annotation_paths)} annotations from {folder_path} to the project {project_folder_name}.")
2464+
logger.info(
2465+
f"Uploading {len(annotation_paths)} annotations from {folder_path} to the project {project_folder_name}."
2466+
)
24662467
response = controller.upload_annotations_from_folder(
24672468
project_name=project_name,
24682469
folder_name=folder_name,
@@ -2518,16 +2519,17 @@ def upload_preannotations_from_folder_to_project(
25182519
"When using recursive subfolder parsing same name annotations in different "
25192520
"subfolders will overwrite each other.",
25202521
)
2521-
logger.info("The JSON files should follow a specific naming convention, matching file names already present "
2522-
"on the platform. Existing annotations will be overwritten")
2522+
logger.info(
2523+
"The JSON files should follow a specific naming convention, matching file names already present "
2524+
"on the platform. Existing annotations will be overwritten"
2525+
)
25232526
logger.info("Existing annotations will be overwritten.",)
25242527
annotation_paths = get_annotation_paths(
25252528
folder_path, from_s3_bucket, recursive_subfolders
25262529
)
2527-
if not annotation_paths:
2528-
raise AppException("Could not find annotations matching existing items on the platform.")
25292530
logger.info(
2530-
f"Uploading {len(annotation_paths)} annotations from {folder_path} to the project {project_folder_name}.")
2531+
f"Uploading {len(annotation_paths)} annotations from {folder_path} to the project {project_folder_name}."
2532+
)
25312533
response = controller.upload_annotations_from_folder(
25322534
project_name=project_name,
25332535
folder_name=folder_name,

src/superannotate/lib/core/helpers.py

Lines changed: 56 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
def map_annotation_classes_name(annotation_classes, reporter: Reporter) -> dict:
99
classes_data = defaultdict(dict)
1010
for annotation_class in annotation_classes:
11-
class_info = {"id": annotation_class.uuid}
11+
class_info = {"id": annotation_class.uuid, "attribute_groups": {}}
1212
if annotation_class.attribute_groups:
1313
for attribute_group in annotation_class.attribute_groups:
1414
attribute_group_data = defaultdict(dict)
@@ -27,12 +27,11 @@ def map_annotation_classes_name(annotation_classes, reporter: Reporter) -> dict:
2727
" Only one of the annotation class attribute groups will be used."
2828
" This will result in errors in annotation upload."
2929
)
30-
class_info["attribute_groups"] = {
31-
attribute_group["name"]: {
32-
"id": attribute_group["id"],
33-
"attributes": attribute_group_data,
34-
}
30+
class_info["attribute_groups"][attribute_group["name"]] = {
31+
"id": attribute_group["id"],
32+
"attributes": attribute_group_data,
3533
}
34+
3635
if annotation_class.name in classes_data.keys():
3736
reporter.log_warning(
3837
f"Duplicate annotation class name {annotation_class.name}."
@@ -43,6 +42,16 @@ def map_annotation_classes_name(annotation_classes, reporter: Reporter) -> dict:
4342
return classes_data
4443

4544

45+
def fill_document_tags(
46+
annotations: dict, annotation_classes: dict,
47+
):
48+
new_tags = []
49+
for tag in annotations["tags"]:
50+
if annotation_classes.get(tag):
51+
new_tags.append(annotation_classes[tag]["id"])
52+
annotations["tags"] = new_tags
53+
54+
4655
def fill_annotation_ids(
4756
annotations: dict,
4857
annotation_classes_name_maps: dict,
@@ -95,7 +104,10 @@ def fill_annotation_ids(
95104
reporter.log_warning(
96105
f"Couldn't find annotation group {attribute['groupName']}."
97106
)
98-
reporter.store_message("missing_attribute_groups", f"{annotation['className']}.{attribute['groupName']}")
107+
reporter.store_message(
108+
"missing_attribute_groups",
109+
f"{annotation['className']}.{attribute['groupName']}",
110+
)
99111
continue
100112
attribute["groupId"] = annotation_classes_name_maps[annotation_class_name][
101113
"attribute_groups"
@@ -118,14 +130,16 @@ def fill_annotation_ids(
118130
][attribute["groupName"]]["attributes"][attribute["name"]]
119131

120132

121-
def convert_to_video_editor_json(data: dict, class_name_mapper: dict, reporter: Reporter):
133+
def convert_to_video_editor_json(
134+
data: dict, class_name_mapper: dict, reporter: Reporter
135+
):
122136
id_generator = ClassIdGenerator()
123137

124138
def safe_time(timestamp):
125139
return "0" if str(timestamp) == "0.0" else timestamp
126140

127141
def convert_timestamp(timestamp):
128-
return timestamp / 10 ** 6
142+
return timestamp / 10 ** 6 if timestamp else "0"
129143

130144
editor_data = {
131145
"instances": [],
@@ -134,8 +148,8 @@ def convert_timestamp(timestamp):
134148
"metadata": {
135149
"duration": convert_timestamp(data["metadata"]["duration"]),
136150
"name": data["metadata"]["name"],
137-
"width": data["metadata"]["width"],
138-
"height": data["metadata"]["height"],
151+
"width": data["metadata"].get("width"),
152+
"height": data["metadata"].get("height"),
139153
},
140154
}
141155
for instance in data["instances"]:
@@ -145,10 +159,13 @@ def convert_timestamp(timestamp):
145159
"attributes": [],
146160
"timeline": {},
147161
"type": meta["type"],
148-
"locked": True,
162+
# TODO check
163+
"locked": False,
149164
}
150165
if class_name:
151-
editor_instance["classId"] = class_name_mapper.get(class_name, {}).get("id", id_generator.send(class_name))
166+
editor_instance["classId"] = class_name_mapper.get(class_name, {}).get(
167+
"id", id_generator.send(class_name)
168+
)
152169
else:
153170
editor_instance["classId"] = id_generator.send("unknown_class")
154171
if meta.get("pointLabels", None):
@@ -181,13 +198,33 @@ def convert_timestamp(timestamp):
181198

182199
existing_attributes_in_current_instance = set()
183200
for attribute in timestamp_data["attributes"]:
184-
group_name, attr_name = attribute.get("groupName"), attribute.get("name")
185-
if not class_name_mapper[class_name].get("attribute_groups", {}).get(group_name):
186-
reporter.store_message("missing_attribute_groups", f"{class_name}.{group_name}")
187-
elif not class_name_mapper[class_name]["attribute_groups"][group_name].get("attributes", {}).get(attr_name):
188-
reporter.store_message("missing_attributes", f"{class_name}.{group_name}.{attr_name}")
201+
group_name, attr_name = (
202+
attribute.get("groupName"),
203+
attribute.get("name"),
204+
)
205+
if (
206+
not class_name_mapper[class_name]
207+
.get("attribute_groups", {})
208+
.get(group_name)
209+
):
210+
reporter.store_message(
211+
"missing_attribute_groups", f"{class_name}.{group_name}"
212+
)
213+
elif (
214+
not class_name_mapper[class_name]["attribute_groups"][
215+
group_name
216+
]
217+
.get("attributes", {})
218+
.get(attr_name)
219+
):
220+
reporter.store_message(
221+
"missing_attributes",
222+
f"{class_name}.{group_name}.{attr_name}",
223+
)
189224
else:
190-
existing_attributes_in_current_instance.add((group_name, attr_name))
225+
existing_attributes_in_current_instance.add(
226+
(group_name, attr_name)
227+
)
191228
attributes_to_add = (
192229
existing_attributes_in_current_instance - active_attributes
193230
)

src/superannotate/lib/core/types.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class Metadata(BaseModel):
4848

4949

5050
class PointLabels(BaseModel):
51-
__root__: Dict[constr(regex=r'^[0-9]*$'), str]
51+
__root__: Dict[constr(regex=r"^[0-9]*$"), str]
5252

5353

5454
class BaseInstance(BaseModel):
@@ -227,10 +227,10 @@ class VideoInstance(BaseModel):
227227

228228
class VideoAnnotation(BaseModel):
229229
metadata: VideoMetaData
230-
instances: List[VideoInstance]
231-
tags: List[str]
230+
instances: Optional[List[VideoInstance]]
231+
tags: Optional[List[str]]
232232

233233

234234
class DocumentAnnotation(BaseModel):
235235
instances: list
236-
tags: List[str]
236+
tags: Optional[List[str]]

src/superannotate/lib/core/usecases/annotations.py

Lines changed: 48 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212
from lib.core.entities import FolderEntity
1313
from lib.core.entities import ImageEntity
1414
from lib.core.entities import ProjectEntity
15-
from lib.core.exceptions import AppException
1615
from lib.core.helpers import convert_to_video_editor_json
1716
from lib.core.helpers import fill_annotation_ids
17+
from lib.core.helpers import fill_document_tags
1818
from lib.core.helpers import map_annotation_classes_name
1919
from lib.core.reporter import Reporter
2020
from lib.core.service_types import UploadAnnotationAuthData
@@ -110,6 +110,7 @@ def annotations_to_upload(self):
110110
for idx, detail in enumerate(images_detail):
111111
if detail.name == image_data.name:
112112
images_detail[idx] = detail._replace(id=image_data.uuid)
113+
break
113114

114115
missing_annotations = list(
115116
filter(lambda image_detail: image_detail.id is None, images_detail)
@@ -118,12 +119,9 @@ def annotations_to_upload(self):
118119
filter(lambda image_detail: image_detail.id is not None, images_detail)
119120
)
120121
if missing_annotations:
121-
for missing in missing_annotations:
122-
logger.warning(
123-
f"Couldn't find image {missing.path} for annotation upload."
124-
)
125-
if not annotations_to_upload:
126-
raise AppException("No image to attach annotations.")
122+
logger.warning(
123+
f"Couldn't find {len(missing_annotations)}/{len(annotations_to_upload + missing_annotations)} items on the platform that match the annotations you want to upload."
124+
)
127125
self._missing_annotations = missing_annotations
128126
self._annotations_to_upload = annotations_to_upload
129127
return self._annotations_to_upload
@@ -196,9 +194,7 @@ def _log_report(self):
196194
template = "Could not find attribute groups matching existing attribute groups on the platform: [{}]"
197195
elif key == "missing_attributes":
198196
template = "Could not find attributes matching existing attributes on the platform: [{}]"
199-
logger.warning(
200-
template.format("', '".join(values))
201-
)
197+
logger.warning(template.format("', '".join(values)))
202198

203199
def execute(self):
204200
uploaded_annotations = []
@@ -207,21 +203,27 @@ def execute(self):
207203
iterations_range = range(
208204
0, len(self.annotations_to_upload), self.AUTH_DATA_CHUNK_SIZE
209205
)
210-
self.reporter.start_progress(iterations_range, description="Uploading Annotations")
211-
for _ in iterations_range:
206+
self.reporter.start_progress(
207+
len(self.annotations_to_upload), description="Uploading Annotations"
208+
)
209+
for step in iterations_range:
212210
annotations_to_upload = self.annotations_to_upload[
213-
_ : _ + self.AUTH_DATA_CHUNK_SIZE # noqa: E203
214-
]
211+
step : step + self.AUTH_DATA_CHUNK_SIZE
212+
] # noqa: E203
215213
upload_data = self.get_annotation_upload_data(
216214
[int(image.id) for image in annotations_to_upload]
217215
)
218-
bucket = self.get_bucket_to_upload([int(image.id) for image in annotations_to_upload])
216+
bucket = self.get_bucket_to_upload(
217+
[int(image.id) for image in annotations_to_upload]
218+
)
219219
if bucket:
220220
image_id_name_map = {
221221
image.id: image for image in self.annotations_to_upload
222222
}
223223
# dummy progress
224-
for _ in range(len(annotations_to_upload) - len(upload_data.images)):
224+
for _ in range(
225+
len(annotations_to_upload) - len(upload_data.images)
226+
):
225227
self.reporter.update_progress()
226228
with concurrent.futures.ThreadPoolExecutor(
227229
max_workers=self.MAX_WORKERS
@@ -251,8 +253,6 @@ def execute(self):
251253
[annotation.path for annotation in self._missing_annotations],
252254
)
253255
self._log_report()
254-
else:
255-
self._response.errors = "Could not find annotations matching existing items on the platform."
256256
return self._response
257257

258258

@@ -337,13 +337,26 @@ def from_s3(self):
337337
def set_annotation_json(self):
338338
if not self._annotation_json:
339339
if self._client_s3_bucket:
340-
self._annotation_json = json.load(self.get_s3_file(self.from_s3, self._annotation_path))
340+
self._annotation_json = json.load(
341+
self.get_s3_file(self.from_s3, self._annotation_path)
342+
)
341343
if self._project.project_type == constances.ProjectType.PIXEL.value:
342-
self._mask = self.get_s3_file(self.from_s3, self._annotation_path.replace(constances.PIXEL_ANNOTATION_POSTFIX, constances.ANNOTATION_MASK_POSTFIX))
344+
self._mask = self.get_s3_file(
345+
self.from_s3,
346+
self._annotation_path.replace(
347+
constances.PIXEL_ANNOTATION_POSTFIX,
348+
constances.ANNOTATION_MASK_POSTFIX,
349+
),
350+
)
343351
else:
344352
self._annotation_json = json.load(open(self._annotation_path))
345353
if self._project.project_type == constances.ProjectType.PIXEL.value:
346-
self._mask = open(self._annotation_path.replace(constances.PIXEL_ANNOTATION_POSTFIX, constances.ANNOTATION_MASK_POSTFIX))
354+
self._mask = open(
355+
self._annotation_path.replace(
356+
constances.PIXEL_ANNOTATION_POSTFIX,
357+
constances.ANNOTATION_MASK_POSTFIX,
358+
)
359+
)
347360

348361
def _is_valid_json(self, json_data: dict):
349362
use_case = ValidateAnnotationUseCase(
@@ -361,22 +374,28 @@ def prepare_annotations(
361374
templates: List[dict],
362375
reporter: Reporter,
363376
) -> dict:
377+
annotation_classes_name_maps = map_annotation_classes_name(
378+
annotation_classes, reporter
379+
)
364380
if project_type in (
365381
constances.ProjectType.VECTOR.value,
366382
constances.ProjectType.PIXEL.value,
367383
constances.ProjectType.DOCUMENT.value,
368384
):
369385
fill_annotation_ids(
370386
annotations=annotations,
371-
annotation_classes_name_maps=map_annotation_classes_name(
372-
annotation_classes, reporter
373-
),
387+
annotation_classes_name_maps=annotation_classes_name_maps,
374388
templates=templates,
375389
reporter=reporter,
376390
)
377391
elif project_type == constances.ProjectType.VIDEO.value:
378392
annotations = convert_to_video_editor_json(
379-
annotations, map_annotation_classes_name(annotation_classes, reporter), reporter
393+
annotations, annotation_classes_name_maps, reporter
394+
)
395+
if project_type == constances.ProjectType.DOCUMENT.value:
396+
fill_document_tags(
397+
annotations=annotations,
398+
annotation_classes=annotation_classes_name_maps,
380399
)
381400
return annotations
382401

@@ -408,7 +427,10 @@ def execute(self):
408427
],
409428
Body=json.dumps(annotation_json),
410429
)
411-
if self._project.project_type == constances.ProjectType.PIXEL.value and self._mask:
430+
if (
431+
self._project.project_type == constances.ProjectType.PIXEL.value
432+
and self._mask
433+
):
412434
bucket.put_object(
413435
Key=self.annotation_upload_data.images[self._image.uuid][
414436
"annotation_bluemap_path"

src/superannotate/lib/infrastructure/services.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import time
12
from contextlib import contextmanager
23
from datetime import datetime
34
from typing import Dict
@@ -653,6 +654,7 @@ def get_bulk_images(
653654
self, project_id: int, team_id: int, folder_id: int, images: List[str]
654655
) -> List[dict]:
655656
bulk_get_images_url = urljoin(self.api_url, self.URL_BULK_GET_IMAGES)
657+
time.sleep(1)
656658

657659
res = self._request(
658660
bulk_get_images_url,
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"metadata":{"name":"text_file_example_1","status":"Completed","url":"https://sa-public-files.s3.us-west-2.amazonaws.com/Text+project/text_file_example_1.txt","projectId":160158,"annotatorEmail":null,"qaEmail":null,"lastAction":{"email":"shab.prog@gmail.com","timestamp":1634899229953}},"instances":[{"start":253,"end":593,"classId":873208,"createdAt":"2021-10-22T10:40:26.151Z","createdBy":{"email":"shab.prog@gmail.com","role":"Admin"},"updatedAt":"2021-10-22T10:40:29.953Z","updatedBy":{"email":"shab.prog@gmail.com","role":"Admin"},"attributes":[],"creationType":"Manual","className":"vid"}],"tags":[],"freeText":""}
1+
{"metadata":{"name":"text_file_example_1","status":"Completed","url":"https://sa-public-files.s3.us-west-2.amazonaws.com/Text+project/text_file_example_1.txt","projectId":160158,"annotatorEmail":null,"qaEmail":null,"lastAction":{"email":"shab.prog@gmail.com","timestamp":1634899229953}},"instances":[{"start":253,"end":593,"classId":873208,"createdAt":"2021-10-22T10:40:26.151Z","createdBy":{"email":"shab.prog@gmail.com","role":"Admin"},"updatedAt":"2021-10-22T10:40:29.953Z","updatedBy":{"email":"shab.prog@gmail.com","role":"Admin"},"attributes":[],"creationType":"Manual","className":"vid"}],"tags":["vid"],"freeText":""}

0 commit comments

Comments
 (0)