未验证 提交 98616c72 编写于 作者: R Roman Donchenko 提交者: GitHub

Modernize OpenVINO-based Nuclio functions and allow them to run on Kubernetes (#6129)

Currently, OpenVINO-based functions assume that a local directory will
be mounted into the container. In Kubernetes, that isn't possible, so
implement an alternate approach: create a separate base image and
inherit the function image from it.

In addition, implement some modernizations:

* Upgrade the version of OpenVINO to the latest (2022.3). Make the
necessary updates to the code. Note that 2022.1 introduced an entirely
new inference API, but I haven't switched to it yet to minimize changes.

* Use the runtime version of the Docker image as the base instead of the
dev version. This significantly reduces the size of the final image (by
~3GB).

* Replace the `faster_rcnn_inception_v2_coco` model with
`faster_rcnn_inception_resnet_v2_atrous_coco`, as the former has been
  removed from OMZ.

* Ditto with `person-reidentification-retail-0300` -> `0277`.

* The IRs used in the DEXTR function are not supported by OpenVINO
anymore (format too old), so rewrite the build process to create them
from the original code/weights instead.
上级 2a41d596
......@@ -11,6 +11,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Running SAM masks decoder on frontend (<https://github.com/opencv/cvat/pull/6019>)
- The `person-reidentification-retail-0300` and
`faster_rcnn_inception_v2_coco` Nuclio functions were replaced with
`person-reidentification-retail-0277` and
`faster_rcnn_inception_resnet_v2_atrous_coco`, respectively
(<https://github.com/opencv/cvat/pull/6129>).
- OpenVINO-based Nuclio functions now use the OpenVINO 2022.3 runtime
(<https://github.com/opencv/cvat/pull/6129>).
### Deprecated
- TDB
......@@ -24,6 +31,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- The issue azure.core.exceptions.ResourceExistsError: The specified blob already exists (<https://github.com/opencv/cvat/pull/6082>)
- Image scaling when moving between images with different resolution (<https://github.com/opencv/cvat/pull/6081>)
- Invalid completed job count reporting (<https://github.com/opencv/cvat/issues/6098>)
- OpenVINO-based Nuclio functions can now be deployed to Kubernetes
(<https://github.com/opencv/cvat/pull/6129>).
### Security
- TDB
......
../serverless/common/openvino
\ No newline at end of file
{{- if .Values.nuclio.enabled }}
apiVersion: v1
kind: ConfigMap
metadata:
name: cvat-nuclio-openvino-common
namespace: {{ .Release.Namespace }}
labels:
{{- include "cvat.labels" . | nindent 4 }}
app: cvat-app
tier: nuclio
{{- if semverCompare ">=1.21-0" .Capabilities.KubeVersion.GitVersion }}
immutable: true
{{- end }}
binaryData:
python3: |-
{{ .Files.Get "nuclio_func_common_files/python3" | b64enc }}
model_loader.py:
{{ .Files.Get "nuclio_func_common_files/model_loader.py" | b64enc }}
{{- end}}
#!/bin/bash
args=$@
. /opt/intel/openvino/bin/setupvars.sh
PYTHONPATH=/opt/nuclio/common/openvino:$PYTHONPATH
/usr/bin/python3 $args
#!/bin/bash
# Sample commands to deploy nuclio functions on CPU
set -eu
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
FUNCTIONS_DIR=${1:-$SCRIPT_DIR}
nuctl create project cvat
export DOCKER_BUILDKIT=1
docker build -t cvat.openvino.base "$SCRIPT_DIR/openvino/base"
nuctl create project cvat --platform local
shopt -s globstar
for func_config in "$FUNCTIONS_DIR"/**/function.yaml
do
func_root=$(dirname "$func_config")
echo Deploying $(dirname "$func_root") function...
nuctl deploy --project-name cvat --path "$func_root" \
--volume "$SCRIPT_DIR/common:/opt/nuclio/common" \
--platform local
done
func_root="$(dirname "$func_config")"
func_rel_path="$(realpath --relative-to="$SCRIPT_DIR" "$(dirname "$func_root")")"
nuctl get function
if [ -f "$func_root/Dockerfile" ]; then
docker build -t "cvat.${func_rel_path//\//.}.base" "$func_root"
fi
echo "Deploying $func_rel_path function..."
nuctl deploy --project-name cvat --path "$func_root" --platform local
done
nuctl get function --platform local
#!/bin/bash
# Sample commands to deploy nuclio functions on GPU
set -eu
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
FUNCTIONS_DIR=${1:-$SCRIPT_DIR}
nuctl create project cvat
nuctl create project cvat --platform local
shopt -s globstar
for func_config in "$FUNCTIONS_DIR"/**/function-gpu.yaml
do
func_root=$(dirname "$func_config")
echo "Deploying $(dirname "$func_root") function..."
func_root="$(dirname "$func_config")"
func_rel_path="$(realpath --relative-to="$SCRIPT_DIR" "$(dirname "$func_root")")"
echo "Deploying $func_rel_path function..."
nuctl deploy --project-name cvat --path "$func_root" \
--volume "$SCRIPT_DIR/common:/opt/nuclio/common" \
--file "$func_config" --platform local
done
nuctl get function
nuctl get function --platform local
FROM openvino/ubuntu20_runtime:2022.3.0
USER root
RUN apt-get update \
&& apt-get -y --no-install-recommends install python-is-python3 \
&& rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir opencv-python-headless pillow pyyaml
COPY model_loader.py shared.py /opt/nuclio/common/openvino/
ENV PYTHONPATH=/opt/nuclio/common/openvino:$PYTHONPATH
USER openvino
......@@ -14,24 +14,22 @@ class ModelLoader:
# Initialize input blobs
self._input_info_name = None
for blob_name in network.inputs:
if len(network.inputs[blob_name].shape) == 4:
for blob_name in network.input_info:
if len(network.input_info[blob_name].tensor_desc.dims) == 4:
self._input_blob_name = blob_name
elif len(network.inputs[blob_name].shape) == 2:
self._input_layout = network.input_info[blob_name].tensor_desc.dims
elif len(network.input_info[blob_name].tensor_desc.dims) == 2:
self._input_info_name = blob_name
else:
raise RuntimeError(
"Unsupported {}D input layer '{}'. Only 2D and 4D input layers are supported"
.format(len(network.inputs[blob_name].shape), blob_name))
.format(len(network.input_info[blob_name].tensor_desc.dims), blob_name))
# Initialize output blob
self._output_blob_name = next(iter(network.outputs))
# Load network
self._net = ie_core.load_network(network, "CPU", num_requests=2)
input_type = network.inputs[self._input_blob_name]
self._input_layout = input_type if isinstance(input_type, list) else input_type.shape
def _prepare_inputs(self, image, preprocessing):
image = np.array(image)
......@@ -67,5 +65,5 @@ class ModelLoader:
return self._input_layout[2:]
@property
def layers(self):
return self._network.layers
def network(self):
return self._network
FROM openvino/ubuntu20_dev:2022.3.0 AS build
USER root
RUN apt-get update \
&& apt-get -y --no-install-recommends install patch \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /root
ARG DEXTR_COMMIT=352ccc76067156ebcf7267b07e0a5e43d32e83d5
# TODO: use `ADD --checksum` when that feature becomes stable
ADD https://data.vision.ee.ethz.ch/csergi/share/DEXTR/dextr_pascal-sbd.pth ./
ADD https://github.com/scaelles/DEXTR-PyTorch/archive/$DEXTR_COMMIT.zip dextr.zip
RUN python3 -m zipfile -e dextr.zip .
WORKDIR /root/DEXTR-PyTorch-$DEXTR_COMMIT
ADD export.py adaptive-pool.patch .
RUN patch -p1 -i adaptive-pool.patch
RUN python3 export.py /root/dextr_pascal-sbd.pth /root/dextr.onnx
RUN mo --input_model=/root/dextr.onnx --model_name=dextr --output_dir=/root
FROM cvat.openvino.base
COPY --from=build --chown=root:root /root/dextr.xml /root/dextr.bin /opt/nuclio/
This is a hack to work around the the lack of support for AdaptiveAvgPool2d
in PyTorch's ONNX exporter (<https://github.com/pytorch/pytorch/issues/42653>).
It might become unnecessary in the future, since OpenVINO 2023 is to add support
for AdaptiveAvgPool2d exported with operator_export_type=ONNX_ATEN_FALLBACK
(<https://github.com/openvinotoolkit/openvino/pull/14682>).
diff --git a/networks/deeplab_resnet.py b/networks/deeplab_resnet.py
index ecfa084..e8ff297 100644
--- a/networks/deeplab_resnet.py
+++ b/networks/deeplab_resnet.py
@@ -99,7 +99,14 @@ class PSPModule(nn.Module):
self.final = nn.Conv2d(out_features, n_classes, kernel_size=1)
def _make_stage_1(self, in_features, size):
- prior = nn.AdaptiveAvgPool2d(output_size=(size, size))
+ kernel_size, stride = {
+ 1: (64, 64),
+ 2: (32, 32),
+ 3: (22, 21),
+ 6: (11, 9),
+ }[size]
+
+ prior = nn.AvgPool2d(kernel_size=kernel_size, stride=stride)
conv = nn.Conv2d(in_features, in_features//4, kernel_size=1, bias=False)
bn = nn.BatchNorm2d(in_features//4, affine=affine_par)
relu = nn.ReLU(inplace=True)
#!/usr/bin/env python3
import sys
import torch
import torch.nn
import torch.onnx
import networks.deeplab_resnet as resnet
net = resnet.resnet101(1, nInputChannels=4, classifier='psp')
state_dict_checkpoint = torch.load(sys.argv[1], map_location=torch.device('cpu'))
net.load_state_dict(state_dict_checkpoint)
full_net = torch.nn.Sequential(
net,
torch.nn.Upsample((512, 512), mode='bilinear', align_corners=True),
torch.nn.Sigmoid(),
)
full_net.eval()
input_tensor = torch.randn((1, 4, 512, 512))
torch.onnx.export(full_net, input_tensor, sys.argv[2])
......@@ -13,43 +13,13 @@ metadata:
spec:
description: Deep Extreme Cut
runtime: 'python:3.6'
runtime: 'python:3.8'
handler: main:handler
eventTimeout: 30s
env:
- name: NUCLIO_PYTHON_EXE_PATH
value: /opt/nuclio/common/openvino/python3
volumes:
- volume:
name: openvino-common
configMap:
name: "cvat-nuclio-openvino-common"
defaultMode: 0750
volumeMount:
name: openvino-common
mountPath: /opt/nuclio/common/openvino
build:
image: cvat/openvino.dextr
baseImage: openvino/ubuntu18_runtime:2020.2
directives:
preCopy:
- kind: USER
value: root
- kind: WORKDIR
value: /opt/nuclio
- kind: RUN
value: ln -s /usr/bin/pip3 /usr/bin/pip
postCopy:
- kind: RUN
value: curl -O https://download.01.org/openvinotoolkit/models_contrib/cvat/dextr_model_v1.zip
- kind: RUN
value: unzip dextr_model_v1.zip
- kind: RUN
value: pip3 install -U pip && pip3 install wheel Pillow
image: cvat.openvino.dextr
baseImage: cvat.openvino.dextr.base
triggers:
myHttpTrigger:
......
FROM openvino/ubuntu20_dev:2022.3.0 AS build
USER root
RUN omz_downloader \
--name face-detection-0205,emotions-recognition-retail-0003,age-gender-recognition-retail-0013 \
--precisions FP32 \
-o /opt/nuclio/open_model_zoo
FROM cvat.openvino.base
COPY --from=build --chown=root:root /opt/nuclio/open_model_zoo /opt/nuclio/open_model_zoo
metadata:
name: openvino-omz-face-detection-0205
name: openvino-omz-intel-face-detection-0205
namespace: cvat
annotations:
name: Attributed face detection
......@@ -28,47 +28,13 @@ metadata:
spec:
description: Detection network finding faces and defining age, gender and emotion attributes
runtime: 'python:3.6'
runtime: 'python:3.8'
handler: main:handler
eventTimeout: 30000s
env:
- name: NUCLIO_PYTHON_EXE_PATH
value: /opt/nuclio/common/openvino/python3
volumes:
- volume:
name: openvino-common
configMap:
name: "cvat-nuclio-openvino-common"
defaultMode: 0750
volumeMount:
name: openvino-common
mountPath: /opt/nuclio/common/openvino
build:
image: cvat.openvino.omz.intel.face-detection-0205
baseImage: openvino/ubuntu18_dev:2021.1
directives:
preCopy:
- kind: USER
value: root
- kind: WORKDIR
value: /opt/nuclio
- kind: RUN
value: ln -s /usr/bin/pip3 /usr/bin/pip
- kind: RUN
value: /opt/intel/openvino/deployment_tools/open_model_zoo/tools/downloader/downloader.py --name face-detection-0205 -o /opt/nuclio/open_model_zoo
- kind: RUN
value: /opt/intel/openvino/deployment_tools/open_model_zoo/tools/downloader/downloader.py --name emotions-recognition-retail-0003 -o /opt/nuclio/open_model_zoo
- kind: RUN
value: /opt/intel/openvino/deployment_tools/open_model_zoo/tools/downloader/downloader.py --name age-gender-recognition-retail-0013 -o /opt/nuclio/open_model_zoo
postCopy:
- kind: RUN
value: apt update && DEBIAN_FRONTEND=noninteractive apt install --no-install-recommends -y python3-skimage
- kind: RUN
value: pip3 install "numpy<1.16.0" # workaround for skimage
baseImage: cvat.openvino.omz.intel.face-detection-0205.base
triggers:
myHttpTrigger:
......
FROM openvino/ubuntu20_dev:2022.3.0 AS build
USER root
RUN omz_downloader \
--name person-reidentification-retail-0277 \
--precisions FP32 \
-o /opt/nuclio/open_model_zoo
FROM cvat.openvino.base
USER root
RUN pip install --no-cache-dir scipy
COPY --from=build /opt/nuclio/open_model_zoo /opt/nuclio/open_model_zoo
USER openvino
metadata:
name: openvino-omz-intel-person-reidentification-retail-0300
name: openvino-omz-intel-person-reidentification-retail-0277
namespace: cvat
annotations:
name: Person reidentification
......@@ -9,39 +9,13 @@ metadata:
spec:
description: Person reidentification model for a general scenario
runtime: 'python:3.6'
runtime: 'python:3.8'
handler: main:handler
eventTimeout: 30s
env:
- name: NUCLIO_PYTHON_EXE_PATH
value: /opt/nuclio/common/openvino/python3
volumes:
- volume:
name: openvino-common
configMap:
name: "cvat-nuclio-openvino-common"
defaultMode: 0750
volumeMount:
name: openvino-common
mountPath: /opt/nuclio/common/openvino
build:
image: cvat.openvino.omz.intel.person-reidentification-retail-0300
baseImage: openvino/ubuntu18_dev:2020.2
directives:
preCopy:
- kind: USER
value: root
- kind: WORKDIR
value: /opt/nuclio
- kind: RUN
value: ln -s /usr/bin/pip3 /usr/bin/pip
- kind: RUN
value: /opt/intel/openvino/deployment_tools/open_model_zoo/tools/downloader/downloader.py --name person-reidentification-retail-0300 -o /opt/nuclio/open_model_zoo
- kind: RUN
value: /opt/intel/openvino/deployment_tools/open_model_zoo/tools/downloader/converter.py --name person-reidentification-retail-0300 --precisions FP32 -d /opt/nuclio/open_model_zoo -o /opt/nuclio/open_model_zoo
image: cvat.openvino.omz.intel.person-reidentification-retail-0277
baseImage: cvat.openvino.omz.intel.person-reidentification-retail-0277.base
triggers:
myHttpTrigger:
......
......@@ -13,7 +13,7 @@ def init_context(context):
context.logger.info("Init context...100%")
def handler(context, event):
context.logger.info("Run person-reidentification-retail-0300 model")
context.logger.info("Run person-reidentification-retail-0277 model")
data = event.body
buf0 = io.BytesIO(base64.b64decode(data["image0"]))
buf1 = io.BytesIO(base64.b64decode(data["image1"]))
......
......@@ -13,9 +13,9 @@ from model_loader import ModelLoader
class ModelHandler:
def __init__(self):
base_dir = os.path.abspath(os.environ.get("MODEL_PATH",
"/opt/nuclio/open_model_zoo/intel/person-reidentification-retail-0300/FP32"))
model_xml = os.path.join(base_dir, "person-reidentification-retail-0300.xml")
model_bin = os.path.join(base_dir, "person-reidentification-retail-0300.bin")
"/opt/nuclio/open_model_zoo/intel/person-reidentification-retail-0277/FP32"))
model_xml = os.path.join(base_dir, "person-reidentification-retail-0277.xml")
model_bin = os.path.join(base_dir, "person-reidentification-retail-0277.bin")
self.model = ModelLoader(model_xml, model_bin)
......
FROM openvino/ubuntu20_dev:2022.3.0 AS build
USER root
RUN omz_downloader \
--name semantic-segmentation-adas-0001 \
--precisions FP32 \
-o /opt/nuclio/open_model_zoo
FROM cvat.openvino.base
COPY --from=build --chown=root:root /opt/nuclio/open_model_zoo /opt/nuclio/open_model_zoo
metadata:
name: openvino-omz-semantic-segmentation-adas-0001
name: openvino-omz-intel-semantic-segmentation-adas-0001
namespace: cvat
annotations:
name: Semantic segmentation for ADAS
......@@ -32,43 +32,13 @@ metadata:
spec:
description: Segmentation network to classify each pixel into typical 20 classes for ADAS
runtime: 'python:3.6'
runtime: 'python:3.8'
handler: main:handler
eventTimeout: 30s
env:
- name: NUCLIO_PYTHON_EXE_PATH
value: /opt/nuclio/common/openvino/python3
volumes:
- volume:
name: openvino-common
configMap:
name: "cvat-nuclio-openvino-common"
defaultMode: 0750
volumeMount:
name: openvino-common
mountPath: /opt/nuclio/common/openvino
build:
image: cvat.openvino.omz.intel.semantic-segmentation-adas-0001
baseImage: openvino/ubuntu18_dev:2020.2
directives:
preCopy:
- kind: USER
value: root
- kind: WORKDIR
value: /opt/nuclio
- kind: RUN
value: ln -s /usr/bin/pip3 /usr/bin/pip
- kind: RUN
value: /opt/intel/openvino/deployment_tools/open_model_zoo/tools/downloader/downloader.py --name semantic-segmentation-adas-0001 -o /opt/nuclio/open_model_zoo
postCopy:
- kind: RUN
value: apt update && DEBIAN_FRONTEND=noninteractive apt install --no-install-recommends -y python3-skimage
- kind: RUN
value: pip3 install "numpy<1.16.0" # workaround for skimage
baseImage: cvat.openvino.omz.intel.semantic-segmentation-adas-0001.base
triggers:
myHttpTrigger:
......
FROM openvino/ubuntu20_dev:2022.3.0 AS build
USER root
RUN omz_downloader \
--name text-detection-0004 \
--precisions FP32 \
-o /opt/nuclio/open_model_zoo
FROM cvat.openvino.base
COPY --from=build --chown=root:root /opt/nuclio/open_model_zoo /opt/nuclio/open_model_zoo
......@@ -12,37 +12,13 @@ metadata:
spec:
description: Text detector based on PixelLink architecture with MobileNetV2-like as a backbone for indoor/outdoor scenes.
runtime: 'python:3.6'
runtime: 'python:3.8'
handler: main:handler
eventTimeout: 30s
env:
- name: NUCLIO_PYTHON_EXE_PATH
value: /opt/nuclio/common/openvino/python3
volumes:
- volume:
name: openvino-common
configMap:
name: "cvat-nuclio-openvino-common"
defaultMode: 0750
volumeMount:
name: openvino-common
mountPath: /opt/nuclio/common/openvino
build:
image: cvat.openvino.omz.intel.text-detection-0004
baseImage: openvino/ubuntu18_dev:2020.2
directives:
preCopy:
- kind: USER
value: root
- kind: WORKDIR
value: /opt/nuclio
- kind: RUN
value: ln -s /usr/bin/pip3 /usr/bin/pip
- kind: RUN
value: /opt/intel/openvino/deployment_tools/open_model_zoo/tools/downloader/downloader.py --name text-detection-0004 -o /opt/nuclio/open_model_zoo
baseImage: cvat.openvino.omz.intel.text-detection-0004.base
triggers:
myHttpTrigger:
......
FROM openvino/ubuntu20_dev:2022.3.0 AS build
USER root
RUN omz_downloader \
--name faster_rcnn_inception_resnet_v2_atrous_coco \
-o /opt/nuclio/open_model_zoo.orig
RUN omz_converter \
--name faster_rcnn_inception_resnet_v2_atrous_coco \
--precisions FP32 \
-d /opt/nuclio/open_model_zoo.orig \
-o /opt/nuclio/open_model_zoo
FROM cvat.openvino.base
COPY --from=build --chown=root:root /opt/nuclio/open_model_zoo /opt/nuclio/open_model_zoo
metadata:
name: openvino-omz-public-faster-rcnn-inception-v2-coco
name: openvino-omz-public-faster-rcnn-inception-resnet-v2-atrous-coco
namespace: cvat
annotations:
name: Faster RCNN
......@@ -90,40 +90,14 @@ metadata:
]
spec:
description: Faster RCNN inception v2 COCO via Intel OpenVINO toolkit
runtime: 'python:3.6'
description: Faster R-CNN with Inception ResNet v2 Atrous via Intel OpenVINO toolkit
runtime: 'python:3.8'
handler: main:handler
eventTimeout: 30s
env:
- name: NUCLIO_PYTHON_EXE_PATH
value: /opt/nuclio/common/openvino/python3
volumes:
- volume:
name: openvino-common
configMap:
name: "cvat-nuclio-openvino-common"
defaultMode: 0750
volumeMount:
name: openvino-common
mountPath: /opt/nuclio/common/openvino
build:
image: cvat.openvino.omz.public.faster_rcnn_inception_v2_coco
baseImage: openvino/ubuntu18_dev:2020.2
directives:
preCopy:
- kind: USER
value: root
- kind: WORKDIR
value: /opt/nuclio
- kind: RUN
value: ln -s /usr/bin/pip3 /usr/bin/pip
- kind: RUN
value: /opt/intel/openvino/deployment_tools/open_model_zoo/tools/downloader/downloader.py --name faster_rcnn_inception_v2_coco -o /opt/nuclio/open_model_zoo
- kind: RUN
value: /opt/intel/openvino/deployment_tools/open_model_zoo/tools/downloader/converter.py --name faster_rcnn_inception_v2_coco --precisions FP32 -d /opt/nuclio/open_model_zoo -o /opt/nuclio/open_model_zoo
image: cvat.openvino.omz.public.faster_rcnn_inception_resnet_v2_atrous_coco
baseImage: cvat.openvino.omz.public.faster_rcnn_inception_resnet_v2_atrous_coco.base
triggers:
myHttpTrigger:
......
......@@ -22,7 +22,7 @@ def init_context(context):
context.logger.info("Init context...100%")
def handler(context, event):
context.logger.info("Run faster_rcnn_inception_v2_coco model")
context.logger.info("Run faster_rcnn_inception_resnet_v2_atrous_coco model")
data = event.body
buf = io.BytesIO(base64.b64decode(data["image"]))
threshold = float(data.get("threshold", 0.5))
......
......@@ -8,9 +8,9 @@ from model_loader import ModelLoader
class ModelHandler:
def __init__(self, labels):
base_dir = os.path.abspath(os.environ.get("MODEL_PATH",
"/opt/nuclio/open_model_zoo/public/faster_rcnn_inception_v2_coco/FP32"))
model_xml = os.path.join(base_dir, "faster_rcnn_inception_v2_coco.xml")
model_bin = os.path.join(base_dir, "faster_rcnn_inception_v2_coco.bin")
"/opt/nuclio/open_model_zoo/public/faster_rcnn_inception_resnet_v2_atrous_coco/FP32"))
model_xml = os.path.join(base_dir, "faster_rcnn_inception_resnet_v2_atrous_coco.xml")
model_bin = os.path.join(base_dir, "faster_rcnn_inception_resnet_v2_atrous_coco.bin")
self.model = ModelLoader(model_xml, model_bin)
self.labels = labels
......@@ -36,4 +36,4 @@ class ModelHandler:
"type": "rectangle",
})
return results
\ No newline at end of file
return results
FROM openvino/ubuntu20_dev:2022.3.0 AS build
USER root
RUN omz_downloader \
--name mask_rcnn_inception_resnet_v2_atrous_coco \
-o /opt/nuclio/open_model_zoo.orig
RUN omz_converter \
--name mask_rcnn_inception_resnet_v2_atrous_coco \
--precisions FP32 \
-d /opt/nuclio/open_model_zoo.orig \
-o /opt/nuclio/open_model_zoo
FROM cvat.openvino.base
USER root
RUN pip install --no-cache-dir scikit-image
COPY --from=build /opt/nuclio/open_model_zoo /opt/nuclio/open_model_zoo
USER openvino
......@@ -2,7 +2,7 @@
# have enough memory (more than 4GB). Look here how to do that
# https://stackoverflow.com/questions/44417159/docker-process-killed-with-cryptic-killed-message
metadata:
name: openvino-mask-rcnn-inception-resnet-v2-atrous-coco
name: openvino-omz-public-mask-rcnn-inception-resnet-v2-atrous-coco
namespace: cvat
annotations:
name: Mask RCNN
......@@ -94,45 +94,13 @@ metadata:
spec:
description: Mask RCNN inception resnet v2 COCO via Intel OpenVINO
runtime: 'python:3.6'
runtime: 'python:3.8'
handler: main:handler
eventTimeout: 60s
env:
- name: NUCLIO_PYTHON_EXE_PATH
value: /opt/nuclio/common/openvino/python3
volumes:
- volume:
name: openvino-common
configMap:
name: "cvat-nuclio-openvino-common"
defaultMode: 0750
volumeMount:
name: openvino-common
mountPath: /opt/nuclio/common/openvino
build:
image: cvat.openvino.omz.public.mask_rcnn_inception_resnet_v2_atrous_coco
baseImage: openvino/ubuntu18_dev:2020.2
directives:
preCopy:
- kind: USER
value: root
- kind: WORKDIR
value: /opt/nuclio
- kind: RUN
value: ln -s /usr/bin/pip3 /usr/bin/pip
- kind: RUN
value: /opt/intel/openvino/deployment_tools/open_model_zoo/tools/downloader/downloader.py --name mask_rcnn_inception_resnet_v2_atrous_coco -o /opt/nuclio/open_model_zoo
- kind: RUN
value: /opt/intel/openvino/deployment_tools/open_model_zoo/tools/downloader/converter.py --name mask_rcnn_inception_resnet_v2_atrous_coco --precisions FP32 -d /opt/nuclio/open_model_zoo -o /opt/nuclio/open_model_zoo
postCopy:
- kind: RUN
value: apt update && DEBIAN_FRONTEND=noninteractive apt install --no-install-recommends -y python3-skimage
- kind: RUN
value: pip3 install "numpy<1.16.0" # workaround for skimage
baseImage: cvat.openvino.omz.public.mask_rcnn_inception_resnet_v2_atrous_coco.base
triggers:
myHttpTrigger:
......
FROM openvino/ubuntu20_dev:2022.3.0 AS build
USER root
RUN omz_downloader \
--name yolo-v3-tf \
-o /opt/nuclio/open_model_zoo.orig
RUN omz_converter \
--name yolo-v3-tf \
--precisions FP32 \
-d /opt/nuclio/open_model_zoo.orig \
-o /opt/nuclio/open_model_zoo
FROM cvat.openvino.base
COPY --from=build --chown=root:root /opt/nuclio/open_model_zoo /opt/nuclio/open_model_zoo
......@@ -91,39 +91,13 @@ metadata:
spec:
description: YOLO v3 via Intel OpenVINO
runtime: 'python:3.6'
runtime: 'python:3.8'
handler: main:handler
eventTimeout: 30s
env:
- name: NUCLIO_PYTHON_EXE_PATH
value: /opt/nuclio/common/openvino/python3
volumes:
- volume:
name: openvino-common
configMap:
name: "cvat-nuclio-openvino-common"
defaultMode: 0750
volumeMount:
name: openvino-common
mountPath: /opt/nuclio/common/openvino
build:
image: cvat/openvino.omz.public.yolo-v3-tf
baseImage: openvino/ubuntu18_dev:2020.2
directives:
preCopy:
- kind: USER
value: root
- kind: WORKDIR
value: /opt/nuclio
- kind: RUN
value: ln -s /usr/bin/pip3 /usr/bin/pip
- kind: RUN
value: /opt/intel/openvino/deployment_tools/open_model_zoo/tools/downloader/downloader.py --name yolo-v3-tf -o /opt/nuclio/open_model_zoo
- kind: RUN
value: /opt/intel/openvino/deployment_tools/open_model_zoo/tools/downloader/converter.py --name yolo-v3-tf --precisions FP32 -d /opt/nuclio/open_model_zoo -o /opt/nuclio/open_model_zoo
image: cvat.openvino.omz.public.yolo-v3-tf
baseImage: cvat.openvino.omz.public.yolo-v3-tf.base
triggers:
myHttpTrigger:
......
......@@ -4,24 +4,26 @@
import os
from math import exp
import ngraph
from model_loader import ModelLoader
class YoloParams:
# ------------------------------------------- Extracting layer parameters ------------------------------------------
# Magic numbers are copied from yolo samples
def __init__(self, param, side):
self.num = 3 if 'num' not in param else int(param['num'])
self.coords = 4 if 'coords' not in param else int(param['coords'])
self.classes = 80 if 'classes' not in param else int(param['classes'])
self.num = param.get('num', 3)
self.coords = param.get('coords', 4)
self.classes = param.get('classes', 80)
self.side = side
self.anchors = [10.0, 13.0, 16.0, 30.0, 33.0, 23.0, 30.0, 61.0, 62.0, 45.0, 59.0, 119.0, 116.0, 90.0, 156.0,
198.0,
373.0, 326.0] if 'anchors' not in param else [float(a) for a in param['anchors'].split(',')]
self.anchors = param.get('anchors', [
10.0, 13.0, 16.0, 30.0, 33.0, 23.0, 30.0, 61.0, 62.0, 45.0, 59.0,
119.0, 116.0, 90.0, 156.0, 198.0, 373.0, 326.0])
self.isYoloV3 = False
if param.get('mask'):
mask = [int(idx) for idx in param['mask'].split(',')]
if mask := param.get('mask'):
self.num = len(mask)
maskedAnchors = []
......@@ -119,6 +121,19 @@ class ModelHandler:
self.model = ModelLoader(model_xml, model_bin)
self.labels = labels
ng_func = ngraph.function_from_cnn(self.model.network)
self.output_info = {}
for node in ng_func.get_ordered_ops():
layer_name = node.get_friendly_name()
if layer_name not in self.model.network.outputs:
continue
parent_node = node.inputs()[0].get_source_output().get_node()
shape = list(parent_node.shape)
yolo_params = YoloParams(node._get_attributes(), shape[2])
self.output_info[layer_name] = (shape, yolo_params)
def infer(self, image, threshold):
output_layer = self.model.infer(image)
......@@ -126,10 +141,10 @@ class ModelHandler:
objects = []
origin_im_size = (image.height, image.width)
for layer_name, out_blob in output_layer.items():
out_blob = out_blob.reshape(self.model.layers[self.model.layers[layer_name].parents[0]].shape)
layer_params = YoloParams(self.model.layers[layer_name].params, out_blob.shape[2])
shape, yolo_params = self.output_info[layer_name]
out_blob = out_blob.reshape(shape)
objects += parse_yolo_region(out_blob, self.model.input_size(),
origin_im_size, layer_params, threshold)
origin_im_size, yolo_params, threshold)
# Filtering overlapping boxes (non-maximum suppression)
IOU_THRESHOLD = 0.4
......@@ -157,4 +172,4 @@ class ModelHandler:
"type": "rectangle",
})
return results
\ No newline at end of file
return results
......@@ -45,33 +45,16 @@ description: 'Information about the installation of components needed for semi-a
sudo ln -sf $(pwd)/nuctl-<version>-linux-amd64 /usr/local/bin/nuctl
```
- Create `cvat` project inside nuclio dashboard where you will deploy new serverless functions
and deploy a couple of DL models. Commands below should be run only after CVAT has been installed
- Deploy a couple of functions.
This will automatically create a `cvat` Nuclio project to contain the functions.
Commands below should be run only after CVAT has been installed
using `docker compose` because it runs nuclio dashboard which manages all serverless functions.
```bash
nuctl create project cvat
./serverless/deploy_cpu.sh serverless/openvino/dextr
./serverless/deploy_cpu.sh serverless/openvino/omz/public/yolo-v3-tf
```
```bash
nuctl deploy --project-name cvat \
--path serverless/openvino/dextr/nuclio \
--volume `pwd`/serverless/common:/opt/nuclio/common \
--platform local
```
```bash
nuctl deploy --project-name cvat \
--path serverless/openvino/omz/public/yolo-v3-tf/nuclio \
--volume `pwd`/serverless/common:/opt/nuclio/common \
--platform local
```
**Note:**
- See [deploy_cpu.sh](https://github.com/cvat-ai/cvat/blob/develop/serverless/deploy_cpu.sh)
for more examples.
#### GPU Support
You will need to install [Nvidia Container Toolkit](https://www.tensorflow.org/install/docker#gpu_support).
......
......@@ -15,69 +15,14 @@ Follow this [guide](/docs/administration/advanced/installation_automatic_annotat
of the Nuclio dashboard. All you need in order to run the dashboard is Docker. See
[nuclio documentation](https://github.com/nuclio/nuclio#quick-start-steps)
for more details.
- Create `cvat` project inside nuclio dashboard where you will deploy new
serverless functions and deploy a couple of DL models.
- Deploy a couple of functions.
This will automatically create a `cvat` Nuclio project to contain the functions.
```bash
nuctl create project cvat
./serverless/deploy_cpu.sh serverless/openvino/dextr
./serverless/deploy_cpu.sh serverless/openvino/omz/public/yolo-v3-tf
```
```bash
nuctl deploy --project-name cvat \
--path serverless/openvino/dextr/nuclio \
--volume `pwd`/serverless/common:/opt/nuclio/common \
--platform local
```
<details>
```
20.07.17 12:02:23.247 nuctl (I) Deploying function {"name": ""}
20.07.17 12:02:23.248 nuctl (I) Building {"versionInfo": "Label: 1.4.8, Git commit: 238d4539ac7783896d6c414535d0462b5f4cbcf1, OS: darwin, Arch: amd64, Go version: go1.14.3", "name": ""}
20.07.17 12:02:23.447 nuctl (I) Cleaning up before deployment
20.07.17 12:02:23.535 nuctl (I) Function already exists, deleting
20.07.17 12:02:25.877 nuctl (I) Staging files and preparing base images
20.07.17 12:02:25.891 nuctl (I) Building processor image {"imageName": "cvat/openvino.dextr:latest"}
20.07.17 12:02:25.891 nuctl.platform.docker (I) Pulling image {"imageName": "quay.io/nuclio/handler-builder-python-onbuild:1.4.8-amd64"}
20.07.17 12:02:29.270 nuctl.platform.docker (I) Pulling image {"imageName": "quay.io/nuclio/uhttpc:0.0.1-amd64"}
20.07.17 12:02:33.208 nuctl.platform (I) Building docker image {"image": "cvat/openvino.dextr:latest"}
20.07.17 12:02:34.464 nuctl.platform (I) Pushing docker image into registry {"image": "cvat/openvino.dextr:latest", "registry": ""}
20.07.17 12:02:34.464 nuctl.platform (I) Docker image was successfully built and pushed into docker registry {"image": "cvat/openvino.dextr:latest"}
20.07.17 12:02:34.464 nuctl (I) Build complete {"result": {"Image":"cvat/openvino.dextr:latest","UpdatedFunctionConfig":{"metadata":{"name":"openvino.dextr","namespace":"nuclio","labels":{"nuclio.io/project-name":"cvat"},"annotations":{"framework":"openvino","spec":"","type":"interactor"}},"spec":{"description":"Deep Extreme Cut","handler":"main:handler","runtime":"python:3.6","env":[{"name":"NUCLIO_PYTHON_EXE_PATH","value":"/opt/nuclio/python3"}],"resources":{},"image":"cvat/openvino.dextr:latest","targetCPU":75,"triggers":{"myHttpTrigger":{"class":"","kind":"http","name":"","maxWorkers":2,"workerAvailabilityTimeoutMilliseconds":10000,"attributes":{"maxRequestBodySize":33554432}}},"volumes":[{"volume":{"name":"volume-1","hostPath":{"path":"/Users/nmanovic/Workspace/cvat/serverless/openvino/common"}},"volumeMount":{"name":"volume-1","mountPath":"/opt/nuclio/common"}}],"build":{"image":"cvat/openvino.dextr","baseImage":"openvino/ubuntu18_runtime:2020.2","directives":{"postCopy":[{"kind":"RUN","value":"curl -O https://download.01.org/openvinotoolkit/models_contrib/cvat/dextr_model_v1.zip"},{"kind":"RUN","value":"unzip dextr_model_v1.zip"},{"kind":"RUN","value":"pip3 install Pillow"},{"kind":"USER","value":"openvino"}],"preCopy":[{"kind":"USER","value":"root"},{"kind":"WORKDIR","value":"/opt/nuclio"},{"kind":"RUN","value":"ln -s /usr/bin/pip3 /usr/bin/pip"}]},"codeEntryType":"image"},"platform":{},"readinessTimeoutSeconds":60,"eventTimeout":"30s"}}}}
20.07.17 12:02:35.012 nuctl.platform (I) Waiting for function to be ready {"timeout": 60}
20.07.17 12:02:37.448 nuctl (I) Function deploy complete {"httpPort": 55274}
```
</details>
```bash
nuctl deploy --project-name cvat \
--path serverless/openvino/omz/public/yolo-v3-tf/nuclio \
--volume `pwd`/serverless/common:/opt/nuclio/common \
--platform local
```
<details>
```
20.07.17 12:05:23.377 nuctl (I) Deploying function {"name": ""}
20.07.17 12:05:23.378 nuctl (I) Building {"versionInfo": "Label: 1.4.8, Git commit: 238d4539ac7783896d6c414535d0462b5f4cbcf1, OS: darwin, Arch: amd64, Go version: go1.14.3", "name": ""}
20.07.17 12:05:23.590 nuctl (I) Cleaning up before deployment
20.07.17 12:05:23.694 nuctl (I) Function already exists, deleting
20.07.17 12:05:24.271 nuctl (I) Staging files and preparing base images
20.07.17 12:05:24.274 nuctl (I) Building processor image {"imageName": "cvat/openvino.omz.public.yolo-v3-tf:latest"}
20.07.17 12:05:24.274 nuctl.platform.docker (I) Pulling image {"imageName": "quay.io/nuclio/handler-builder-python-onbuild:1.4.8-amd64"}
20.07.17 12:05:27.432 nuctl.platform.docker (I) Pulling image {"imageName": "quay.io/nuclio/uhttpc:0.0.1-amd64"}
20.07.17 12:05:31.462 nuctl.platform (I) Building docker image {"image": "cvat/openvino.omz.public.yolo-v3-tf:latest"}
20.07.17 12:05:32.798 nuctl.platform (I) Pushing docker image into registry {"image": "cvat/openvino.omz.public.yolo-v3-tf:latest", "registry": ""}
20.07.17 12:05:32.798 nuctl.platform (I) Docker image was successfully built and pushed into docker registry {"image": "cvat/openvino.omz.public.yolo-v3-tf:latest"}
20.07.17 12:05:32.798 nuctl (I) Build complete {"result": {"Image":"cvat/openvino.omz.public.yolo-v3-tf:latest","UpdatedFunctionConfig":{"metadata":{"name":"openvino.omz.public.yolo-v3-tf","namespace":"nuclio","labels":{"nuclio.io/project-name":"cvat"},"annotations":{"framework":"openvino","name":"YOLO v3","spec":"[\n { \"id\": 0, \"name\": \"person\" },\n { \"id\": 1, \"name\": \"bicycle\" },\n { \"id\": 2, \"name\": \"car\" },\n { \"id\": 3, \"name\": \"motorbike\" },\n { \"id\": 4, \"name\": \"aeroplane\" },\n { \"id\": 5, \"name\": \"bus\" },\n { \"id\": 6, \"name\": \"train\" },\n { \"id\": 7, \"name\": \"truck\" },\n { \"id\": 8, \"name\": \"boat\" },\n { \"id\": 9, \"name\": \"traffic light\" },\n { \"id\": 10, \"name\": \"fire hydrant\" },\n { \"id\": 11, \"name\": \"stop sign\" },\n { \"id\": 12, \"name\": \"parking meter\" },\n { \"id\": 13, \"name\": \"bench\" },\n { \"id\": 14, \"name\": \"bird\" },\n { \"id\": 15, \"name\": \"cat\" },\n { \"id\": 16, \"name\": \"dog\" },\n { \"id\": 17, \"name\": \"horse\" },\n { \"id\": 18, \"name\": \"sheep\" },\n { \"id\": 19, \"name\": \"cow\" },\n { \"id\": 20, \"name\": \"elephant\" },\n { \"id\": 21, \"name\": \"bear\" },\n { \"id\": 22, \"name\": \"zebra\" },\n { \"id\": 23, \"name\": \"giraffe\" },\n { \"id\": 24, \"name\": \"backpack\" },\n { \"id\": 25, \"name\": \"umbrella\" },\n { \"id\": 26, \"name\": \"handbag\" },\n { \"id\": 27, \"name\": \"tie\" },\n { \"id\": 28, \"name\": \"suitcase\" },\n { \"id\": 29, \"name\": \"frisbee\" },\n { \"id\": 30, \"name\": \"skis\" },\n { \"id\": 31, \"name\": \"snowboard\" },\n { \"id\": 32, \"name\": \"sports ball\" },\n { \"id\": 33, \"name\": \"kite\" },\n { \"id\": 34, \"name\": \"baseball bat\" },\n { \"id\": 35, \"name\": \"baseball glove\" },\n { \"id\": 36, \"name\": \"skateboard\" },\n { \"id\": 37, \"name\": \"surfboard\" },\n { \"id\": 38, \"name\": \"tennis racket\" },\n { \"id\": 39, \"name\": \"bottle\" },\n { \"id\": 40, \"name\": \"wine glass\" },\n { \"id\": 41, \"name\": \"cup\" },\n { \"id\": 42, \"name\": \"fork\" },\n { \"id\": 43, \"name\": \"knife\" },\n { \"id\": 44, \"name\": \"spoon\" },\n { \"id\": 45, \"name\": \"bowl\" },\n { \"id\": 46, \"name\": \"banana\" },\n { \"id\": 47, \"name\": \"apple\" },\n { \"id\": 48, \"name\": \"sandwich\" },\n { \"id\": 49, \"name\": \"orange\" },\n { \"id\": 50, \"name\": \"broccoli\" },\n { \"id\": 51, \"name\": \"carrot\" },\n { \"id\": 52, \"name\": \"hot dog\" },\n { \"id\": 53, \"name\": \"pizza\" },\n { \"id\": 54, \"name\": \"donut\" },\n { \"id\": 55, \"name\": \"cake\" },\n { \"id\": 56, \"name\": \"chair\" },\n { \"id\": 57, \"name\": \"sofa\" },\n { \"id\": 58, \"name\": \"pottedplant\" },\n { \"id\": 59, \"name\": \"bed\" },\n { \"id\": 60, \"name\": \"diningtable\" },\n { \"id\": 61, \"name\": \"toilet\" },\n { \"id\": 62, \"name\": \"tvmonitor\" },\n { \"id\": 63, \"name\": \"laptop\" },\n { \"id\": 64, \"name\": \"mouse\" },\n { \"id\": 65, \"name\": \"remote\" },\n { \"id\": 66, \"name\": \"keyboard\" },\n { \"id\": 67, \"name\": \"cell phone\" },\n { \"id\": 68, \"name\": \"microwave\" },\n { \"id\": 69, \"name\": \"oven\" },\n { \"id\": 70, \"name\": \"toaster\" },\n { \"id\": 71, \"name\": \"sink\" },\n { \"id\": 72, \"name\": \"refrigerator\" },\n { \"id\": 73, \"name\": \"book\" },\n { \"id\": 74, \"name\": \"clock\" },\n { \"id\": 75, \"name\": \"vase\" },\n { \"id\": 76, \"name\": \"scissors\" },\n { \"id\": 77, \"name\": \"teddy bear\" },\n { \"id\": 78, \"name\": \"hair drier\" },\n { \"id\": 79, \"name\": \"toothbrush\" }\n]\n","type":"detector"}},"spec":{"description":"YOLO v3 via Intel OpenVINO","handler":"main:handler","runtime":"python:3.6","env":[{"name":"NUCLIO_PYTHON_EXE_PATH","value":"/opt/nuclio/common/python3"}],"resources":{},"image":"cvat/openvino.omz.public.yolo-v3-tf:latest","targetCPU":75,"triggers":{"myHttpTrigger":{"class":"","kind":"http","name":"","maxWorkers":2,"workerAvailabilityTimeoutMilliseconds":10000,"attributes":{"maxRequestBodySize":33554432}}},"volumes":[{"volume":{"name":"volume-1","hostPath":{"path":"/Users/nmanovic/Workspace/cvat/serverless/openvino/common"}},"volumeMount":{"name":"volume-1","mountPath":"/opt/nuclio/common"}}],"build":{"image":"cvat/openvino.omz.public.yolo-v3-tf","baseImage":"openvino/ubuntu18_dev:2020.2","directives":{"postCopy":[{"kind":"USER","value":"openvino"}],"preCopy":[{"kind":"USER","value":"root"},{"kind":"WORKDIR","value":"/opt/nuclio"},{"kind":"RUN","value":"ln -s /usr/bin/pip3 /usr/bin/pip"},{"kind":"RUN","value":"/opt/intel/openvino/deployment_tools/open_model_zoo/tools/downloader/downloader.py --name yolo-v3-tf -o /opt/nuclio/open_model_zoo"},{"kind":"RUN","value":"/opt/intel/openvino/deployment_tools/open_model_zoo/tools/downloader/converter.py --name yolo-v3-tf --precisions FP32 -d /opt/nuclio/open_model_zoo -o /opt/nuclio/open_model_zoo"}]},"codeEntryType":"image"},"platform":{},"readinessTimeoutSeconds":60,"eventTimeout":"30s"}}}}
20.07.17 12:05:33.285 nuctl.platform (I) Waiting for function to be ready {"timeout": 60}
20.07.17 12:05:35.452 nuctl (I) Function deploy complete {"httpPort": 57308}
```
</details>
- Display a list of running serverless functions using `nuctl` command or see them
in nuclio dashboard:
......@@ -103,23 +48,23 @@ image=$(curl https://upload.wikimedia.org/wikipedia/en/7/7d/Lenna_%28test_image%
cat << EOF > /tmp/input.json
{"image": "$image"}
EOF
cat /tmp/input.json | nuctl invoke openvino.omz.public.yolo-v3-tf -c 'application/json'
cat /tmp/input.json | nuctl invoke openvino-omz-public-yolo-v3-tf -c 'application/json'
```
<details>
```
20.07.17 12:07:44.519 nuctl.platform.invoker (I) Executing function {"method": "POST", "url": "http://:57308", "headers": {"Content-Type":["application/json"],"X-Nuclio-Log-Level":["info"],"X-Nuclio-Target":["openvino.omz.public.yolo-v3-tf"]}}
20.07.17 12:07:45.275 nuctl.platform.invoker (I) Got response {"status": "200 OK"}
20.07.17 12:07:45.275 nuctl (I) >>> Start of function logs
20.07.17 12:07:45.275 ino.omz.public.yolo-v3-tf (I) Run yolo-v3-tf model {"worker_id": "0", "time": 1594976864570.9353}
20.07.17 12:07:45.275 nuctl (I) <<< End of function logs
23.05.11 22:14:17.275 nuctl.platform.invoker (I) Executing function {"method": "POST", "url": "http://0.0.0.0:32771", "bodyLength": 631790, "headers": {"Content-Type":["application/json"],"X-Nuclio-Log-Level":["info"],"X-Nuclio-Target":["openvino-omz-public-yolo-v3-tf"]}}
23.05.11 22:14:17.788 nuctl.platform.invoker (I) Got response {"status": "200 OK"}
23.05.11 22:14:17.789 nuctl (I) >>> Start of function logs
23.05.11 22:14:17.789 ino-omz-public-yolo-v3-tf (I) Run yolo-v3-tf model {"worker_id": "0", "time": 1683828857301.8765}
23.05.11 22:14:17.789 nuctl (I) <<< End of function logs
> Response headers:
Date = Fri, 17 Jul 2020 09:07:45 GMT
Server = nuclio
Date = Thu, 11 May 2023 18:14:17 GMT
Content-Type = application/json
Content-Length = 100
Server = nuclio
> Response body:
[
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册