From d5076aca1efaeec70b490b7756d60cb62e8f784b Mon Sep 17 00:00:00 2001 From: Christoph Beger Date: Tue, 7 Nov 2023 08:08:45 +0000 Subject: [PATCH 01/10] Add devcontainer configuration --- .devcontainer/devcontainer.json | 20 ++++++++++++++++++++ .dockerignore | 1 + .gitignore | 2 ++ 3 files changed, 23 insertions(+) create mode 100644 .devcontainer/devcontainer.json create mode 100644 .dockerignore diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..7976379 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,20 @@ +{ + "name": "Python 3.9", + "image": "mcr.microsoft.com/devcontainers/python:3.9", + "runArgs": [ + // "--gpus", + // "all", + "--env-file", + ".devcontainer/.env" + ], + "postCreateCommand": "pip install -r requirements.txt", + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python", + "ms-azuretools.vscode-docker", + "mhutchie.git-graph" + ] + } + } +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0a3de7f --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +.devcontainer diff --git a/.gitignore b/.gitignore index b6caefd..5f60a0b 100644 --- a/.gitignore +++ b/.gitignore @@ -81,3 +81,5 @@ modesl/* /imgs/* /models/* + +*.env \ No newline at end of file From d2e97e9b535f4ae5a9b4c3d0a0e200864543dfac Mon Sep 17 00:00:00 2001 From: Christoph Beger Date: Tue, 7 Nov 2023 09:26:01 +0000 Subject: [PATCH 02/10] Add OpenAPI specification --- .devcontainer/devcontainer.json | 3 +- Readme.md | 30 ++++++------ deeplasia-api.yml | 81 +++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 14 deletions(-) create mode 100644 deeplasia-api.yml diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 7976379..b26ca89 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -13,7 +13,8 @@ "extensions": [ "ms-python.python", "ms-azuretools.vscode-docker", - "mhutchie.git-graph" + "mhutchie.git-graph", + "42Crunch.vscode-openapi" ] } } diff --git a/Readme.md b/Readme.md index 5912d17..81d5cc3 100644 --- a/Readme.md +++ b/Readme.md @@ -3,10 +3,10 @@ ## Run in conda environment ```bash -$ conda create -n flask_ba python=3.9 -$ conda activate flask_ba -$ pip install -r requirements.txt -$ python app.py +conda create -n flask_ba python=3.9 +conda activate flask_ba +pip install -r requirements.txt +python app.py ``` ## Docker @@ -15,8 +15,8 @@ To run the application in [docker](https://www.section.io/engineering-education/ use the following command: ```bash -$ sudo docker build -t flask_bone_age:latest . -$ sudo docker run -p 8080:8080 flask_bone_age:latest +sudo docker build -t flask_bone_age:latest . +sudo docker run -p 8080:8080 flask_bone_age:latest ``` ### Limiting CPU usage @@ -33,29 +33,33 @@ Note, that this should match the number of threads specified by PyTorch (`torch. ## Request -On python the request can be conducted as follows: +In python the request can be conducted as follows: ```python import requests url = "http://localhost:8080/predict" -test_img = "/home/rassman/bone2gene/data/annotated/ACh/ach_00001.png" -files = {'file': open(test_img,'rb')} +test_img = "/path/to/xray.png" +files = { "file": open(test_img, "rb") } data = { "sex": "female", # specify if known, else is predicted - "use_mask": "1" # 1 for True, 0 for False, default is 1 + "use_mask": True # default is true } -resp = requests.post(url, files=files, data=data) +resp = requests.post(url, files=files, json=data) resp.json() ``` Gives something like: -```json lines -{'bone_age': 164.9562530517578, 'sex_predicted': False, 'used_sex': 'f'} +```json +{ + "bone_age": 164.9562530517578, + "sex_predicted": false, + "used_sex": "female" +} ``` So the canonical way would be as described above, with using the predicted mask and specifying the sex. diff --git a/deeplasia-api.yml b/deeplasia-api.yml new file mode 100644 index 0000000..57c2769 --- /dev/null +++ b/deeplasia-api.yml @@ -0,0 +1,81 @@ +openapi: 3.0.3 +info: + title: Deeplasia API + version: 0.1.0 + description: This is the OpenAPI specification of the Deeplasia bone age assessment service. + license: + name: CC BY-NC-SA 4.0 DEED + url: 'https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en' +servers: + - url: 'http://localhost:8080' + description: Localhost instance. + - url: 'http://host.docker.internal:8080' + description: In case the server is running on the Docker host. +paths: + /predict: + post: + summary: Send an X-ray image with additional configurations to get a bone age prediction. + requestBody: + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/PredictionInput' + responses: + '200': + description: The result of the bone age prediction. + content: + application/json: + schema: + $ref: '#/components/schemas/BoneAgePrediction' + tags: + - boneAge +components: + schemas: + BoneAgePrediction: + description: Result of a bone age prediction. + type: object + properties: + bone_age: + description: The predicted bone age in months. + type: number + format: double + example: 164.9562530517578 + sex_predicted: + description: Whether the sex that was used for the prediction was also predicted or given in the configurations. + type: boolean + example: false + used_sex: + description: The sex that was used for the prediction. + type: string + enum: + - female + - male + required: + - bone_age + - sex_predicted + - used_sex + PredictionInput: + description: Input to perform a bone age prediction. + type: object + properties: + sex: + description: | + The sex of the person of whom the X-ray image was taken. + If not specified, sex is also predicted. + type: string + enum: + - female + - male + use_mask: + description: Whether to apply a mask before predicting bone age. + type: boolean + default: true + fileName: + description: The X-ray image. + type: string + format: binary + required: + - fileName +tags: + - name: boneAge + description: Everything about bone age predictions. From 87ccc757d9bdfa05a130c50c58f854c5d89c1b58 Mon Sep 17 00:00:00 2001 From: Christoph Beger Date: Tue, 7 Nov 2023 11:39:34 +0100 Subject: [PATCH 03/10] Add error responses to api specification --- app.py | 38 ++++++++++++++++++-------------------- deeplasia-api.yml | 4 ++++ 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/app.py b/app.py index 511142c..27c4b7a 100644 --- a/app.py +++ b/app.py @@ -5,7 +5,7 @@ # TODO: add logging to file -from flask import Flask, jsonify, request +from flask import abort, Flask, request import torch import numpy as np @@ -98,26 +98,24 @@ def get_prediction(image_bytes, sex, use_mask): return age.item(), sex, sex_predicted -@app.route("/predict", methods=["POST"]) +@app.post("/predict") def predict(): - if request.method == "POST": - if "file" not in request.files: - print("no file") - return jsonify({"error": "No file part"}) - file = request.files["file"] - image_bytes = file.read() - - sex = request.form.get("sex") - use_mask = request.form.get("use_mask") - - bone_age, sex, sex_predicted = get_prediction(image_bytes, sex, use_mask) - return jsonify( - { - "bone_age": bone_age, - "used_sex": sex, - "sex_predicted": sex_predicted, - } - ) + if "file" not in request.files: + abort(400, "No file provided!") + + file = request.files["file"] + image_bytes = file.read() + + sex = request.form.get("sex") + use_mask = request.form.get("use_mask") + + bone_age, sex, sex_predicted = get_prediction(image_bytes, sex, use_mask) + + return { + "bone_age": bone_age, + "used_sex": sex, + "sex_predicted": sex_predicted, + } if __name__ == "__main__": diff --git a/deeplasia-api.yml b/deeplasia-api.yml index 57c2769..96682bb 100644 --- a/deeplasia-api.yml +++ b/deeplasia-api.yml @@ -27,6 +27,10 @@ paths: application/json: schema: $ref: '#/components/schemas/BoneAgePrediction' + '400': + description: Bad request. + '500': + description: Internal server error. tags: - boneAge components: From 5e1bbd98dd88a96fb74449d81ffde0d643bd9510 Mon Sep 17 00:00:00 2001 From: Christoph Beger Date: Thu, 9 Nov 2023 09:56:37 +0100 Subject: [PATCH 04/10] Serve api spec file --- app.py | 7 ++++++- deeplasia-api.yml | 6 +++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app.py b/app.py index 27c4b7a..3e1a15b 100644 --- a/app.py +++ b/app.py @@ -5,7 +5,7 @@ # TODO: add logging to file -from flask import abort, Flask, request +from flask import abort, Flask, request, Response import torch import numpy as np @@ -117,6 +117,11 @@ def predict(): "sex_predicted": sex_predicted, } +@app.get("/") +def ping(): + with open("deeplasia-api.yml", "r") as f: + return Response(f.read(), mimetype="application/json") + abort(404, "Not found!") if __name__ == "__main__": from waitress import serve diff --git a/deeplasia-api.yml b/deeplasia-api.yml index 96682bb..5985c0d 100644 --- a/deeplasia-api.yml +++ b/deeplasia-api.yml @@ -1,6 +1,6 @@ openapi: 3.0.3 info: - title: Deeplasia API + title: Deeplasia Service API version: 0.1.0 description: This is the OpenAPI specification of the Deeplasia bone age assessment service. license: @@ -74,12 +74,12 @@ components: description: Whether to apply a mask before predicting bone age. type: boolean default: true - fileName: + file: description: The X-ray image. type: string format: binary required: - - fileName + - file tags: - name: boneAge description: Everything about bone age predictions. From 1836f072f5fe4de7dde57ede698ac6b035afbbe9 Mon Sep 17 00:00:00 2001 From: Christoph Beger Date: Tue, 7 Nov 2023 13:30:35 +0100 Subject: [PATCH 05/10] Reduce image size by 1GB and improve build time --- .dockerignore | 1 + Dockerfile | 17 +++++++---------- LICENSE.md => LICENSE | 0 Readme.md => README.md | 4 ++-- app.py | 4 +--- 5 files changed, 11 insertions(+), 15 deletions(-) rename LICENSE.md => LICENSE (100%) rename Readme.md => README.md (94%) diff --git a/.dockerignore b/.dockerignore index 0a3de7f..190c649 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1,2 @@ .devcontainer +models diff --git a/Dockerfile b/Dockerfile index ebc4721..54ff771 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,15 @@ -FROM python:3.9 -# easier base image ? +FROM python:3.9-slim -COPY . /app +# If your company uses a self-signed CA: +# ENV PIP_TRUSTED_HOST=download.pytorch.org WORKDIR /app -RUN ["apt-get", "update"] -RUN ["apt-get", "-y", "install", "vim"] - -#Install necessary packages from requirements.txt with no cache dir allowing for installation on machine with very little memory on board +COPY requirements.txt /app/. RUN pip install -r requirements.txt -#Exposing the default waitress port +COPY . /app + EXPOSE 8080 -#Running the streamlit app -CMD ["python", "app.py"] \ No newline at end of file +CMD [ "waitress-serve", "app:app"] diff --git a/LICENSE.md b/LICENSE similarity index 100% rename from LICENSE.md rename to LICENSE diff --git a/Readme.md b/README.md similarity index 94% rename from Readme.md rename to README.md index 81d5cc3..cf6285b 100644 --- a/Readme.md +++ b/README.md @@ -15,8 +15,8 @@ To run the application in [docker](https://www.section.io/engineering-education/ use the following command: ```bash -sudo docker build -t flask_bone_age:latest . -sudo docker run -p 8080:8080 flask_bone_age:latest +sudo docker build -t flask_bone_age . +sudo docker run -p 8080:8080 -v ./models:/app/models deeplasia-service ``` ### Limiting CPU usage diff --git a/app.py b/app.py index 3e1a15b..72ac9d0 100644 --- a/app.py +++ b/app.py @@ -124,9 +124,7 @@ def ping(): abort(404, "Not found!") if __name__ == "__main__": - from waitress import serve - - serve(app, host="0.0.0.0", port=8080) + app.run() # can be called as `python app.py` From c9f39471517d431de4d4d03dbc9ddf72a72d15c5 Mon Sep 17 00:00:00 2001 From: Christoph Beger Date: Tue, 7 Nov 2023 14:19:01 +0100 Subject: [PATCH 06/10] Add dependabot --- .github/dependabot.yml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..5c03104 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" From 5aa8d31f93c4059a9c307b5a2e3afeafe7f5db69 Mon Sep 17 00:00:00 2001 From: Christoph Beger Date: Tue, 7 Nov 2023 14:19:12 +0100 Subject: [PATCH 07/10] Add docker build workflow --- .github/workflows/build.yml | 38 +++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..de96faf --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,38 @@ +name: Build Docker Image + +on: + workflow_dispatch: + release: + types: [created] + +jobs: + build-and-publish-docker-image: + name: Build Docker image and publish to GitHub Container Registry + runs-on: ubuntu-latest + env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + permissions: + contents: read + packages: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Log into registry ${{ env.REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} From 007fac2114d86a236afe9bbb8b01a21a99f966d1 Mon Sep 17 00:00:00 2001 From: Christoph Beger Date: Tue, 7 Nov 2023 14:21:34 +0100 Subject: [PATCH 08/10] Add html file to display OpenAPI spec --- index.html | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 index.html diff --git a/index.html b/index.html new file mode 100644 index 0000000..3ede1f4 --- /dev/null +++ b/index.html @@ -0,0 +1,47 @@ + + + + + + Deeplasia Service API + + +
+ + + + From 2933c78f5698dd2e82ae03a7016f858826647764 Mon Sep 17 00:00:00 2001 From: Christoph Beger Date: Thu, 9 Nov 2023 09:58:34 +0100 Subject: [PATCH 09/10] Update README.md and add threads env var --- Dockerfile | 2 ++ README.md | 89 +++++++++++++++++++++++++++++++++++++++++++----------- app.py | 4 ++- 3 files changed, 77 insertions(+), 18 deletions(-) diff --git a/Dockerfile b/Dockerfile index 54ff771..20b3f9a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,7 @@ FROM python:3.9-slim +ENV DEEPLASIA_THREADS=4 + # If your company uses a self-signed CA: # ENV PIP_TRUSTED_HOST=download.pytorch.org diff --git a/README.md b/README.md index cf6285b..a42ea2e 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,82 @@ -# Flask setup +# Deeplasia Service -## Run in conda environment +Deeplasia is a prior-free deep learning approach to asses bone age in children and adolescents. +This repository contains a RESTfull service with a simple API to process X-ray images and predict bone age in months. -```bash +Please refer for more information: + +* http://www.deeplasia.de/ +* https://github.com/aimi-bonn/Deeplasia + +[![Build Docker Image](https://github.com/CrescNet/deeplasia-service/actions/workflows/build.yml/badge.svg)](https://github.com/CrescNet/deeplasia-service/actions/workflows/build.yml) + +## How to Use + +In order to run this application, you must provide the deep learning models. Please contact use to get them. + +Use the environment variable `DEEPLASIA_THREADS` to limit the number of threads used by [PyTorch](https://pytorch.org/) (defaults to 4 threads). + +### Run in Conda Environment + +**Requirements:** + +* [Conda](https://docs.conda.io) must be installed +* Deep learning models are located in the directory `./models` + +Run the following CLI commands and navigate to . + +```sh conda create -n flask_ba python=3.9 conda activate flask_ba pip install -r requirements.txt -python app.py +python flask run ``` -## Docker +### Run with Docker + +**Requirements:** + +* [Docker](https://docs.docker.com/engine/install/) must be installed +* Deep learning models are not included in the image and must be mounted on container start + +You can use our pre built Docker image to run the application: -To run the application in [docker](https://www.section.io/engineering-education/how-to-deploy-streamlit-app-with-docker/) -use the following command: +```sh +docker run -p 8080:8080 -v ./models:/app/models ghcr.io/crescnet/deeplasia-service +``` + +Or you can build the image yourself (clone this repository first): ```bash -sudo docker build -t flask_bone_age . -sudo docker run -p 8080:8080 -v ./models:/app/models deeplasia-service +docker build -t deeplasia-service . +docker run -p 8080:8080 -v ./models:/app/models deeplasia-service ``` -### Limiting CPU usage +Navigate to + +#### Limiting CPU usage To [limit the CPU usage of the docker container](https://docs.docker.com/config/containers/resource_constraints/), add the following flags to the docker run cmd: -```bash +```sh --cpus= ``` -Note, that this should match the number of threads specified by PyTorch (`torch.set_num_threads(threads)` in `app.py`). +Note, that this should match the number of threads specified with environment variable `DEEPLASIA_THREADS`. + +e.g.: + +```sh +docker run -p 8080:8080 --cpus=2 -e "DEEPLASIA_THREADS=2" -v ./models:/app/models ghcr.io/crescnet/deeplasia-service +``` + +## API + +[![Swagger UI](https://img.shields.io/badge/-Swagger%20UI-%23Clojure?style=flat&logo=swagger&logoColor=white)](https://crescnet.github.io/deeplasia-service/) -# API +Please refer to `deeplasia-api.yml` for an [OpenAPI](https://www.openapis.org/) specification of the API. -## Request +### Request In python the request can be conducted as follows: @@ -62,7 +107,17 @@ Gives something like: } ``` -So the canonical way would be as described above, with using the predicted mask and specifying the sex. +## Predicting Sex + +The canonical way would be as described in previous sections, with using the predicted mask and specifying the sex. If, however, the sex happens to be unknown (or unsure for e.g. errors of inserting the data) the sex can also be predicted. -Skipping the masking by the predicted mask is meant to be a debugging feature, if the results with the mask are not convincing (e.g. predicting 127 months as age), one could re-conduct bone age prediction without the mask and see if makes a difference. -We might think about storing the masks as a visual control as well as logging features in general in the future. \ No newline at end of file + +## Usage of Masks + +Skipping the masking by the predicted mask is meant to be a debugging feature, if the results with the mask are not convincing +(e.g. predicting 127 months as age), one could re-conduct bone age prediction without the mask and see if makes a difference. +We might think about storing the masks as a visual control as well as logging features in general in the future. + +## License + +The code in this repository and the image `deeplasia-service` are licensed under CC BY-NC-SA 4.0 DEED. diff --git a/app.py b/app.py index 72ac9d0..a50a0c7 100644 --- a/app.py +++ b/app.py @@ -19,11 +19,13 @@ SexPredictor, ) +import os + app = Flask(__name__) use_cuda = torch.cuda.is_available() enable_sex_prediction = True -threads = 4 +threads = int(os.getenv('DEEPLASIA_THREADS', 4)) mask_model_path = "./models/fscnn_cos.ckpt" ensemble = { From 2c83fec53cfd61f7cfe016cf5e96f0d7f00ef6b3 Mon Sep 17 00:00:00 2001 From: Christoph Beger Date: Thu, 9 Nov 2023 09:52:11 +0100 Subject: [PATCH 10/10] Update README.md to match origin repository --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a42ea2e..18e1109 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Please refer for more information: * http://www.deeplasia.de/ * https://github.com/aimi-bonn/Deeplasia -[![Build Docker Image](https://github.com/CrescNet/deeplasia-service/actions/workflows/build.yml/badge.svg)](https://github.com/CrescNet/deeplasia-service/actions/workflows/build.yml) +[![Build Docker Image](https://github.com/sRassmann/bone-age-streamlit/actions/workflows/build.yml/badge.svg)](https://github.com/sRassmann/bone-age-streamlit/actions/workflows/build.yml) ## How to Use @@ -42,7 +42,7 @@ python flask run You can use our pre built Docker image to run the application: ```sh -docker run -p 8080:8080 -v ./models:/app/models ghcr.io/crescnet/deeplasia-service +docker run -p 8080:8080 -v ./models:/app/models ghcr.io/srassmann/bone-age-streamlit ``` Or you can build the image yourself (clone this repository first): @@ -67,12 +67,12 @@ Note, that this should match the number of threads specified with environment va e.g.: ```sh -docker run -p 8080:8080 --cpus=2 -e "DEEPLASIA_THREADS=2" -v ./models:/app/models ghcr.io/crescnet/deeplasia-service +docker run -p 8080:8080 --cpus=2 -e "DEEPLASIA_THREADS=2" -v ./models:/app/models ghcr.io/srassmann/bone-age-streamlit ``` ## API -[![Swagger UI](https://img.shields.io/badge/-Swagger%20UI-%23Clojure?style=flat&logo=swagger&logoColor=white)](https://crescnet.github.io/deeplasia-service/) +[![Swagger UI](https://img.shields.io/badge/-Swagger%20UI-%23Clojure?style=flat&logo=swagger&logoColor=white)](https://srassmann.github.io/bone-age-streamlit/) Please refer to `deeplasia-api.yml` for an [OpenAPI](https://www.openapis.org/) specification of the API. @@ -120,4 +120,4 @@ We might think about storing the masks as a visual control as well as logging fe ## License -The code in this repository and the image `deeplasia-service` are licensed under CC BY-NC-SA 4.0 DEED. +The code in this repository and the image `bone-age-streamlit` are licensed under CC BY-NC-SA 4.0 DEED.