From d15b0abc9dca87a7ef4103900ed007c72d1ed5c3 Mon Sep 17 00:00:00 2001 From: Charlie Junod Date: Tue, 27 Aug 2024 17:14:50 +0200 Subject: [PATCH 1/2] first implementation of the microservice. Service name, description etc. is up-to-date and the service runs correctly. --- README.md | 73 ++++++++++-------------------------- pyproject.toml | 2 +- requirements-all.txt | 57 ++++++++++++++++++++++++++++ requirements.txt | 4 +- src/draw_boxes/__init__.py | 0 src/draw_boxes/draw_boxes.py | 14 +++++++ src/main.py | 45 +++++++++++++--------- 7 files changed, 123 insertions(+), 72 deletions(-) create mode 100644 requirements-all.txt create mode 100644 src/draw_boxes/__init__.py create mode 100644 src/draw_boxes/draw_boxes.py diff --git a/README.md b/README.md index 67b3211..d589fb9 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,20 @@ -# Create a new service (generic) template - -This repository contains the Python + FastAPI template to create a service -without a model or from an existing model compatible with the Core engine. - -Please read the documentation at - to -understand how to use this template. - -## Guidelines - -TODO: Add instructions on how to edit this template. - -### Publishing and deploying using a CI/CD pipeline - -This is the recommended way to publish and deploy your service if you have -access to GitHub Actions or GitLab CI. - -TODO - -### Publishing and deploying manually - -This is the recommended way to publish and deploy your service if you do not -have access to GitHub Actions or GitLab CI or do not want to use these services. - -TODO - -## Checklist - -These checklists allow you to ensure everything is set up correctly. - -### Common tasks - -- [ ] Rename the project in the [`pyproject.toml`](./pyproject.toml) file -- [x] Add files that must be ignored to the [`.gitignore`](.gitignore) configuration file -- [ ] TODO - -### Publishing and deploying using a CI/CD pipeline - -> [!NOTE] -> This checklist is specific to the _Publishing and deploying using a CI/CD -> pipeline_ section. - -- [x] Add the environment variables -- [ ] TODO - -### Publishing and deploying manually - -> [!NOTE] -> This checklist is specific to the _Publishing and deploying manually_ section. - -- [x] Edit the [`.env`](.env) configuration file -- [ ] TODO +# Image Draw Bounding Boxes +This microservice draws boxes on an image. It is intended to work with the +text recognition OCR service. The bounding boxes are passed in a JSON file that corresponds to the output of +the text recognition service. Namely, it must have the following structure: +``` +{"boxes":[ + "position":{ + "left": ..., + "top": ..., + "width": ..., + "height": ..., + }, + {"position": ...}, + ... +]} +``` +Where the values in "left", "top", "width" and "height" are given in pixels. +The bounding boxes are drawn as red unfilled rectangles at the coordinates specified by the 4 fields above. + +The JSON file may contain additional fields; they will be ignored. \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 66fbac8..3afd14c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "service" # Edit this and remove comment +name = "image-draw-bounding-boxes" [tool.pytest.ini_options] pythonpath = [".", "src"] diff --git a/requirements-all.txt b/requirements-all.txt new file mode 100644 index 0000000..8be46d6 --- /dev/null +++ b/requirements-all.txt @@ -0,0 +1,57 @@ +aiobotocore==2.12.1 +aiofiles==23.2.1 +aiohappyeyeballs==2.4.0 +aiohttp==3.10.5 +aioitertools==0.11.0 +aiosignal==1.3.1 +annotated-types==0.7.0 +anyio==4.4.0 +async-timeout==4.0.3 +attrs==24.2.0 +botocore==1.34.51 +certifi==2024.7.4 +click==8.1.7 +common_code @ git+https://github.com/swiss-ai-center/common-code.git@e8bfec8bc00995aa2a0b022bcac03780d38856af +coverage==7.6.1 +dnspython==2.6.1 +email_validator==2.1.1 +exceptiongroup==1.2.2 +fastapi==0.110.0 +flake8==7.0.0 +frozenlist==1.4.1 +h11==0.14.0 +httpcore==1.0.5 +httpx==0.27.0 +idna==3.8 +iniconfig==2.0.0 +jmespath==1.0.1 +MarkupSafe==2.1.5 +mccabe==0.7.0 +multidict==6.0.5 +packaging==24.1 +pillow==10.4.0 +pip==22.0.2 +pluggy==1.5.0 +pycodestyle==2.11.1 +pydantic==2.8.2 +pydantic-settings==2.2.1 +pydantic_core==2.20.1 +pyflakes==3.2.0 +pytest==8.1.1 +pytest-asyncio==0.23.5.post1 +pytest-cov==4.1.0 +pytest_httpserver==1.0.10 +python-dateutil==2.9.0.post0 +python-dotenv==1.0.1 +PyYAML==6.0.1 +setuptools==59.6.0 +six==1.16.0 +sniffio==1.3.1 +starlette==0.36.3 +tomli==2.0.1 +typing_extensions==4.12.2 +urllib3==2.0.7 +uvicorn==0.28.0 +Werkzeug==3.0.4 +wrapt==1.16.0 +yarl==1.9.4 diff --git a/requirements.txt b/requirements.txt index ddd9715..29771ff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,3 @@ -common-code[test] @ git+https://github.com/swiss-ai-center/common-code.git@main +pillow~=10.4.0 +pip~=22.0.2 +setuptools~=59.6.0 \ No newline at end of file diff --git a/src/draw_boxes/__init__.py b/src/draw_boxes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/draw_boxes/draw_boxes.py b/src/draw_boxes/draw_boxes.py new file mode 100644 index 0000000..acfbe18 --- /dev/null +++ b/src/draw_boxes/draw_boxes.py @@ -0,0 +1,14 @@ +from PIL import Image, ImageDraw +import io + + +def draw_bounding_boxes(image, boxes): + imageStream = io.BytesIO(image) + image = Image.open(imageStream) + image = ImageDraw.Draw(image) + for elt in boxes: + box = elt['position'] + x, y, w, h = box['left'], box['top'], box['width'], box['height'] + shape = [x, y, x + w, y + h] + image.rectangle(shape, fill=None, outline="red") + return image._image diff --git a/src/main.py b/src/main.py index 5ffd2a7..46888d1 100644 --- a/src/main.py +++ b/src/main.py @@ -1,5 +1,6 @@ import asyncio import time + from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import RedirectResponse @@ -19,15 +20,16 @@ from contextlib import asynccontextmanager # Imports required by the service's model -# TODO: 1. ADD REQUIRED IMPORTS (ALSO IN THE REQUIREMENTS.TXT) +from draw_boxes.draw_boxes import draw_bounding_boxes +import io +import json settings = get_settings() class MyService(Service): - # TODO: 2. CHANGE THIS DESCRIPTION """ - My service model + bounding boxes drawing service """ # Any additional fields must be excluded for Pydantic to work @@ -36,14 +38,12 @@ class MyService(Service): def __init__(self): super().__init__( - # TODO: 3. CHANGE THE SERVICE NAME AND SLUG - name="My Service", - slug="my-service", + name="Image Draw Bounding Boxes", + slug="image-draw-bounding-boxes", url=settings.service_url, summary=api_summary, description=api_description, status=ServiceStatus.AVAILABLE, - # TODO: 4. CHANGE THE INPUT AND OUTPUT FIELDS, THE TAGS AND THE HAS_AI VARIABLE data_in_fields=[ FieldDescription( name="image", @@ -52,10 +52,14 @@ def __init__(self): FieldDescriptionType.IMAGE_JPEG, ], ), + FieldDescription( + name="data", + type=[FieldDescriptionType.APPLICATION_JSON] + ) ], data_out_fields=[ FieldDescription( - name="result", type=[FieldDescriptionType.APPLICATION_JSON] + name="image-with-boxes", type=[FieldDescriptionType.IMAGE_PNG, FieldDescriptionType.IMAGE_JPEG] ), ], tags=[ @@ -70,7 +74,6 @@ def __init__(self): ) self._logger = get_logger(settings) - # TODO: 5. CHANGE THE PROCESS METHOD (CORE OF THE SERVICE) def process(self, data): # NOTE that the data is a dictionary with the keys being the field names set in the data_in_fields # The objects in the data variable are always bytes. It is necessary to convert them to the desired type @@ -78,10 +81,19 @@ def process(self, data): # raw = data["image"].data # input_type = data["image"].type # ... do something with the raw data + img = data['image'].data + img_format = data['image'].type + boxes_data = json.loads(data['data'].data) + + img_boxes = draw_bounding_boxes(image=img, boxes=boxes_data['boxes']) + img_type = str(data['image'].type).split('_')[1] + img_byte_arr = io.BytesIO() + img_boxes.save(img_byte_arr, format=img_type) + img_byte_arr = img_byte_arr.getvalue() # NOTE that the result must be a dictionary with the keys being the field names set in the data_out_fields return { - "result": TaskData(data=..., type=FieldDescriptionType.APPLICATION_JSON) + "image-with-boxes": TaskData(data=img_byte_arr, type=img_format) } @@ -135,19 +147,18 @@ async def announce(): await service_service.graceful_shutdown(my_service, engine_url) -# TODO: 6. CHANGE THE API DESCRIPTION AND SUMMARY -api_description = """My service -bla bla bla... +api_description = """This microservice draws boxes on an image. It is intended to work with the +text recognition OCR service. The bounding boxes are passed in a JSON file that corresponds to the output of +the text recognition service. """ -api_summary = """My service -bla bla bla... +api_summary = """This microservice draws boxes on an image. It is intended to work with the +text recognition OCR service. """ # Define the FastAPI application with information -# TODO: 7. CHANGE THE API TITLE, VERSION, CONTACT AND LICENSE app = FastAPI( lifespan=lifespan, - title="Sample Service API.", + title="Image Draw Bounding Boxes Service API.", description=api_description, version="0.0.1", contact={ From 2b2077c0c760c37944fb3c26da978892ef1026f2 Mon Sep 17 00:00:00 2001 From: Charlie Junod Date: Mon, 2 Sep 2024 15:31:14 +0200 Subject: [PATCH 2/2] Added a link to the doc to the readme and changed the common-code source in requirements and requirements-all as demanded in the pull request review. --- README.md | 4 +++- requirements-all.txt | 1 - requirements.txt | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d589fb9..df67cca 100644 --- a/README.md +++ b/README.md @@ -17,4 +17,6 @@ the text recognition service. Namely, it must have the following structure: Where the values in "left", "top", "width" and "height" are given in pixels. The bounding boxes are drawn as red unfilled rectangles at the coordinates specified by the 4 fields above. -The JSON file may contain additional fields; they will be ignored. \ No newline at end of file +The JSON file may contain additional fields; they will be ignored. + +_Check the [related documentation](https://docs.swiss-ai-center.ch/reference/services/image-draw-bounding-boxes) for more information._ \ No newline at end of file diff --git a/requirements-all.txt b/requirements-all.txt index 8be46d6..9788912 100644 --- a/requirements-all.txt +++ b/requirements-all.txt @@ -11,7 +11,6 @@ attrs==24.2.0 botocore==1.34.51 certifi==2024.7.4 click==8.1.7 -common_code @ git+https://github.com/swiss-ai-center/common-code.git@e8bfec8bc00995aa2a0b022bcac03780d38856af coverage==7.6.1 dnspython==2.6.1 email_validator==2.1.1 diff --git a/requirements.txt b/requirements.txt index 29771ff..3159573 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +common-code[test] @ git+https://github.com/swiss-ai-center/common-code.git@main pillow~=10.4.0 pip~=22.0.2 setuptools~=59.6.0 \ No newline at end of file