Skip to content

Commit de18c97

Browse files
authored
Merge pull request #16 from superannotateai/develop
Attach Public URLs
2 parents b2d692c + 8d63a50 commit de18c97

File tree

15 files changed

+538
-105
lines changed

15 files changed

+538
-105
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ PYTESTS=pytest
99
COVERAGE=coverage
1010

1111
tests: check_formatting docs
12-
$(PYTESTS) -n auto --full-trace --verbose tests
12+
$(PYTESTS) -n auto --full-trace tests
1313

1414
stress-tests: SA_STRESS_TESTS=1
1515
stress-tests: tests

docs/source/cli.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,19 @@ to look for. If the argument is not given then value *jpg,jpeg,png,tif,tiff,webp
7979

8080
----------
8181

82+
.. _ref_attach_image_urls:
83+
84+
Attaching image URLs
85+
~~~~~~~~~~~~~~~~~~~~
86+
87+
To attach image URLs to project use:
88+
89+
.. code-block:: bash
90+
91+
superannotatecli attach-image-urls --project <project_name/folder_name> --attachments <csv_path> [--annotation_status <annotation_status>]
92+
93+
----------
94+
8295
.. _ref_upload_videos:
8396

8497
Uploading videos

docs/source/superannotate.sdk.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ ________
3737
.. autofunction:: superannotate.delete_folders
3838
.. autofunction:: superannotate.rename_folder
3939
.. autofunction:: superannotate.upload_images_to_project
40+
.. autofunction:: superannotate.attach_image_urls_to_project
4041
.. autofunction:: superannotate.upload_images_from_public_urls_to_project
4142
.. autofunction:: superannotate.upload_images_from_google_cloud_to_project
4243
.. autofunction:: superannotate.upload_images_from_azure_blob_to_project

docs/source/tutorial.sdk.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ To create a new "Vector" project with name "Example Project 1" and description
9898
9999
sa.create_project(project, "test", "Vector")
100100
101+
.. warning::
102+
103+
In general, SDK functions are not thread-safe.
104+
101105
Creating a folder in a project
102106
______________________________
103107

superannotate/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ def consensus(*args, **kwargs):
7272
upload_images_from_google_cloud_to_project,
7373
upload_images_from_public_urls_to_project,
7474
upload_images_from_s3_bucket_to_project, upload_images_to_project,
75-
upload_preannotations_from_folder_to_project, upload_video_to_project,
76-
upload_videos_from_folder_to_project
75+
attach_image_urls_to_project, upload_preannotations_from_folder_to_project,
76+
upload_video_to_project, upload_videos_from_folder_to_project
7777
)
7878
from .db.search_projects import search_projects
7979
from .db.teams import (

superannotate/__main__.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ def main():
6262
create_folder(command, further_args)
6363
elif command == "upload-images":
6464
image_upload(command, further_args)
65+
elif command == "attach-image-urls":
66+
attach_image_urls(command, further_args)
6567
elif command == "upload-videos":
6668
video_upload(command, further_args)
6769
elif command in ["upload-preannotations", "upload-annotations"]:
@@ -288,6 +290,31 @@ def image_upload(command_name, args):
288290
)
289291

290292

