[[_TOC_]]
# Anotação Automática por meio do CVAT
No CVAT, os modelos de Deep Learning podem ser implementados através de serverless functions, por meio do framework nuclio. A ideia central da anotação automática é implementar um script em python que receba a imagem limpa, e retorne a imagem anotada (o framework pode ser darknet, pytorch, tensorflow, etc). O script deve ser empacotado em um container que será gerenciado pelo nuclio e interpretado pelo CVAT.
### Guia de Instalação
```
git clone https://github.com/opencv/cvat.git
cd cvat/
```
```
docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f components/serverless/docker-compose.serverless.yml up -d --build
```
Próximo passo é instalar o nuclio. Para isso baixamos o binário em https://github.com/nuclio/nuclio/releases e movemos para o diretório usr/bin. Referência: https://nuclio.io/docs/latest/reference/nuctl/nuctl/
Agora podemos verificar nossas serverless functions através do seguinte comando:
```
$ nuctl get functions
No functions found
```
### Estrutura de uma Serverless Function
```
├── cvat
│ ├── serverless
│ │ ├── darknet (ou outro framework)
│ │ │ ├── nuclio
│ │ │ │ ├── main.py
│ │ │ │ ├── function.yaml
│ │ │ │ ├── function-gpu.yaml
```
```
main.py : Implementação do detector (API Python)
function.yaml : Descreve o microsserviço (imagem, etc)
```
### Serverless Function para o framework Darknet (YoloV4)
#### Exemplo de main.py
```python=
# importing module
import sys
# appending a path
sys.path.append('/home/developer/darknet')
import json
import base64
from PIL import Image
import io
import numpy as np
import cv2
from darknet_images import image_detection
from darknet import load_network
#import cv2
def init_context(context):
context.logger.info("Init context... 0%")
weights = '/home/developer/models/unilever/primary/yolov4_best_pessoas.weights'
datafile = '/home/developer/models/unilever/primary/unilever-primary.data'
cfg = '/home/developer/models/unilever/primary/yolov4_pessoas.cfg'
thresh= 0.5
network, class_names, class_colors = load_network(cfg, datafile, weights, 1)
context.user_data.model = network
context.user_data.class_names = class_names
context.user_data.class_colors = class_colors
context.logger.info("Init context...100%")
def handler(context, event):
context.logger.info("Executando: unilever-primary-yolov4-tiny-darknet")
data = event.body
buf = base64.b64decode(data["image"])
image_as_np = np.frombuffer(buf, dtype=np.uint8)
input_image = cv2.imdecode(image_as_np, flags=1)
threshold = float(data.get("threshold", 0.25))
image, detections = image_detection(
input_image,
context.user_data.model,
context.user_data.class_names,
context.user_data.class_colors,
threshold
)
scale_x = input_image.shape[1] / image.shape[1]
scale_y = input_image.shape[0] / image.shape[0]
yolo_results_json = detections
encoded_results = []
for result in yolo_results_json:
x, y, w, h = result[2]
x = x * scale_x
w = w * scale_x
y = y * scale_y
h = h * scale_y
xmin = int(round(x - (w / 2)))
xmax = int(round(x + (w / 2)))
ymin = int(round(y - (h / 2)))
ymax = int(round(y + (h / 2)))
encoded_results.append({
'confidence': str( float(result[1])/100.0 ),
'label': result[0],
'points': [
xmin,
ymin,
xmax,
ymax
],
'type': 'rectangle'
})
return context.Response(body=json.dumps(encoded_results), headers={},
content_type='application/json', status_code=200)
```
Note que a estrutura do arquivo main.py está pronta para modelos YoloV4. Dessa forma, deve-se alterar apenas os paths contidos nas variáveis weights, datafile e cfg.
```
datafile (.data): Contém número de classes e path para o arquivo .names do modelo
```
#### Exemplo de arquivo function-gpu.yaml
```yaml=
metadata:
name: unilever-yolov4-tiny-primary
namespace: cvat
annotations:
name: Unilever Primary
type: detector
framework: darknet
spec: |
[
{ "id": 0, "name": "pessoa" },
{ "id": 1, "name": "empilhadeira" },
{ "id": 2, "name": "paleteira_veiculo" },
{ "id": 3, "name": "paleteira_manual" },
{ "id": 4, "name": "AGV" }
]
spec:
description: Unilever Yolo-v4-Tiny Primario
runtime: 'python:3.8'
handler: main:handler
eventTimeout: 30s
build:
image: cvat/darknet-unilever-primary
baseImage: mettainnovations/yolo-aa-1.0:latest
directives:
preCopy:
- kind: USER
value: root
- kind: RUN
value: apt update && apt install --no-install-recommends -y libglib2.0-0
- kind: WORKDIR
value: /opt/nuclio
triggers:
myHttpTrigger:
maxWorkers: 1
kind: 'http'
workerAvailabilityTimeoutMilliseconds: 10000
attributes:
maxRequestBodySize: 33554432 # 32MB
port: 32002
resources:
limits:
nvidia.com/gpu: 1
platform:
attributes:
restartPolicy:
name: always
maximumRetryCount: 3
mountMode: volume
```
### Serverless Function para o framework MaskRCNN (Segmentação)
#### Exemplo de main.py
```python=
import json
import base64
from PIL import Image
import io
from model_loader import ModelLoader
import numpy as np
import yaml
def init_context(context):
context.logger.info("Init context... 0%")
with open("/opt/nuclio/function-gpu.yaml", 'rb') as function_file:
functionconfig = yaml.safe_load(function_file)
labels_spec = functionconfig['metadata']['annotations']['spec']
labels = {item['id']: item['name'] for item in json.loads(labels_spec)}
model_handler = ModelLoader(labels)
context.user_data.model_handler = model_handler
context.logger.info("Init context...100%")
def handler(context, event):
context.logger.info("Run tf.matterport.mask_rcnn model")
data = event.body
buf = io.BytesIO(base64.b64decode(data["image"]))
threshold = float(data.get("threshold", 0.2))
image = Image.open(buf)
results = context.user_data.model_handler.infer(np.array(image), threshold)
return context.Response(body=json.dumps(results), headers={},
content_type='application/json', status_code=200)
```
#### Exemplo de model_loader.py
```python=
import os
import numpy as np
import sys
from skimage.measure import find_contours, approximate_polygon
import tensorflow as tf
MASK_RCNN_DIR = os.path.abspath(os.environ.get('MASK_RCNN_DIR'))
if MASK_RCNN_DIR:
sys.path.append(MASK_RCNN_DIR) # To find local version of the library
from mrcnn import model as modellib
from mrcnn.config import Config
class ModelLoader:
def __init__(self, labels):
COCO_MODEL_PATH = os.path.join(MASK_RCNN_DIR, "mask_rcnn_trilho_0100.h5")
if COCO_MODEL_PATH is None:
raise OSError('Model path env not found in the system.')
class InferenceConfig(Config):
NAME = "efvm-track-inspector"
NUM_CLASSES = 1 + 6 # COCO has 80 classes
GPU_COUNT = 1
IMAGES_PER_GPU = 1
IMAGE_MIN_DIM = 320
IMAGE_MAX_DIM = 640
DETECTION_MIN_CONFIDENCE = 0.7
RPN_ANCHOR_SCALES = (8, 16, 32, 64, 128)
TRAIN_ROIS_PER_IMAGE = 32
MAX_GT_INSTANCES = 50
POST_NMS_ROIS_INFERENCE = 500
POST_NMS_ROIS_TRAINING = 1000
BACKBONE = 'resnet50'
# Limit gpu memory to 30% to allow for other nuclio gpu functions. Increase fraction as you like
import keras.backend.tensorflow_backend as ktf
def get_session(gpu_fraction=0.1):
gpu_options = tf.GPUOptions(
per_process_gpu_memory_fraction=gpu_fraction,
allow_growth=True)
return tf.Session(config=tf.ConfigProto(gpu_options=gpu_options))
ktf.set_session(get_session())
# Print config details
self.config = InferenceConfig()
self.config.display()
self.model = modellib.MaskRCNN(mode="inference",
config=self.config, model_dir=MASK_RCNN_DIR)
self.model.load_weights(COCO_MODEL_PATH, by_name=True)
self.labels = labels
def infer(self, image, threshold):
output = self.model.detect([image], verbose=1)[0]
results = []
MASK_THRESHOLD = 0.5
for i in range(len(output["rois"])):
score = output["scores"][i]
class_id = output["class_ids"][i]
mask = output["masks"][:, :, i]
if score >= threshold:
mask = mask.astype(np.uint8)
contours = find_contours(mask, MASK_THRESHOLD)
# only one contour exist in our case
contour = contours[0]
contour = np.flip(contour, axis=1)
# Approximate the contour and reduce the number of points
contour = approximate_polygon(contour, tolerance=2.5)
if len(contour) < 4:
continue
label = self.labels[class_id]
results.append({
"confidence": str(score),
"label": label,
"points": contour.ravel().tolist(),
"type": "polygon",
})
return results
```
#### Exemplo de arquivo function-gpu.yaml
```yaml=
metadata:
name: efvm-track-inspection
namespace: cvat
annotations:
name: efvm-track-inspection
type: detector
framework: tensorflow
spec: |
[
{ "id": 0, "name": "BG" },
{ "id": 1, "name": "grampo" },
{ "id": 2, "name": "dormente" },
{ "id": 3, "name": "trilho" },
{ "id": 4, "name": "lastro" },
{ "id": 5, "name": "sombra" },
{ "id": 6, "name": "vegetacao" }
]
spec:
description: Mask RCNN optimized for GPU (EFVM track inspection classes)
runtime: 'python:3.6'
handler: main:handler
eventTimeout: 30s
env:
- name: MASK_RCNN_DIR
value: /opt/nuclio/Mask_RCNN
build:
image: cvat/efvm/track_inspection
baseImage: tensorflow/tensorflow:1.15.5-gpu-py3
directives:
postCopy:
- kind: WORKDIR
value: /opt/nuclio
- kind: RUN
value: apt-key adv --fetch-keys https://developer.download.nvidia.com/compute/cuda/repos/ubuntu1804/x86_64/3bf863cc.pub && apt-get update
- kind: RUN
value: apt update && apt install --no-install-recommends -y git curl
- kind: RUN
value: git clone --depth 1 https://github.com/matterport/Mask_RCNN.git
- kind: RUN
value: curl -L https://github.com/matterport/Mask_RCNN/releases/download/v2.0/mask_rcnn_coco.h5 -o Mask_RCNN/mask_rcnn_coco.h5
- kind: RUN
value: pip3 install numpy cython pyyaml keras==2.1.0 scikit-image Pillow
triggers:
myHttpTrigger:
maxWorkers: 1
kind: 'http'
workerAvailabilityTimeoutMilliseconds: 10000
attributes:
maxRequestBodySize: 33554432 # 32MB
resources:
limits:
nvidia.com/gpu: 1
platform:
attributes:
restartPolicy:
name: always
maximumRetryCount: 3
mountMode: volume
```
# Passo a Passo para Deploy de uma Serverless Function Darknet em GPU
1) Criar [estrutura de diretórios padrão](#estrutura) em **/mnt/storage/workspace/cvat/**
2) Copiar modelo (**.cfg**, **.weights**, **.data** e **.names**) para pasta **/mnt/storage/workspace/yolo-aa/models**
3) Rebuildar imagem **mettainnovations/yolo-aa**
```
cd /mnt/storage/workspace/yolo-aa/
sudo docker build -t mettainnovations/yolo-aa-1.0 .
```
4) Editar os paths do modelo em [main.py](#main) e editar campos no arquivo [function-gpu.yaml](#yaml).
> Note que passamos **mettainnovations/yolo-aa** como imagem base no arquivo **.yaml**.
> Verifique se a porta configurada no arquivo **.yaml** já está sendo utilizada por outra serverless function (É possível verificar através do comando **nuctl get functions**)
> Além disso, lembre-se que os **paths devem ser relativos ao container! ( /home/developer/models/... )**
5) Fazer deploy via script
```
cd /mnt/storage/workspace/cvat/
sudo ./serverless/deploy_gpu.sh ./serverless/darknet/unilever/primary/
```
6) Verifique o status da serverless function
```
$ sudo nuctl get functions
NAMESPACE | NAME | PROJECT | STATE | REPLICAS | NODE PORT
nuclio | pth-foolwood-siammask | cvat | ready | 1/1 | 49153
```
Note que passamos o path para o diretório da serverless function, composto por main.py, function.yaml e function-gpu.yaml
# Passo a Passo para Deploy de uma Serverless Function MaskRCNN em GPU
1) Criar [estrutura de diretórios padrão](#estrutura) em **/mnt/storage/workspace/cvat/**
2) Editar arquivos model_loader.py, main.py e function-gpu.yaml, passando os paths e classes relativos à rede.
3) Fazer deploy via script
```
cd /mnt/storage/workspace/cvat/
sudo ./serverless/deploy_gpu.sh ./serverless/tensorflow/efvm_inspecao/
```
4) Copiar modelo de segmentação (.h5) pra dentro do container gerado.
```bash=
sudo docker cp ./mask_rcnn_trilho_0100.h5 <container_id>:/opt/nuclio/Mask_RCNN
```
Assim, a Serverless Function de Segmentação estará pronta para uso.
### Anotando automaticamente via Serverless Function
Após deploy, o detector fica disponível na aba **AI Tools** do Job:

É possível anotar todas as imagens de um Job de uma única vez. Para isso, vá até a Task referente à esse Job na aba **Tasks**, selecione **Actios** e depois **Automatic Annotation**. Em seguida, selecione o modelo que será utilzado para anotação automática.
### Referências
1 - [Tutorial de Deploy](https://github.com/opencv/cvat/blob/d8ab99c22f84718d853e1f4939abed24dd63a17e/docs/serverless_tutorial.md)
2 - [Versões do Nuclio](https://github.com/nuclio/nuclio/releases)
3 - [Instalação do Nuclio](https://nuclio.io/docs/latest/reference/nuctl/nuctl/)
4 - [Repositório Git do CVAT](https://github.com/opencv/cvat)