From 02de5444276ecc55e3ce91ff63c9449977012496 Mon Sep 17 00:00:00 2001 From: ueta-eisuke Date: Sat, 26 Dec 2020 14:03:55 +0900 Subject: [PATCH 1/2] add task api --- .gitignore | 4 +- Makefile | 7 ++ README.md | 153 ++++++++++++++++++++++++++++++---- contributing/README.md | 22 +++++ contributing/requirements.txt | 3 + examples/create_image_task.py | 61 ++++++++++++++ fastlabel/__init__.py | 146 ++++++++++++++++++++++++++++---- fastlabel/const.py | 2 +- setup.py | 9 +- tox.ini | 4 + 10 files changed, 376 insertions(+), 35 deletions(-) create mode 100644 Makefile create mode 100644 contributing/README.md create mode 100644 contributing/requirements.txt create mode 100644 examples/create_image_task.py create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index 704a47a..013b97f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,6 @@ .pytest_cache dist build -fastlabel.egg-info \ No newline at end of file +fastlabel.egg-info +test*.py +annotation.json \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4f810dc --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +all: black isort + +black: + black . + +isort: + isort . diff --git a/README.md b/README.md index 9f49e3d..13339ad 100644 --- a/README.md +++ b/README.md @@ -23,30 +23,155 @@ import fastlabel client = fastlabel.Client() ``` -## Model Analysis +## Task -### Upload Predictions +### Create Task + +- Create a new task. ```python -import fastlabel +task = client.create_image_task( + project_id="YOUR_PROJECT_ID", + key="sample.jpg", + url="https://sample.com/sample.jpg" +) +``` -# Initialize client -client = fastlabel.Client() +- Create a new task with pre-defined labels. (Class should be configured on your project in advance) + +```python +task = client.create_image_task( + project_id="YOUR_PROJECT_ID", + key="sample.jpg", + url="https://sample.com/sample.jpg", + labels=[ + { + "type": "bbox", + "value": "bbox", + "points": [ + { "x": 100, "y": 100}, # top-left + { "x": 200, "y": 200} # bottom-right + ] + } + ] +) +``` + +> Check [examples/create_image_task.py](/examples/create_image_task.py) for other label types, such as line, keyPoint and polygon. + +### Find Task -# Create predictions +- Find a single task. + +```python +task = client.find_task(task_id="YOUR_TASK_ID") +``` + +### Get Tasks + +- Get tasks. (Up to 100 tasks) + +```python +tasks = client.get_tasks(project_id="YOUR_PROJECT_ID") +``` + +- Filter and Get tasks. (Up to 100 tasks) + +```python +tasks = client.get_tasks( + project_id="YOUR_PROJECT_ID", + status="submitted", # status can be 'registered', 'registered', 'submitted' or 'skipped' + review_status="accepted" # review_status can be 'notReviewed', 'inProgress', 'accepted' or 'declined' +) +``` + +- Get a large size of tasks. (Over 100 tasks) + +```python +import time + +# Iterate pages until new tasks are empty. +all_tasks = [] +start_after = None +while True: + time.sleep(1) + + tasks = client.get_tasks(project_id="YOUR_PROJECT_ID", start_after=start_after) + all_tasks.extend(tasks) + + if len(tasks) > 0: + start_after = tasks[-1]["id"] # Set the last task id to start_after + else: + break +``` + +> Please wait seconds before sending another requests! + +### Delete Task + +```python +client.delete_task(task_id="YOUR_TASK_ID") +``` + +### Task Response + +- Example of a single task object + +```python +{ + "id": "YOUR_TASK_ID", + "key": "sample.png", + "assigneeId": null, + "assigneeName": null, + "status": "registered", + "reviewAssigneeId": null, + "reviewAssigneeName": null, + "reviewStatus": "notReviewed", + "projectId": "YOUR_PROJECT_ID", + "datasetId": "YOUR_DATASET_ID", + "labels": [ + { + "id": "YOUR_LABEL_ID", + "type": "bbox", + "value": "window", + "title": "窓", + "color": "#d9713e", + "metadata": [], + "points": [ + { "x": 100, "y": 100}, # top-left + { "x": 200, "y": 200} # bottom-right + ] + } + ], + "duration": 0, + "image": { + "width": 1500, + "height": 1200 + }, + "createdAt": "2020-12-25T15:02:00.513", + "updatedAt": "2020-12-25T15:02:00.513" +} +``` + +## Model Analysis + +### Upload Predictions + +```python +# Create your model predictions predictions = [ { - "fileKey": "sample1.jpg", # file name exists in your project + "fileKey": "sample.jpg", # file name exists in your project "labels": [ { - "value": "line_a", # class value exists in your project + "value": "bbox_a", # class value exists in your project "points": [ - { "x": 10, "y": 10 }, - { "x": 20, "y": 20 }, + { "x": 10, "y": 10 }, # top-left + { "x": 20, "y": 20 }, # botom-right ] }, { - "value": "line_b", + "value": "bbox_b", "points": [ { "x": 30, "y": 30 }, { "x": 40, "y": 40 }, @@ -58,9 +183,9 @@ predictions = [ # Upload predictions client.upload_predictions( - project_id="project_id", # your fastlabel project id - analysis_type="line", # annotation type to be analyze ("bbox" or "line" are supported) - threshold=25, # IoU percentage/pixel to analyze labels. (Ex: 0 - 100) + project_id="YOUR_PROJECT_ID", # your fastlabel project id + analysis_type="bbox", # annotation type to be analyze (Only "bbox" or "line" are supported) + threshold=80, # IoU percentage/pixel distance to analyze labels. (Ex: 0 - 100) predictions=predictions ) ``` diff --git a/contributing/README.md b/contributing/README.md new file mode 100644 index 0000000..1d9d69d --- /dev/null +++ b/contributing/README.md @@ -0,0 +1,22 @@ +# Contributing Guideline + +## Pull Request Checklist + +Before sending your pull requests, make sure you followed this list. + +- Read Contributing Guideline +- Run formatter and linter + +## Formatter and Linter + +### Installation + +```bash +$ pip install -r contributing/requirements.txt +``` + +### Run Formatter and Linter + +```bash +make all +``` diff --git a/contributing/requirements.txt b/contributing/requirements.txt new file mode 100644 index 0000000..b3b14f7 --- /dev/null +++ b/contributing/requirements.txt @@ -0,0 +1,3 @@ +black==20.8b1 +flake8==3.8.4 +isort==5.6.4 \ No newline at end of file diff --git a/examples/create_image_task.py b/examples/create_image_task.py new file mode 100644 index 0000000..d5eb0f8 --- /dev/null +++ b/examples/create_image_task.py @@ -0,0 +1,61 @@ +from pprint import pprint + +import fastlabel + +# Initialize client +client = fastlabel.Client() + +project_id = "YOUR_PROJECT_ID" +key = "YOUR_IMAGE_KEY" # Should be an unique in your project +url = "YOUR_IMAGE_URL" +labels = [ + { + "type": "bbox", + "value": "bbox", + "points": [ + {"x": 100, "y": 100}, # top-left + {"x": 200, "y": 200}, # bottom-right + ], + }, + { + "type": "line", + "value": "line", + "points": [{"x": 200, "y": 200}, {"x": 250, "y": 250}], + }, + {"type": "keyPoint", "value": "keyPoint", "points": {"x": 10, "y": 10}}, + { + "type": "polygon", + "value": "polygon", + "points": [ + {"x": 300, "y": 300}, + {"x": 320, "y": 320}, + {"x": 340, "y": 220}, + {"x": 310, "y": 200}, + ], + }, + { + "type": "polyline", + "value": "polyline", + "points": [ + {"x": 100, "y": 300}, + {"x": 120, "y": 320}, + {"x": 140, "y": 220}, + {"x": 110, "y": 200}, + ], + }, + { + "type": "segmentation", + "value": "segmentation", + "points": [ + [ + {"x": 400, "y": 400}, + {"x": 420, "y": 420}, + {"x": 440, "y": 420}, + {"x": 410, "y": 400}, + ] + ], + }, +] + +task = client.create_image_task(project_id=project_id, key=key, url=url, labels=labels) +pprint(task) diff --git a/fastlabel/__init__.py b/fastlabel/__init__.py index e734d29..4f3b7a0 100644 --- a/fastlabel/__init__.py +++ b/fastlabel/__init__.py @@ -1,6 +1,8 @@ import os -import requests from logging import getLogger + +import requests + from fastlabel.const import AnalysisType logger = getLogger(__name__) @@ -8,19 +10,20 @@ APP_BASE_URL = "https://app.fastlabel.ai/projects/" FASTLABEL_ENDPOINT = "https://api-fastlabel-production.web.app/api/v1/" + class Client: api_key = None def __init__(self) -> None: - if not os.environ.get('FASTLABEL_API_KEY'): + if not os.environ.get("FASTLABEL_API_KEY"): raise ValueError("FASTLABEL_API_KEY is not configured.") - self.api_key = "Bearer " + os.environ.get('FASTLABEL_API_KEY') + self.api_key = "Bearer " + os.environ.get("FASTLABEL_API_KEY") def _getrequest(self, endpoint: str, params=None) -> dict: """Makes a get request to an endpoint. If an error occurs, assumes that endpoint returns JSON as: - { 'status_code': XXX, + { 'statusCode': XXX, 'error': 'I failed' } """ params = params or {} @@ -28,25 +31,52 @@ def _getrequest(self, endpoint: str, params=None) -> dict: "Content-Type": "application/json", "Authorization": self.api_key, } - r = requests.get(FASTLABEL_ENDPOINT + endpoint, - headers=headers, params=params) + r = requests.get(FASTLABEL_ENDPOINT + endpoint, headers=headers, params=params) if r.status_code == 200: return r.json() else: try: - error = r.json()['error'] + print(r.json()) + error = r.json()["error"] except ValueError: error = r.text if r.status_code == 400: raise FastLabelInvalidException(error, r.status_code) else: raise FastLabelException(error, r.status_code) - + + def _deleterequest(self, endpoint: str, params=None) -> dict: + """Makes a delete request to an endpoint. + If an error occurs, assumes that endpoint returns JSON as: + { 'statusCode': XXX, + 'error': 'I failed' } + """ + params = params or {} + headers = { + "Content-Type": "application/json", + "Authorization": self.api_key, + } + r = requests.delete( + FASTLABEL_ENDPOINT + endpoint, headers=headers, params=params + ) + + if r.status_code == 200: + return r.json() + else: + try: + error = r.json()["error"] + except ValueError: + error = r.text + if r.status_code == 400: + raise FastLabelInvalidException(error, r.status_code) + else: + raise FastLabelException(error, r.status_code) + def _postrequest(self, endpoint, payload=None): """Makes a post request to an endpoint. If an error occurs, assumes that endpoint returns JSON as: - { 'status_code': XXX, + { 'statusCode': XXX, 'error': 'I failed' } """ payload = payload or {} @@ -60,7 +90,7 @@ def _postrequest(self, endpoint, payload=None): return r.json() else: try: - error = r.json()['error'] + error = r.json()["error"] except ValueError: error = r.text if r.status_code == 400: @@ -68,24 +98,110 @@ def _postrequest(self, endpoint, payload=None): else: raise FastLabelException(error, r.status_code) - def upload_predictions(self, project_id: str, analysis_type: AnalysisType, threshold: int, predictions: list) -> None: + def upload_predictions( + self, + project_id: str, + analysis_type: AnalysisType, + threshold: int, + predictions: list, + ) -> None: + """ + Upload predictions to analyze your model. + """ endpoint = "predictions/upload" payload = { "projectId": project_id, "analysisType": analysis_type, "threshold": threshold, - "predictions": predictions + "predictions": predictions, } self._postrequest(endpoint, payload=payload) - logger.warn("Successfully uploaded! See " + APP_BASE_URL + project_id + "/modelAnalysis") + logger.warn( + "Successfully uploaded! See " + APP_BASE_URL + project_id + "/modelAnalysis" + ) + + def find_task(self, task_id: str) -> dict: + """ + Find a signle task. + """ + endpoint = "tasks/" + task_id + return self._getrequest(endpoint) + + def get_tasks( + self, + project_id: str, + status: str = None, + review_status: str = None, + limit: int = 100, + start_after: str = None, + ) -> dict: + """ + Returns a list of tasks. + + Returns up to 100 at a time, to get more, set task id of the last page passed back to startAfter param. + + project_id is id of your project. (Required) + status can be 'registered', 'registered', 'submitted' or 'skipped'. (Optional) + review_status can be 'notReviewed', 'inProgress', 'accepted' or 'declined'. (Optional) + limit is the max number of results to display per page, (Optional) + start_after can be use to fetch the next page of tasks. (Optional) + """ + endpoint = "tasks/" + params = {"projectId": project_id} + if status: + params["status"] = status + if review_status: + params["reviewStatus"] = review_status + if limit: + params["limit"] = limit + if start_after: + params["startAfter"] = start_after + return self._getrequest(endpoint, params=params) + + def delete_task(self, task_id: str) -> None: + """ + Delete a single task. + """ + endpoint = "tasks/" + task_id + self._deleterequest(endpoint) + + def create_image_task( + self, + project_id: str, + key: str, + url: str, + status: str = None, + review_status: str = None, + labels: list = [], + ) -> dict: + """ + Create a single task for image project. + + project_id is id of your project. + key is an unique identifier of task in your project. (Required) + url is a link to get image data. (Required) + status can be 'registered', 'inProgress', 'submitted' or 'skipped'. (Optional) + review_status can be 'notReviewed', 'inProgress', 'accepted' or 'declined'. (Optional) + labels is a list of label to be set in advance. (Optional) + """ + endpoint = "tasks/image" + payload = {"projectId": project_id, "key": key, "url": url} + if status: + payload["status"] = status + if review_status: + payload["review_status"] = review_status + if labels: + payload["labels"] = labels + return self._postrequest(endpoint, payload=payload) class FastLabelException(Exception): def __init__(self, message, errcode): super(FastLabelException, self).__init__( - ' {}'.format(errcode, message)) + " {}".format(errcode, message) + ) self.code = errcode class FastLabelInvalidException(FastLabelException, ValueError): - pass \ No newline at end of file + pass diff --git a/fastlabel/const.py b/fastlabel/const.py index 3126059..272e8e2 100644 --- a/fastlabel/const.py +++ b/fastlabel/const.py @@ -1,3 +1,3 @@ class AnalysisType(object): bbox = "bbox" - line = "line" \ No newline at end of file + line = "line" diff --git a/setup.py b/setup.py index 0aefc90..ef4795a 100644 --- a/setup.py +++ b/setup.py @@ -3,12 +3,12 @@ with open("README.md", "r", encoding="utf-8") as file: long_description = file.read() -with open('requirements.txt', encoding="utf-8") as file: +with open("requirements.txt", encoding="utf-8") as file: install_requires = file.read() setuptools.setup( name="fastlabel", - version="0.1.1", + version="0.2.0", author="eisuke-ueta", author_email="eisuke.ueta@fastlabel.ai", description="The official Python SDK for FastLabel API, the Data Platform for AI", @@ -16,5 +16,6 @@ long_description_content_type="text/markdown", packages=setuptools.find_packages(), install_requires=install_requires, - python_requires='>=3.8', - include_package_data=True) + python_requires=">=3.8", + include_package_data=True, +) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..2859084 --- /dev/null +++ b/tox.ini @@ -0,0 +1,4 @@ +[tool.isort] +include_trailing_comma = true +line_length = 88 +multi_line_output = 3 \ No newline at end of file From ba41813e81872246e51ebf5a1f723c8bf5eeea16 Mon Sep 17 00:00:00 2001 From: ueta-eisuke Date: Sat, 26 Dec 2020 14:13:19 +0900 Subject: [PATCH 2/2] update readme --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 13339ad..944b73f 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,10 @@ import fastlabel client = fastlabel.Client() ``` +## Limitation + +API is allowed to call 5000 times per hour. If you create/delete a large size of tasks, please wait a second for every requests. + ## Task ### Create Task @@ -105,7 +109,7 @@ while True: break ``` -> Please wait seconds before sending another requests! +> Please wait a second before sending another requests! ### Delete Task @@ -185,7 +189,7 @@ predictions = [ client.upload_predictions( project_id="YOUR_PROJECT_ID", # your fastlabel project id analysis_type="bbox", # annotation type to be analyze (Only "bbox" or "line" are supported) - threshold=80, # IoU percentage/pixel distance to analyze labels. (Ex: 0 - 100) + threshold=80, # IoU percentage/pixel distance to check labels are correct. (Ex: 0 - 100) predictions=predictions ) ```