293+
def attach_image_urls(command_name, args):
294+
parser = argparse.ArgumentParser(prog=_CLI_COMMAND + " " + command_name)
295+
parser.add_argument(
296+
'--project', required=True, help='Project name to upload'
297+
)
298+
parser.add_argument(
299+
'--attachments',
300+
required=True,
301+
help='path to csv file on attachments metadata'
302+
)
303+
parser.add_argument(
304+
'--annotation_status',
305+
required=False,
306+
default="NotStarted",
307+
help=
308+
'Set images\' annotation statuses after upload. Default is NotStarted'
309+
)
310+
args = parser.parse_args(args)
311+
sa.attach_image_urls_to_project(
312+
project=args.project,
313+
attachments=args.attachments,
314+
annotation_status=args.annotation_status
315+
)
316+
317+
291318
def export_project(command_name, args):
292319
parser = argparse.ArgumentParser(prog=_CLI_COMMAND + " " + command_name)
293320
parser.add_argument(

superannotate/common.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@
2929
"Completed": 5,
3030
"Skipped": 6
3131
}
32+
33+
_UPLOAD_STATES_STR_TO_CODES = {"Initial": 1, "Basic": 2, "External": 3}
34+
_UPLOAD_STATES_CODES_TO_STR = {1: "Initial", 2: "Basic", 3: "External"}
35+
3236
_USER_ROLES = {"Admin": 2, "Annotator": 3, "QA": 4, "Customer": 5, "Viewer": 6}
3337
_AVAILABLE_SEGMENTATION_MODELS = ['autonomous', 'generic']
3438
_MODEL_TRAINING_STATUSES = {
@@ -118,6 +122,14 @@ def annotation_status_str_to_int(annotation_status):
118122
return _ANNOTATION_STATUSES[annotation_status]
119123

120124

125+
def upload_state_str_to_int(upload_state):
126+
return _UPLOAD_STATES_STR_TO_CODES[upload_state]
127+
128+
129+
def upload_state_int_to_str(upload_state):
130+
return _UPLOAD_STATES_CODES_TO_STR[upload_state]
131+
132+
121133
def annotation_status_int_to_str(annotation_status):
122134
"""Converts metadata annotation_status int value to a string
123135

superannotate/db/exports.py

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66
import zipfile
77
from datetime import datetime
88
from pathlib import Path
9+
import shutil
910

1011
import boto3
1112
import requests
1213
from tqdm import tqdm
1314

1415
from ..api import API
15-
from ..common import annotation_status_str_to_int
16+
from ..common import annotation_status_str_to_int, upload_state_int_to_str
1617
from ..exceptions import (
1718
SABaseException, SAExistingExportNameException,
1819
SANonExistingExportNameException
@@ -123,6 +124,12 @@ def prepare_export(
123124
"""
124125
if not isinstance(project, dict):
125126
project = get_project_metadata_bare(project)
127+
upload_state = upload_state_int_to_str(project.get("upload_state"))
128+
if upload_state == "External" and include_fuse == True:
129+
logger.info(
130+
"Include fuse functionality is not supported for projects containing images attached with URLs"
131+
)
132+
include_fuse = False
126133
team_id, project_id = project["team_id"], project["id"]
127134
if annotation_statuses is None:
128135
annotation_statuses = [2, 3, 4, 5]
@@ -203,6 +210,15 @@ def __upload_files_to_aws_thread(
203210
already_uploaded[i] = True
204211

205212

213+
def _download_file(url, local_filename):
214+
with requests.get(url, stream=True) as r:
215+
r.raise_for_status()
216+
with open(local_filename, 'wb') as f:
217+
for chunk in r.iter_content(chunk_size=8192):
218+
f.write(chunk)
219+
return local_filename
220+
221+
206222
def download_export(
207223
project, export, folder_path, extract_zip_contents=True, to_s3_bucket=None
208224
):
@@ -237,25 +253,26 @@ def download_export(
237253
break
238254

239255
filename = Path(res['path']).name
240-
r = requests.get(res['download'], allow_redirects=True)
241-
if to_s3_bucket is None:
242-
filepath = Path(folder_path) / filename
243-
open(filepath, 'wb').write(r.content)
244-
if extract_zip_contents:
245-
with zipfile.ZipFile(filepath, 'r') as f:
246-
f.extractall(folder_path)
247-
Path.unlink(filepath)
248-
logger.info("Extracted %s to folder %s", filepath, folder_path)
249-
else:
250-
logger.info("Downloaded export ID %s to %s", res['id'], filepath)
251-
else:
252-
with tempfile.TemporaryDirectory() as tmpdirname:
253-
filepath = Path(tmpdirname) / filename
254-
open(filepath, 'wb').write(r.content)
256+
with tempfile.TemporaryDirectory() as tmpdirname:
257+
temp_filepath = Path(tmpdirname) / filename
258+
_download_file(res['download'], temp_filepath)
259+
if to_s3_bucket is None:
260+
filepath = Path(folder_path) / filename
261+
shutil.copyfile(temp_filepath, filepath)
255262
if extract_zip_contents:
256263
with zipfile.ZipFile(filepath, 'r') as f:
257-
f.extractall(tmpdirname)
264+
f.extractall(folder_path)
258265
Path.unlink(filepath)
266+
logger.info("Extracted %s to folder %s", filepath, folder_path)
267+
else:
268+
logger.info(
269+
"Downloaded export ID %s to %s", res['id'], filepath
270+
)
271+
else:
272+
if extract_zip_contents:
273+
with zipfile.ZipFile(temp_filepath, 'r') as f:
274+
f.extractall(tmpdirname)
275+
Path.unlink(temp_filepath)
259276
files_to_upload = []
260277
for file in Path(tmpdirname).rglob("*.*"):
261278
if not file.is_file():
@@ -290,4 +307,4 @@ def download_export(
290307
t.join()
291308
finish_event.set()
292309
tqdm_thread.join()
293-
logger.info("Exported to AWS %s/%s", to_s3_bucket, folder_path)
310+
logger.info("Exported to AWS %s/%s", to_s3_bucket, folder_path)

superannotate/db/images.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,11 @@ def download_image(
621621
:return: paths of downloaded image and annotations if included
622622
:rtype: tuple
623623
"""
624+
if (include_fuse or include_overlay) and not include_annotations:
625+
raise SABaseException(
626+
0,
627+
"To download fuse or overlay image need to set include_annotations=True in download_image"
628+
)
624629
if not Path(local_dir_path).is_dir():
625630
raise SABaseException(
626631
0, f"local_dir_path {local_dir_path} is not an existing directory"
@@ -631,6 +636,12 @@ def download_image(
631636
)
632637

633638
project, project_folder = get_project_and_folder_metadata(project)
639+
upload_state = common.upload_state_int_to_str(project.get("upload_state"))
640+
if upload_state == "External":
641+
raise SABaseException(
642+
0,
643+
"The function does not support projects containing images attached with URLs"
644+
)
634645
img = get_image_bytes(
635646
(project, project_folder), image_name, variant=variant
636647
)
@@ -698,6 +709,13 @@ def get_image_bytes(project, image_name, variant='original'):
698709
:return: io.BytesIO() of the image
699710
:rtype: io.BytesIO()
700711
"""
712+
project, project_folder = get_project_and_folder_metadata(project)
713+
upload_state = common.upload_state_int_to_str(project.get("upload_state"))
714+
if upload_state == "External":
715+
raise SABaseException(
716+
0,
717+
"The function does not support projects containing images attached with URLs"
718+
)
701719
if variant not in ["original", "lores"]:
702720
raise SABaseException(
703721
0, "Image download variant should be either original or lores"
@@ -818,7 +836,7 @@ def _get_image_pre_or_annotations(project, image_name, pre):
818836
fill_class_and_attribute_names(res_json, annotation_classes_dict)
819837
result = {
820838
f"{pre}annotation_json":
821-
response.json(),
839+
res_json,
822840
f"{pre}annotation_json_filename":
823841
common.get_annotation_json_name(image_name, project_type)
824842
}
@@ -1041,12 +1059,11 @@ def create_fuse_image(
10411059
(image_size[1], image_size[0], 4), [0, 0, 0, 255], np.uint8
10421060
)
10431061
fi_ovl[:, :, :3] = np.array(pil_image)
1062+
fi_pil_ovl = Image.fromarray(fi_ovl)
1063+
draw_ovl = ImageDraw.Draw(fi_pil_ovl)
10441064
if project_type == "Vector":
10451065
fi_pil = Image.fromarray(fi)
10461066
draw = ImageDraw.Draw(fi_pil)
1047-
if output_overlay:
1048-
fi_pil_ovl = Image.fromarray(fi_ovl)
1049-
draw_ovl = ImageDraw.Draw(fi_pil_ovl)
10501067
for annotation in annotation_json["instances"]:
10511068
if "className" not in annotation:
10521069
continue
@@ -1159,6 +1176,11 @@ def create_fuse_image(
11591176
temp_mask = np.alltrue(annotation_mask == part_color, axis=2)
11601177
fi[temp_mask] = fill_color
11611178
fi_pil = Image.fromarray(fi)
1179+
alpha = 0.5 # transparency measure
1180+
if output_overlay:
1181+
fi_pil_ovl = Image.fromarray(
1182+
cv2.addWeighted(fi, alpha, fi_ovl, 1 - alpha, 0)
1183+
)
11621184

11631185
if in_memory:
11641186
if output_overlay:

0 commit comments

Comments
 (0)