Skip to content

Commit

Permalink
Merge pull request #20 from hasty-ai/kp-add-tags
Browse files Browse the repository at this point in the history
Add image tags
  • Loading branch information
proskudin authored Sep 28, 2021
2 parents 36128e3 + e563b78 commit ca3d53d
Show file tree
Hide file tree
Showing 8 changed files with 345 additions and 2 deletions.
12 changes: 12 additions & 0 deletions docs/source/python_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ Python API
.. autoclass:: LabelClass
:members:

:py:class:`~TagClass`
^^^^^^^^^^^^^^^^^^^^

.. autoclass:: TagClass
:members:

:py:class:`~Attribute`
^^^^^^^^^^^^^^^^^^^^^^

Expand All @@ -47,6 +53,12 @@ Python API
.. autoclass:: Label
:members:

:py:class:`~Tag`
^^^^^^^^^^^^^^^^^^^^

.. autoclass:: Tag
:members:

:py:class:`~Attributer`
^^^^^^^^^^^^^^^^^^^^

Expand Down
6 changes: 5 additions & 1 deletion hasty/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from hasty.label import Label
from hasty.label_class import LabelClass
from hasty.project import Project
from hasty.tag import Tag
from hasty.tag_class import TagClass
import hasty.label_utils as label_utils


Expand All @@ -17,7 +19,7 @@ def int_or_str(value):
return value


__version__ = '0.2.3'
__version__ = '0.3.0'
VERSION = tuple(map(int_or_str, __version__.split('.')))

__all__ = [
Expand All @@ -33,5 +35,7 @@ def int_or_str(value):
'LabelClass',
'Project',
'SemanticSegmentor',
'Tag',
'TagClass',
'label_utils'
]
4 changes: 4 additions & 0 deletions hasty/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ def __init__(self, message):
def max_labels_per_batch(cls, got):
return LimitExceededException(f"Max number of labels per batch is 100, got {got}")

@classmethod
def max_tags_per_batch(cls, got):
return LimitExceededException(f"Max number of tags per batch is 100, got {got}")


class InferenceException(Exception):
def __init__(self, message):
Expand Down
33 changes: 32 additions & 1 deletion hasty/image.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from collections import OrderedDict
from typing import List, Union
from typing import Dict, List, Union
import os
import urllib.request

Expand All @@ -9,6 +9,8 @@
from .exception import ValidationException
from .label import Label
from .label_class import LabelClass
from .tag import Tag
from .tag_class import TagClass


class Image(HastyObject):
Expand Down Expand Up @@ -231,3 +233,32 @@ def download(self, filepath: str):
filepath (str): Local path
"""
urllib.request.urlretrieve(self._public_url, filepath)

def get_tags(self):
"""
Returns image tags (list of `~hasty.Tag` objects)
"""
return PaginatedList(Tag, self._requester,
Tag.endpoint.format(project_id=self.project_id, image_id=self.id),
obj_params={"project_id": self.project_id, "image_id": self.id})

def add_tags(self, tags: List[Union[Dict, TagClass]]):
"""
Create multiple tags. Returns a list of `~hasty.Tag` objects
Args:
tags (list of dict/`~hasty.TagClass`): List of tags, keys:
tag_class_id: Tag class ID
"""
return Tag._batch_create(self._requester, self._project_id, self._id, tags)

def delete_tags(self, tags: List[Union[Dict, Tag, TagClass]]):
"""
Removes multiple tags
Args:
tags (list of dict/`~hasty.Tag`/`~hasty.TagClass`): List of tags, keys:
tag_id: Tag ID of the label (optional if tag_class_id is specified)
tag_class_id: Tag class ID of the label (optional if id is specified)
"""
Tag._batch_delete(self._requester, self._project_id, self._id, tags)
29 changes: 29 additions & 0 deletions hasty/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from .inference import Attributer, Detector, InstanceSegmentor, SemanticSegmentor
from .image import Image
from .label_class import LabelClass
from .tag_class import TagClass


class Project(HastyObject):
Expand Down Expand Up @@ -231,6 +232,34 @@ def create_label_class(self, name: str, color: str = None, class_type: str = "ob
"""
return LabelClass._create(self._requester, self._id, name, color, class_type, norder)

def get_tag_classes(self):
"""
Get tag classes, list of :py:class:`~hasty.TagClass` objects.
"""
return PaginatedList(TagClass, self._requester,
TagClass.endpoint.format(project_id=self._id),
obj_params={"project_id": self.id})

def get_tag_class(self, tag_class_id: str):
"""
Get tag class by id, returns `~hasty.TagClass` object
Arguments:
tag_class_id (str): Tag class id
"""
res = self._requester.get(TagClass.endpoint_class.format(project_id=self.id, tag_class_id=tag_class_id))
return TagClass(self._requester, res, {"project_id": self.id})

