Skip to content

Commit 4ecaba7

Browse files
committed
Add assign images
1 parent b4d25cb commit 4ecaba7

File tree

8 files changed

+262
-51
lines changed

8 files changed

+262
-51
lines changed

docs/source/superannotate.sdk.rst

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,8 @@ ________
4242
.. autofunction:: superannotate.unshare_project
4343
.. autofunction:: superannotate.get_project_settings
4444
.. autofunction:: superannotate.set_project_settings
45-
..
46-
## .. autofunction:: superannotate.get_project_default_image_quality_in_editor
47-
## .. autofunction:: superannotate.set_project_default_image_quality_in_editor
45+
.. autofunction:: superannotate.get_project_default_image_quality_in_editor
46+
.. autofunction:: superannotate.set_project_default_image_quality_in_editor
4847
.. autofunction:: superannotate.get_project_workflow
4948
.. autofunction:: superannotate.set_project_workflow
5049

@@ -77,6 +76,8 @@ ______
7776
.. autofunction:: superannotate.upload_annotations_from_json_to_image
7877
.. autofunction:: superannotate.copy_image
7978
.. autofunction:: superannotate.move_image
79+
.. autofunction:: superannotate.pin_image
80+
.. autofunction:: superannotate.assign_images
8081
.. autofunction:: superannotate.delete_image
8182
.. autofunction:: superannotate.add_annotation_bbox_to_image
8283
.. autofunction:: superannotate.add_annotation_polygon_to_image

install.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,6 @@ pip install -e .
1919

2020
# for testing
2121
pip install pytest pytest-xdist
22+
23+
# for docs
24+
pip install sphinx sphinx_rtd_theme

