Skip to content

Commit

Permalink
Use MONAI Label Server directly for cvat annotation plugin (#1776)
Browse files Browse the repository at this point in the history
* Use MONAI Label Server directly for cvat annotation plugin

Signed-off-by: Sachidanand Alle <[email protected]>

* Update Readme

Signed-off-by: Sachidanand Alle <[email protected]>

---------

Signed-off-by: Sachidanand Alle <[email protected]>
  • Loading branch information
SachidanandAlle authored Nov 6, 2024
1 parent fca868e commit 5dce9ae
Show file tree
Hide file tree
Showing 12 changed files with 442 additions and 277 deletions.
29 changes: 12 additions & 17 deletions plugins/cvat/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,12 @@ To install CVAT and enable Semi-Automatic and Automatic Annotation, follow these
```bash
git clone https://github.com/opencv/cvat
cd cvat
git checkout v2.1.0 # MONAI Label requires tag v2.1.0

# Use your external IP instead of localhost to make the CVAT projects sharable
export CVAT_HOST=127.0.0.1
export CVAT_VERSION=v2.1.0

# Start CVAT from docker-compose, make sure the IP and port are available.
# Refer: https://docs.cvat.ai/docs/administration/advanced/installation_automatic_annotation/
docker-compose -f docker-compose.yml -f components/serverless/docker-compose.serverless.yml up -d

# Create a CVAT superuser account
Expand All @@ -47,19 +46,26 @@ docker exec -it cvat bash -ic 'python3 ~/manage.py createsuperuser'

After completing these steps, CVAT should be accessible via http://127.0.0.1:8080 in Chrome. Use the superuser account created during installation to log in.



#### Setup Nuclio Container Platform
```bash
# Get Nuclio dashboard
wget https://github.com/nuclio/nuclio/releases/download/1.5.16/nuctl-1.5.16-linux-amd64
chmod +x nuctl-1.5.16-linux-amd64
ln -sf $(pwd)/nuctl-1.5.16-linux-amd64 /usr/local/bin/nuctl
export NUCLIO_VERSION=1.13.0
wget https://github.com/nuclio/nuclio/releases/download/$NUCLIO_VERSION/nuctl-$NUCLIO_VERSION-linux-amd64
chmod +x nuctl-$NUCLIO_VERSION-linux-amd64
ln -sf $(pwd)/nuctl-$NUCLIO_VERSION-linux-amd64 /usr/local/bin/nuctl
```

#### Deployment of Endoscopy Models
This step is to deploy MONAI Label plugin with endoscopic models using Nuclio tool.

```bash
# Run MONAI Label Server (Make sure this Host/IP is accessible inside a docker)
export MONAI_LABEL_SERVER=http://`hostname -I | awk '{print $1}'`:8000

git clone https://github.com/Project-MONAI/MONAILabel.git

# Deploy all endoscopy models
./plugins/cvat/deploy.sh endoscopy
# Or to deploy specific function and model, e.g., tooltracking
Expand All @@ -77,15 +83,4 @@ To check or monitor the status of deployed function containers, you can open the
That's it! With these steps, you should have successfully installed CVAT with the MONAI Label extension and deployed endoscopic models using the Nuclio tool.

### Publish Latest Model to CVAT/Nuclio
Once you've fine-tuned the model and confirmed that it meets all the necessary conditions, you can push the updated model to the CVAT/Nuclio function container. This will allow you to use the latest version of the model in your workflows and applications.

```bash
workspace/endoscopy/update_cvat_model.sh <FUNCTION_NAME>

# Bundle Example: publish tool tracking bundle trained model (run this command on the node where cvat/nuclio containers are running)
workspace/endoscopy/update_cvat_model.sh tootracking
# Bundle Example: publish inbody trained model
workspace/endoscopy/update_cvat_model.sh inbody
# DeepEdit Example: publish deepedit trained model (Not from bundle)
workspace/endoscopy/update_cvat_model.sh deepedit
```
> Not Needed to publish the model to CVAT. Model is always served via MONAI Label.
5 changes: 5 additions & 0 deletions plugins/cvat/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
FUNCTION=${1:-**}
MODEL=${2:-*}
FUNCTIONS_DIR=${3:-$SCRIPT_DIR}
MONAI_LABEL_SERVER="${MONAI_LABEL_SERVER:-http://`hostname -I | awk '{print $1}'`:8000}"

nuctl create project cvat

Expand All @@ -26,6 +27,10 @@ shopt -s globstar
for func_config in "$FUNCTIONS_DIR"/$FUNCTION/${MODEL}.yaml
do
func_root="$FUNCTIONS_DIR"
echo "Using MONAI Label Server: $MONAI_LABEL_SERVER"
cp $func_config ${func_config}.bak
sed -i "s|http://monailabel.com|$MONAI_LABEL_SERVER|g" $func_config
mv ${func_config}.bak $func_config
echo "Deploying $func_config..."
nuctl deploy --project-name cvat --path "$func_root" --file "$func_config" --platform local
done
Expand Down
144 changes: 144 additions & 0 deletions plugins/cvat/detector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Copyright (c) MONAI Consortium
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import base64
import io
import json
import logging
import os
import tempfile

import numpy as np
from PIL import Image

from monailabel.client import MONAILabelClient

logging.basicConfig(
level=logging.INFO,
format="[%(asctime)s] [%(process)s] [%(threadName)s] [%(levelname)s] (%(name)s:%(lineno)d) - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)


def init_context(context):
context.logger.info("Init context... 0%")
server = os.environ.get("MONAI_LABEL_SERVER", "http://0.0.0.0:8000")
model = os.environ.get("MONAI_LABEL_MODEL", "tooltracking")
client = MONAILabelClient(server)

info = client.info()
model_info = info["models"][model] if info and info["models"] else None
context.logger.info(f"Monai Label Info: {model_info}")
assert model_info

context.user_data.model = model
context.user_data.model_handler = client
context.logger.info("Init context...100%")


def handler(context, event):
model: str = context.user_data.model
client: MONAILabelClient = context.user_data.model_handler
context.logger.info(f"Run model: {model}")

data = event.body
image = Image.open(io.BytesIO(base64.b64decode(data["image"])))
context.logger.info(f"Image: {image.size}")

image_file = tempfile.NamedTemporaryFile(suffix=".jpg").name
image.save(image_file)

params = {"output": "json"}
_, output_json = client.infer(model=model, image_id="", file=image_file, params=params)
if isinstance(output_json, str) or isinstance(output_json, bytes):
output_json = json.loads(output_json)

results = []
prediction = output_json.get("prediction")
if prediction:
context.logger.info(f"(Classification) Prediction: {prediction}")
# CVAT Limitation:: tag is not yet supported https://github.com/opencv/cvat/issues/4212
# CVAT Limitation:: select highest score and create bbox to represent as tag
e = None
for element in prediction:
if element["score"] > 0:
e = element if e is None or element["score"] > e["score"] else e
context.logger.info(f"New Max Element: {e}")

context.logger.info(f"Final Element with Max Score: {e}")
if e:
results.append(
{
"label": e["label"],
"confidence": e["score"],
"type": "rectangle",
"points": [0, 0, image.size[0] - 1, image.size[1] - 1],
}
)
context.logger.info(f"(Classification) Results: {results}")
else:
annotations = output_json.get("annotations")
for a in annotations:
annotation = a.get("annotation", {})
if not annotation:
continue

elements = annotation.get("elements", [])
for element in elements:
label = element["label"]
contours = element["contours"]
for contour in contours:
points = np.array(contour, int)
results.append(
{
"label": label,
"points": points.flatten().tolist(),
"type": "polygon",
}
)

return context.Response(
body=json.dumps(results),
headers={},
content_type="application/json",
status_code=200,
)


if __name__ == "__main__":
import logging
from argparse import Namespace

logging.basicConfig(
level=logging.INFO,
format="[%(asctime)s] [%(process)s] [%(threadName)s] [%(levelname)s] (%(name)s:%(lineno)d) - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)

def print_all(*args, **kwargs):
return {"args": args, **kwargs}

with open("/home/sachi/Datasets/endo/frame001.jpg", "rb") as fp:
image = base64.b64encode(fp.read())

event = {"body": {"image": image}}
event = Namespace(**event)

context = Namespace(
**{
"logger": logging.getLogger(__name__),
"user_data": Namespace(**{"model": None, "model_handler": None}),
"Response": print_all,
}
)
init_context(context)
response = handler(context, event)
logging.info(response)
21 changes: 4 additions & 17 deletions plugins/cvat/endoscopy/deepedit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ metadata:
namespace: cvat
annotations:
name: DeepEdit
version: 2
type: interactor
framework: pytorch
spec:
min_pos_points: 1
min_neg_points: 0
Expand All @@ -24,7 +24,7 @@ metadata:
spec:
description: A pre-trained DeepEdit model for interactive model for Endoscopy
runtime: 'python:3.8'
handler: main:handler
handler: interactor:handler
eventTimeout: 30s

build:
Expand All @@ -34,17 +34,9 @@ spec:
directives:
preCopy:
- kind: ENV
value: MONAI_LABEL_APP_DIR=/usr/local/monailabel/sample-apps/endoscopy
value: MONAI_LABEL_SERVER=http://monailabel.com
- kind: ENV
value: MONAI_LABEL_MODELS=deepedit
- kind: ENV
value: PYTHONPATH=/usr/local/monailabel/sample-apps/endoscopy
- kind: ENV
value: MONAI_PRETRAINED_PATH=https://github.com/Project-MONAI/MONAILabel/releases/download/data
- kind: ENV
value: INTERACTOR_MODEL=true
- kind: ENV
value: MONAI_LABEL_FLIP_INPUT_POINTS=false
value: MONAI_LABEL_MODEL=deepedit

triggers:
myHttpTrigger:
Expand All @@ -53,11 +45,6 @@ spec:
workerAvailabilityTimeoutMilliseconds: 10000
attributes:
maxRequestBodySize: 33554432 # 32MB
port: 8902

resources:
limits:
nvidia.com/gpu: 1

platform:
attributes:
Expand Down
15 changes: 3 additions & 12 deletions plugins/cvat/endoscopy/inbody.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ metadata:
spec:
description: A pre-trained classification model for Endoscopy to flag if image follows InBody or OutBody
runtime: 'python:3.8'
handler: main:handler
handler: detector:handler
eventTimeout: 30s

build:
Expand All @@ -35,13 +35,9 @@ spec:
directives:
preCopy:
- kind: ENV
value: MONAI_LABEL_APP_DIR=/usr/local/monailabel/sample-apps/endoscopy
value: MONAI_LABEL_SERVER=http://monailabel.com
- kind: ENV
value: MONAI_LABEL_MODELS=inbody
- kind: ENV
value: PYTHONPATH=/usr/local/monailabel/sample-apps/endoscopy
- kind: ENV
value: MONAI_PRETRAINED_PATH=https://github.com/Project-MONAI/MONAILabel/releases/download/data
value: MONAI_LABEL_MODEL=inbody

triggers:
myHttpTrigger:
Expand All @@ -50,11 +46,6 @@ spec:
workerAvailabilityTimeoutMilliseconds: 10000
attributes:
maxRequestBodySize: 33554432 # 32MB
port: 8901

resources:
limits:
nvidia.com/gpu: 1

platform:
attributes:
Expand Down
15 changes: 3 additions & 12 deletions plugins/cvat/endoscopy/tooltracking.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ metadata:
spec:
description: A pre-trained tool tracking model for Endoscopy
runtime: 'python:3.8'
handler: main:handler
handler: detector:handler
eventTimeout: 30s

build:
Expand All @@ -34,13 +34,9 @@ spec:
directives:
preCopy:
- kind: ENV
value: MONAI_LABEL_APP_DIR=/usr/local/monailabel/sample-apps/endoscopy
value: MONAI_LABEL_SERVER=http://monailabel.com
- kind: ENV
value: MONAI_LABEL_MODELS=tooltracking
- kind: ENV
value: PYTHONPATH=/usr/local/monailabel/sample-apps/endoscopy
- kind: ENV
value: MONAI_PRETRAINED_PATH=https://github.com/Project-MONAI/MONAILabel/releases/download/data
value: MONAI_LABEL_MODEL=tooltracking

triggers:
myHttpTrigger:
Expand All @@ -49,11 +45,6 @@ spec:
workerAvailabilityTimeoutMilliseconds: 10000
attributes:
maxRequestBodySize: 33554432 # 32MB
port: 8900

resources:
limits:
nvidia.com/gpu: 1

platform:
attributes:
Expand Down
Loading

0 comments on commit 5dce9ae

Please sign in to comment.