def create_tag_class(self, name: str, norder: float = None):
"""
Create tag class, returns :py:class:`~hasty.TagClass` object.
Args:
name (str): Tag class name
norder (float, optional): Order in the Hasty tool
"""
return TagClass._create(self._requester, self._id, name, norder)

def get_attributes(self):
"""
Get label classes, list of :py:class:`~hasty.Attribute` objects.
Expand Down
103 changes: 103 additions & 0 deletions hasty/tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
from collections import OrderedDict

from .exception import LimitExceededException, ValidationException
from .hasty_object import HastyObject
from .tag_class import TagClass


C_TAGS_LIMIT = 100


class Tag(HastyObject):
endpoint = '/v1/projects/{project_id}/images/{image_id}/tags'

def __repr__(self):
return self.get__repr__(OrderedDict({"id": self._id, "tag_class_id": self._tag_class_id,
"tag_class_name": self._tag_class_name, "image_id": self._image_id}))

@property
def id(self):
"""
:type: string
"""
return self._id

@property
def tag_class_id(self):
"""
:type: string
"""
return self._tag_class_id

@property
def project_id(self):
"""
:type: string
"""
return self._project_id

@property
def image_id(self):
"""
:type: string
"""
return self._image_id

@property
def tag_class_name(self):
"""
:type: string
"""
return self._tag_class_name

def _init_properties(self):
self._id = None
self._tag_class_id = None
self._tag_class_name = None
self._project_id = None
self._image_id = None

def _set_prop_values(self, data):
if "id" in data:
self._id = data["id"]
if "name" in data:
self._name = data["name"]
if "project_id" in data:
self._project_id = data["project_id"]
if "norder" in data:
self._norder = data["norder"]

@staticmethod
def _batch_create(requester, project_id, image_id, tags):
if len(tags) == 0:
return []
data = []
if len(tags) > C_TAGS_LIMIT:
raise LimitExceededException.max_tags_per_batch(len(tags))
for tag in tags:
if isinstance(tag, TagClass):
data.append({"tag_class_id": tag.id})
elif "tag_class_id" not in tag:
raise ValidationException(f"tag_class_id property should be defined")
else:
data.append({"tag_class_id": tag["tag_class_id"]})
res = requester.post(Tag.endpoint.format(project_id=project_id, image_id=image_id), json_data=data)
new_tags = []
for tag in res:
new_tags.append(Tag(requester, tag, {"project_id": project_id, "image_id": image_id}))
return new_tags

@staticmethod
def _batch_delete(requester, project_id, image_id, tags):
data = []
for tag in tags:
if isinstance(tag, TagClass):
data.append({"tag_class_id": tag.id})
elif isinstance(tag, Tag):
data.append({"tag_id": tag.id})
elif "tag_class_id" not in tag and "tag_id" not in tag:
raise ValidationException(f"tag_class_id or tag_id property should be defined")
else:
data.append({"tag_id": tag.get("tag_id"),
"tag_class_id": tag.get("tag_class_id")})
requester.delete(Tag.endpoint.format(project_id=project_id, image_id=image_id), json_data=data)
84 changes: 84 additions & 0 deletions hasty/tag_class.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from collections import OrderedDict

from .hasty_object import HastyObject


class TagClass(HastyObject):
endpoint = '/v1/projects/{project_id}/tag_classes'
endpoint_class = '/v1/projects/{project_id}/tag_classes/{tag_class_id}'

def __repr__(self):
return self.get__repr__(OrderedDict({"id": self._id, "name": self._name, "color": self._color,
"type": self._class_type, "norder": self._norder}))

@property
def id(self):
"""
:type: string
"""
return self._id

@property
def name(self):
"""
:type: string
"""
return self._name

@property
def project_id(self):
"""
:type: string
"""
return self._project_id

@property
def norder(self):
"""
:type: float
"""
return self._norder

def _init_properties(self):
self._id = None
self._name = None
self._project_id = None
self._norder = None

def _set_prop_values(self, data):
if "id" in data:
self._id = data["id"]
if "name" in data:
self._name = data["name"]
if "project_id" in data:
self._project_id = data["project_id"]
if "norder" in data:
self._norder = data["norder"]

@staticmethod
def _create(requester, project_id, name, norder=None):
res = requester.post(TagClass.endpoint.format(project_id=project_id),
json_data={"name": name,
"norder": norder})
return TagClass(requester, res, {"project_id": project_id})

def edit(self, name, norder=None):
"""
Edit tag class properties
Arguments:
name (str): Taп class name
norder (float, optional): Order in the Hasty tool
"""
self._requester.put(TagClass.endpoint_class.format(project_id=self.project_id, tag_class_id=self.id),
json_data={"name": name,
"norder": norder})
self._name = name
self._norder = norder

def delete(self):
"""
Deletes tag class
"""
self._requester.delete(TagClass.endpoint_class.format(project_id=self.project_id, tag_class_id=self.id))

Loading

0 comments on commit ca3d53d

Please sign in to comment.