superannotate/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@
4545
set_image_annotation_status, upload_annotations_from_json_to_image
4646
)
4747
from .db.project import get_project_metadata, search_projects
48-
from .db.project_images import copy_image, move_image, upload_image_to_project
48+
from .db.project_images import (
49+
assign_images, copy_image, move_image, pin_image, upload_image_to_project
50+
)
4951
from .db.projects import (
5052
create_project, create_project_like_project, delete_project,
5153
get_project_default_image_quality_in_editor, get_project_image_count,

superannotate/db/images.py

Lines changed: 40 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -903,50 +903,49 @@ def upload_annotations_from_json_to_image(
903903
path=f'/image/{image_id}/annotation/getAnnotationUploadToken',
904904
params=params
905905
)
906-
if response.ok:
907-
res = response.json()
908-
if project_type == 1: # vector
909-
res = res['objects']
910-
s3_session = boto3.Session(
911-
aws_access_key_id=res['accessKeyId'],
912-
aws_secret_access_key=res['secretAccessKey'],
913-
aws_session_token=res['sessionToken']
914-
)
915-
s3_resource = s3_session.resource('s3')
916-
bucket = s3_resource.Bucket(res["bucket"])
917-
bucket.put_object(
918-
Key=res['filePath'], Body=json.dumps(annotation_json)
919-
)
920-
else: # pixel
921-
if mask is None:
922-
raise SABaseException(0, "Pixel annotation should have mask.")
923-
if not isinstance(mask, io.BytesIO):
924-
with open(mask, "rb") as f:
925-
mask = io.BytesIO(f.read())
926-
res_j = res['pixel']
927-
s3_session = boto3.Session(
928-
aws_access_key_id=res_j['accessKeyId'],
929-
aws_secret_access_key=res_j['secretAccessKey'],
930-
aws_session_token=res_j['sessionToken']
931-
)
932-
s3_resource = s3_session.resource('s3')
933-
bucket = s3_resource.Bucket(res_j["bucket"])
934-
bucket.put_object(
935-
Key=res_j['filePath'], Body=json.dumps(annotation_json)
936-
)
937-
res_m = res['save']
938-
s3_session = boto3.Session(
939-
aws_access_key_id=res_m['accessKeyId'],
940-
aws_secret_access_key=res_m['secretAccessKey'],
941-
aws_session_token=res_m['sessionToken']
942-
)
943-
s3_resource = s3_session.resource('s3')
944-
bucket = s3_resource.Bucket(res_m["bucket"])
945-
bucket.put_object(Key=res_m['filePath'], Body=mask)
946-
else:
906+
if not response.ok:
947907
raise SABaseException(
948908
response.status_code, "Couldn't upload annotation. " + response.text
949909
)
910+
res = response.json()
911+
if project_type == 1: # vector
912+
res = res['objects']
913+
s3_session = boto3.Session(
914+
aws_access_key_id=res['accessKeyId'],
915+
aws_secret_access_key=res['secretAccessKey'],
916+
aws_session_token=res['sessionToken']
917+
)
918+
s3_resource = s3_session.resource('s3')
919+
bucket = s3_resource.Bucket(res["bucket"])
920+
bucket.put_object(
921+
Key=res['filePath'], Body=json.dumps(annotation_json)
922+
)
923+
else: # pixel
924+
if mask is None:
925+
raise SABaseException(0, "Pixel annotation should have mask.")
926+
if not isinstance(mask, io.BytesIO):
927+
with open(mask, "rb") as f:
928+
mask = io.BytesIO(f.read())
929+
res_j = res['pixel']
930+
s3_session = boto3.Session(
931+
aws_access_key_id=res_j['accessKeyId'],
932+
aws_secret_access_key=res_j['secretAccessKey'],
933+
aws_session_token=res_j['sessionToken']
934+
)
935+
s3_resource = s3_session.resource('s3')
936+
bucket = s3_resource.Bucket(res_j["bucket"])
937+
bucket.put_object(
938+
Key=res_j['filePath'], Body=json.dumps(annotation_json)
939+
)
940+
res_m = res['save']
941+
s3_session = boto3.Session(
942+
aws_access_key_id=res_m['accessKeyId'],
943+
aws_secret_access_key=res_m['secretAccessKey'],
944+
aws_session_token=res_m['sessionToken']
945+
)
946+
s3_resource = s3_session.resource('s3')
947+
bucket = s3_resource.Bucket(res_m["bucket"])
948+
bucket.put_object(Key=res_m['filePath'], Body=mask)
950949

951950

952951
def create_fuse_image(

superannotate/db/project_images.py

Lines changed: 93 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
from ..exceptions import SABaseException
1212
from .images import (
1313
delete_image, get_image_annotations, get_image_bytes, get_image_metadata,
14-
set_image_annotation_status, upload_annotations_from_json_to_image
14+
search_images, set_image_annotation_status,
15+
upload_annotations_from_json_to_image
1516
)
1617
from .projects import (
1718
__create_image, _get_project_image_quality_in_editor,
@@ -126,7 +127,8 @@ def copy_image(
126127
image_name,
127128
destination_project,
128129
include_annotations=False,
129-
copy_annotation_status=False
130+
copy_annotation_status=False,
131+
copy_pin=False
130132
):
131133
"""Copy image to a project. The image's project is the same as destination
132134
project then the name will be changed to <image_name>_(<num>).<image_ext>,
@@ -142,6 +144,8 @@ def copy_image(
142144
:type include_annotations: bool
143145
:param copy_annotation_status: enables annotations status copy
144146
:type copy_annotation_status: bool
147+
:param copy_pin: enables image pin status copy
148+
:type copy_pin: bool
145149
"""
146150
if not isinstance(source_project, dict):
147151
source_project = get_project_metadata(source_project)
@@ -190,6 +194,8 @@ def copy_image(
190194
destination_project, new_name,
191195
annotation_status_int_to_str(img_metadata["annotation_status"])
192196
)
197+
if copy_pin:
198+
pin_image(destination_project, new_name, img_metadata["is_pinned"])
193199

194200
logger.info(
195201
"Copied image %s/%s to %s/%s.", source_project["name"], image_name,
@@ -201,8 +207,9 @@ def move_image(
201207
source_project,
202208
image_name,
203209
destination_project,
204-
include_annotations=False,
205-
copy_annotation_status=False
210+
include_annotations=True,
211+
copy_annotation_status=True,
212+
copy_pin=True
206213
):
207214
"""Move image from source_project to destination_project. source_project
208215
and destination_project cannot be the same.
@@ -217,6 +224,8 @@ def move_image(
217224
:type include_annotations: bool
218225
:param copy_annotation_status: enables annotations status copy
219226
:type copy_annotation_status: bool
227+
:param copy_pin: enables image pin status copy
228+
:type copy_pin: bool
220229
"""
221230
if not isinstance(source_project, dict):
222231
source_project = get_project_metadata(source_project)
@@ -228,7 +237,86 @@ def move_image(
228237
)
229238
copy_image(
230239
source_project, image_name, destination_project, include_annotations,
231-
copy_annotation_status
240+
copy_annotation_status, copy_pin
232241
)
233242
delete_image(source_project, image_name)
234243
logger.info("Deleted image %s/%s.", source_project["name"], image_name)
244+
245+
246+
def pin_image(project, image_name, pin=True):
247+
"""Pins (or unpins) image
248+
249+
:param project: project name or metadata of the project
250+
:type project: str or dict
251+
:param image_name: image name
252+
:type image: str
253+
:param pin: sets to pin if True, else unpins image
254+
:type pin: bool
255+
"""
256+
if not isinstance(project, dict):
257+
project = get_project_metadata(project)
258+
img_metadata = get_image_metadata(project, image_name)
259+
team_id, project_id, image_id = project["team_id"], project[
260+
"id"], img_metadata["id"]
261+
params = {"team_id": team_id, "project_id": project_id}
262+
json_req = {"is_pinned": int(pin)}
263+
response = _api.send_request(
264+
req_type='PUT',
265+
path=f'/image/{image_id}',
266+
params=params,
267+
json_req=json_req
268+
)
269+
if not response.ok:
270+
raise SABaseException(
271+
response.status_code, "Couldn't pin image " + response.text
272+
)
273+
274+
275+
def assign_images(project, image_names, user):
276+
"""Assigns images to a user. The assignment role, QA or Annotator, will
277+
be deduced from the user's role in the project. With SDK, the user can be
278+
assigned to a role in the project with the share_project function.
279+
280+
:param project: project name or metadata of the project
281+
:type project: str or dict
282+
:param image_names: list of image names to assign
283+
:type image_names: list of str
284+
:param user: user email
285+
:type user: str
286+
"""
287+
logger.info("Assign %s images to user %s", len(image_names), user)
288+
if len(image_names) == 0:
289+
return
290+
if not isinstance(project, dict):
291+
project = get_project_metadata(project)
292+
folder_id = None
293+
images = search_images(project, return_metadata=True)
294+
image_dict = {}
295+
for image in images:
296+
image_dict[image["name"]] = image["id"]
297+
if folder_id is None:
298+
folder_id = image["folder_id"]
299+
elif folder_id != image["folder_id"]:
300+
raise SABaseException(0, "Folders not implemented yet")
301+
302+
image_ids = []
303+
for image_name in image_names:
304+
image_ids.append(image_dict[image_name])
305+
team_id, project_id = project["team_id"], project["id"]
306+
params = {
307+
"team_id": team_id,
308+
"project_id": project_id,
309+
"folder_id": folder_id
310+
}
311+
json_req = {"user_id": user, "image_ids": image_ids}
312+
response = _api.send_request(
313+
req_type='POST',
314+
path='/images/assign',
315+
params=params,
316+
json_req=json_req
317+
)
318+
if not response.ok:
319+
raise SABaseException(
320+
response.status_code, "Couldn't assign images " + response.text
321+
)
322+
# print(response.json())

tests/test_assign_images.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from pathlib import Path
2+
import time
3+
4+
import pytest
5+
6+
import superannotate as sa
7+
8+
PROJECT_NAME_VECTOR = "test assign images"
9+
10+
11+
def test_assign_images(tmpdir):
12+
tmpdir = Path(tmpdir)
13+
14+
projects = sa.search_projects(PROJECT_NAME_VECTOR, return_metadata=True)
15+
for project in projects:
16+
sa.delete_project(project)
17+
18+
project = sa.create_project(PROJECT_NAME_VECTOR, "test", "Vector")
19+
sa.share_project(project, "hovnatan@superannotate.com", "QA")
20+
21+
sa.upload_images_from_folder_to_project(
22+
project, "./tests/sample_project_vector"
23+
)
24+
25+
sa.assign_images(
26+
project, ["example_image_1.jpg", "example_image_2.jpg"],
27+
"hovnatan@superannotate.com"
28+
)
29+
30+
time.sleep(1)
31+
im1_metadata = sa.get_image_metadata(project, "example_image_1.jpg")
32+
im2_metadata = sa.get_image_metadata(project, "example_image_2.jpg")
33+
34+
assert im1_metadata["qa_id"] == "hovnatan@superannotate.com"
35+
assert im2_metadata["qa_id"] == "hovnatan@superannotate.com"
36+
37+
sa.unshare_project(project, "hovnatan@superannotate.com")
38+
39+
time.sleep(1)
40+
41+
im1_metadata = sa.get_image_metadata(project, "example_image_1.jpg")
42+
im2_metadata = sa.get_image_metadata(project, "example_image_2.jpg")
43+
44+
assert im1_metadata["qa_id"] is None
45+
assert im2_metadata["qa_id"] is None
46+
assert im1_metadata["annotator_id"] is None
47+
assert im2_metadata["annotator_id"] is None
48+
49+
sa.share_project(project, "hovnatan@superannotate.com", "Annotator")
50+
51+
sa.assign_images(
52+
project, ["example_image_1.jpg", "example_image_2.jpg"],
53+
"hovnatan@superannotate.com"
54+
)
55+
56+
time.sleep(1)
57+
im1_metadata = sa.get_image_metadata(project, "example_image_1.jpg")
58+
im2_metadata = sa.get_image_metadata(project, "example_image_2.jpg")
59+
60+
assert im1_metadata["annotator_id"] == "hovnatan@superannotate.com"
61+
assert im2_metadata["annotator_id"] == "hovnatan@superannotate.com"
62+
assert im1_metadata["qa_id"] is None
63+
assert im2_metadata["qa_id"] is None
64+
print(sa.get_project_metadata(PROJECT_NAME_VECTOR))

tests/test_image_copy_move.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ def test_image_copy_mult(tmpdir):
3737
"./tests/sample_project_vector/example_image_2.jpg",
3838
annotation_status="InProgress"
3939
)
40+
sa.pin_image(project, "example_image_1.jpg")
4041

4142
images = sa.search_images(project)
4243
assert len(images) == 2
@@ -48,7 +49,8 @@ def test_image_copy_mult(tmpdir):
4849
image,
4950
project,
5051
include_annotations=True,
51-
copy_annotation_status=True
52+
copy_annotation_status=True,
53+
copy_pin=True
5254
)
5355
assert len(sa.search_images(project)) == 5
5456
images = sa.search_images(project)
@@ -57,6 +59,9 @@ def test_image_copy_mult(tmpdir):
5759
anns = sa.get_image_annotations(project, f"example_image_1_({i+1}).jpg")
5860
assert anns["annotation_json"] is not None
5961

62+
metadata = sa.get_image_metadata(project, f"example_image_1_({i+1}).jpg")
63+
assert metadata["is_pinned"] == 1
64+
6065

6166
def test_image_copy(tmpdir):
6267
tmpdir = Path(tmpdir)

0 commit comments

Comments
 (0)