Skip to content

Commit

Permalink
Merge pull request #2 from CrescNet/flask
Browse files Browse the repository at this point in the history
Deeplasia Service via Flask
  • Loading branch information
sRassmann authored Nov 12, 2023
2 parents 4178892 + 2c83fec commit d1220ee
Show file tree
Hide file tree
Showing 12 changed files with 361 additions and 98 deletions.
21 changes: 21 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"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",
"42Crunch.vscode-openapi"
]
}
}
}
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.devcontainer
models
7 changes: 7 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -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"
38 changes: 38 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -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 }}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,5 @@ modesl/*

/imgs/*
/models/*

*.env
19 changes: 9 additions & 10 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
FROM python:3.9
# easier base image ?
FROM python:3.9-slim

COPY . /app
ENV DEEPLASIA_THREADS=4

WORKDIR /app
# If your company uses a self-signed CA:
# ENV PIP_TRUSTED_HOST=download.pytorch.org

RUN ["apt-get", "update"]
RUN ["apt-get", "-y", "install", "vim"]
WORKDIR /app

#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"]
CMD [ "waitress-serve", "app:app"]
File renamed without changes.
123 changes: 123 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Deeplasia Service

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.

Please refer for more information:

* http://www.deeplasia.de/
* https://github.com/aimi-bonn/Deeplasia

[![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

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 <http://localhost:5000/>.

```sh
conda create -n flask_ba python=3.9
conda activate flask_ba
pip install -r requirements.txt
python flask run
```

### 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:

```sh
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):

```bash
docker build -t deeplasia-service .
docker run -p 8080:8080 -v ./models:/app/models deeplasia-service
```

Navigate to <http://localhost:8080/>

#### 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:

```sh
--cpus=<number_of_cpus>
```

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/srassmann/bone-age-streamlit
```

## API

[![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.

### Request

In python the request can be conducted as follows:

```python
import requests

url = "http://localhost:8080/predict"

test_img = "/path/to/xray.png"
files = { "file": open(test_img, "rb") }

data = {
"sex": "female", # specify if known, else is predicted
"use_mask": True # default is true
}

resp = requests.post(url, files=files, json=data)
resp.json()
```

Gives something like:

```json
{
"bone_age": 164.9562530517578,
"sex_predicted": false,
"used_sex": "female"
}
```

## 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.

## 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 `bone-age-streamlit` are licensed under CC BY-NC-SA 4.0 DEED.
64 changes: 0 additions & 64 deletions Readme.md

This file was deleted.

51 changes: 27 additions & 24 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

# TODO: add logging to file

from flask import Flask, jsonify, request
from flask import abort, Flask, request, Response
import torch

import numpy as np
Expand All @@ -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 = {
Expand Down Expand Up @@ -98,32 +100,33 @@ 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()

if __name__ == "__main__":
from waitress import serve
sex = request.form.get("sex")
use_mask = request.form.get("use_mask")

serve(app, host="0.0.0.0", port=8080)
bone_age, sex, sex_predicted = get_prediction(image_bytes, sex, use_mask)

return {
"bone_age": bone_age,
"used_sex": sex,
"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__":
app.run()


# can be called as `python app.py`
Expand Down
Loading

0 comments on commit d1220ee

Please sign in to comment.