This article describes how to bring a custom model (and parser) to an Azure DeepStream Accelerator solution.
In this tutorial you'll learn how to use the included CLI tool to:
- Create a customer DeepStream Docker image
- Push the Docker image to Azure Container Registry
After the model container is uploaded to the cloud, you will follow steps that are similar to those in previous paths to complete and verify the solution.
- Review the prerequisites section in the Quickstart article
This path consists of eight steps. Most of them are identical to the steps in the Prebuilt model path.
- Prepare your model and parser
- Confirm the edge modules are running
- Identify your RTSP source
- Prepare and upload your container
- Update the deployment manifest
- Deploy your updates to the edge device
- Modify regions of interest
- Use the Player web app to verify results
AI Models output a list of numbers - and if you are bringing your own custom model, we don't know how to interpret those numbers, so you will need to bring a custom parser. This step will show you how to do that.
In pure DeepStream, Python parsing has limited support. If you want to bring a custom parser using the traditional DeepStream mechanism, you may still do so by compiling a .so file and specifying the appropriate file and function in the configuration file. If this is the route you wish to go with, please see the appropriate documentation. The remainder of this Step will focus on Python parsers.
To implement a Python parser, you need to bring a Python source file with code that adheres to a particular API so that we can stitch it into the DeepStream Gstreamer pipeline and interpret the outputs.
A DeepStream pipeline in Azure DeepStream Accelerator consists of one or more models in a cascade; the first model is the so-called "primary" model, and any that follow are called "secondary" models.
This section describes how to implement a custom parser for a primary model.
Depending on your needs, it is necessary to implement the following functions:
parse_det_model()
parse_custom_model()
add_custom_to_meta()
Here is a template that you can use for implementing a custom primary parser:
# import the packages you need
# if you need packages not available in the container by default, add them using the CLI tool
# mandatory
model_type = 0 # 0-detection model, 1-custom model
name = "MySSDParser"
# mandatory if model_type=0
labels = ["none", "person"]
# mandatory if model_type=0
def parse_det_model(config, raw_outputs: dict):
"""
This is used if the model is a *detection model*.
Parameters
----------
config.image_size : tuple
Frame size, Ex.: (1920, 1080)
raw_outputs : dict
Dictionary containing the model's outputs.
Ex.: bboxes = raw_outputs["output:01"]
Returns
-------
bboxes : Nx4 numpy array
Bounding boxes in the format (x,y,w,h)
labels : One dimensional numpy array of N integer numbers
The labes id
scores : One dimensional numpy array of N float numbers
The detection scores
message : Any json serializable
A custom message, it can be None
"""
# your code should determine 'bboxes', 'labels', 'scores' and 'message'
return bboxes, labels, scores, message
# mandatory if model_type=1
def parse_custom_model(config, raw_outputs: dict):
"""
This is used for all other types of models.
Parameters
----------
config.image_size : tuple
Frame size, Ex.: (1920, 1080)
raw_outputs : dict
Dictionary containing the model's outputs.
Ex.: bboxes = raw_outputs["output:01"]
Returns
-------
data : Any
If `data` is not None, it will be passed to the function
add_custom_to_meta()
message : Any json serializable
A custom message, it can be None
"""
# your code should determine 'data' and 'message'
return data, message
# optional if model_type=1
def add_custom_to_meta(self, data, batch_meta, frame_meta):
"""
Parameters
----------
data : Any
The first output from function `parse_custom_model()`
batch_meta : pyds.NvDsBatchMeta
frame_meta : pyds.NvDsFrameMeta
"""
# modify the DS metadata based on the output of your custom model (`data`)
pass
import numpy as np
import ssd_utils as UTILS
model_type=0
name="SSDMobilenetV1Parser"
labels=UTILS.get_labels()
def parse_det_model(config, raw_outputs: dict):
try:
num_detection_layer = raw_outputs["num_detections:0"]
score_layer = raw_outputs["detection_scores:0"]
class_layer = raw_outputs["detection_classes:0"]
box_layer = raw_outputs["detection_boxes:0"]
except:
print("{}. Error: some layers missing in output tensors".format(name))
return [], [], [], None
num_detection = int(num_detection_layer[0])
scores = score_layer[:num_detection]
classes = class_layer[:num_detection].astype('int')
boxes = box_layer[:num_detection, :].clip(0, 1)
# apply NMS
boxes, scores, classes = UTILS.nms(boxes, scores, classes, 0.2)
w, h = config.image_size
# convert frames to (x,y,w,h) format and scale them to frame size
boxes = UTILS.to_xywh(boxes)*np.array([w, h, w, h])
message="Number of objects detected: {}".format(len(scores))
return bboxes, labels, scores, message
Note that when the function parse_det_model()
is used (model_type=0
), the metadata corresponding to the detection is added automatically.
If you want to have more personalized control of this you can use the parse_custom_model()
and add_custom_to_meta()
functions,
in which case you should set model_type=1
.
import numpy as np
import ssd_utils as UTILS
model_type=1
name="SSDMobilenetV1Parser"
def parse_custom_model(config, raw_outputs: dict):
try:
num_detection_layer = raw_outputs["num_detections:0"]
score_layer = raw_outputs["detection_scores:0"]
class_layer = raw_outputs["detection_classes:0"]
box_layer = raw_outputs["detection_boxes:0"]
except:
print("{}. Error: some layers missing in output tensors".format(name))
return [], [], [], None
num_detection = int(num_detection_layer[0])
scores = score_layer[:num_detection]
classes = class_layer[:num_detection].astype('int')
boxes = box_layer[:num_detection, :].clip(0, 1)
# apply NMS
boxes, scores, classes = UTILS.nms(boxes, scores, classes, 0.2)
w, h = config.image_size
# convert frames to (x,y,w,h) format and scale them to frame size
boxes = UTILS.to_xywh(boxes)*np.array([w, h, w, h])
message="Number of objects detected: {}".format(len(scores))
return (bboxes, labels, scores), message
def add_custom_to_meta(self, data, batch_meta, frame_meta):
bboxes, labels, scores = data
# Modify the metadata according to your needs
# import the packages you need
# mandatory
gie_unique_id=3 # the same as `gie-unique-id` in the DeepStream config file.
# mandatory
def parse_sgie_model(config, raw_outputs):
"""
Parameters
----------
raw_outputs : dict
Dictionary containing the models outputs.
Returns
message : Any json serializable
-------
"""
# get `message` from model output
return message
Let's say you have a primary model for face detection and you want to detect the face ladmarks on the detected faces:
import face_utils as UTILS
gie_unique_id=3
def parse_sgie_model(config, raw_outputs):
try:
landmarks = raw_outputs["landmarks"]
except:
print("LandmarksParser. Error: some layers missing in output tensors")
return None
d = UTILS.decode_landmarks(landmarks)
message = {
"LeftEye" : [d.leye.x, d.leye.y],
"RightEye" : [d.reye.x, d.reye.y],
"Nose" : [d.nose.x, d.nose.y],
"LeftMouth" : [d.lmouth.x, d.lmouth.y],
"RightMouth": [d.rmouth.x, d.rmouth.y]
}
return message
Once you have implemented your custom parser, you should package up the model and parser file(s) into
a folder. Copy this folder into the ds-ai-pipeline
folder.
Now that you have a folder with your custom assets, continue to Step 1 of the Prebuilt model path. This should be the only difference between the paths.
Let's take the previous steps and apply them to a concrete example. We will use the BodyPose2D model from the NVIDIA model zoo for this example. Technically, we already support this model as a pre-supported model (see the concrete example for pre-supported models), but we will reuse it here to show how you could bring a custom parser.
-
Prepare a folder whose contents look like this:
Where:
- labels.txt: Comes from the zip file you downloaded
- model.etlt: Comes from the zip file you downloaded
- int8_calibration_288_384.txt: Comes from the zip file you downloaded
- body_pose_parser.py: A Python file that contains the code you need for parsing the model's output
- pose.py: A Python file that contains additional source code that your parser makes use of
- bodypose2d_pgie_config: The primary model configuration file for DeepStream's nvinfer or nvinferserver.
The Python files and the primary model configuration file (bodypose2d_pgie_config) can be found here.
It is outside the scope of this tutorial to go into the details of the bodypose2d_pgie_config.txt file, as it is just a DeepStream NVInfer configuration file, and a working knowledge of DeepStream is assumed. If you have questions about that file, please refer to the appropriate documentation.
At this point, you can either zip this up and upload the zip file to Blob Storage and then download it to the container, or you can build the folder into a custom version of the container. We will walk through both options below.
If you choose to upload the zip file instead of building it into the container, follow these steps:
-
Make sure you have an Azure subscription with an Azure Storage Account attached. You should already have this from the quickstart.
-
Create a blob storage container that has anonymous access (see the screenshot below). In your Storage Account in the Azure Portal, first click on "Containers", then "+ Container", then choose "Container" level access.
-
Zip the folder's contents (make sure not to have a subfolder - just the raw files inside the zip file).
-
Upload your .zip file to blob storage (from the Azure Portal, go to your Storage Account, then "Containers", select a container, then "upload").
-
Modify the deployment manifest template file to point to your zip file:
"unsecureZipUrl": "<YOUR ZIP URL>",
-
Update the deployment manifest template to tell the ai-pipeline where the model configuration file is:
"primaryModelConfigPath": { "configFile": "bodypose2d_pgie_config.txt", "pyFile": "body_pose_parser.py" },
If you choose to rebuild the ai-pipeline container to include your model and parser, follow these steps:
-
Make sure you have somewhere to put the container we are about to build. We (obviously) recommend an Azure Container Registry.
-
Update your deployment manifest .env file with your credentials:
CONTAINER_REGISTRY_NAME=whatever.azurecr.io CONTAINER_REGISTRY_PASSWORD=<YOUR REGISTRY PASSWORD> CONTAINER_REGISTRY_USERNAME=<YOUR REGISTRY USERNAME>
If you are using an ACR (Azure Container Registry), you can enable an Admin user ("Access Keys" -> "Admin User"), then copy the login server, username, and password (see the screenshot below).
-
Create a folder called
custom-assets
in theds-ai-pieline
folder and put the contents of your custom asset folder in there. -
Rebuild the container. In this case, we do not need Triton Inference Server (see here for how to decide), so we will build using nvinfer and use the nvinfer configuration file syntax for the pgie txt file.
- If you are building for ARM:
azdacli create --device-type Jetson --tag azda-byom-example --add "./custom-assets /custom-assets" --build-dir <path to the docker-build directory>
- If you are building for x86:
azdacli create --device-type dGPU --tag azda-byom-example --add "./custom-assets /custom-assets" --build-dir <path to the docker-build directory>
- Please note that regardless of where you are relative to the custom-assets folder, you will use
--add "./custom-assets /custom-assets"
. This is because the path specified is relative to the docker-build directory.
- If you are building for ARM:
-
Either use Docker directly (
docker tag azda-byom-example <your-ACR>/<your-registry-name>:<your-tag>
) or use the CLI tool:azdacli push --image-name azda-byom-example --container-registry <your container registry> --username <your username>
, then type in your password when prompted. -
Change the .env file to update the following item:
DS_AI_PIPELINE_IMAGE_URI=<The tag you pushed>
-
Update the deployment manifest template to tell the ai-pipeline where the model configuration file is:
"primaryModelConfigPath": { "configFile": "/custom-assets/bodypose2d_pgie_config.txt", "pyFile": "/custom-assets/body_pose_parser.py" },
This example will guide how to bring your own model generated by Custom Vision website to Azure DeepStream Accelerator
- The video is going to be inferenced
- A blog storage. Learn how to create a blog storage here
File | Description |
---|---|
amd64/config_infer_custom_vision.txt |
The config file for ai-pipeline module for x86 |
amd64/libnvdsinfer_custom_impl_Yolo_dp61_amd.so |
The parser library for x86 |
arm64/config_infer_custom_vision.txt |
The config file for ai-pipeline module for Jetson Devices |
arm64/libnvdsinfer_custom_impl_Yolo_dp61_arm.so |
The parser library for Jetson Devices |
-
Create an object detection model as in here. Only iterations trained with a compact domain can be exported
-
Export your model with ONNX platform
-
Prepare your MyCustomThings.zip
-
For
AMD64
machine- Download the config_file
config_infer_custom_vision.txt
and parser librarylibnvdsinfer_custom_impl_Yolo_dp61_amd.so
of the custom vision project here
- Download the config_file
-
For
ARM64
machine- Download the config_file
config_infer_custom_vision.txt
and parser librarylibnvdsinfer_custom_impl_Yolo_dp_61_arm.so
of the custom vision project here
- Download the config_file
-
Config the
config_infer_custom_vision.txt
- Modify that the
num-detected-classes
property maps to the number of classes or objects that you've trained your custom vision model for.
- Modify that the
-
Create a
MyCustomThings
folder and add the files includingmodel.onnx
,label.txt
,config_infer_custom_vision.txt
,parser library
(ex:libnvdsinfer_custom_impl_Yolo_dp61_arm.so) or into it -
Zip MyCustomThings folder with the following structure
MyCustomThings/ | - model.onnx - label.txt - config_infer_custom_vision.txt - libnvdsinfer_custom_impl_Yolo_dp61_arm.so
[!Note] For Mac users, please open your terminal and zip your folder by following commands to remove the hidden file
zip -d MyCustomThings.zip "__MACOSX*"
-
-
Upload MyCustomThings.zip and video to your blog storage and note down the url for futher use.
- Visit your Azure blob storage resource
- Select
Containers
under Data Storage, and select+ Container
- Fill in your container name, and select
Blob (anonymous read access for blobs only)
- Click
Create
- Click the container you just created
- Select
Upload
to select your model zip file. ClickUpload
- Repeat the same process from iii to vi to upload your video file if needed
- Note down the blob urls for further use
-
Deploy the corresponding deployment manifest to your devices
-
Place your video to be inferenced to the place you config in the deployment manifest
- Open your terminal and ssh to your device
- Enter into the ai-pipeline container
docker exec -it ai-pipeline bash
- Change directory to the folder to save the videos
cd /opt/nvidia/deepstream/deepstream/samples/streams/
- Download the video you uploaded in the blob storage
wget <The blob url of your video>
-
Config your Module Identity Twin of your
BusinessLogicModule
module- Visit your Azure IoT Hub and click your device id
- Click
BusinessLogicModule
- Click
Module Identity Twin
- Add the value below under the key
startRecording
, { "configId": "PeopleDetection", "state": true }
- Click Save
-
Configure the Module Twin of the
controllermodule
-
Visit your Azure IoT Hub and click your device id
-
Click
controllermodule
-
Click
Module Identity Twin
-
Change the value of the
subtype
toFILE
-
Update the value of the
endpoint
below"endpoint": "file:///opt/nvidia/deepstream/deepstream/samples/streams/XXXXX.mp4",
-
Update the value of the
unsecureZipUrl
"unsecureZipUrl": "https://xxxxxxxxx.blob.core.windows.net/xxxxxxx/MyCustomThings.zip",
-
Update the value of the
primaryModelConfigPath
"primaryModelConfigPath": "config_infer_custom_vision.txt",
-
Click Save
-
After completed the above seven steps, you should able to see the inference video in the blob storage you config in here.
Regardless of whether you uploaded your model using a .zip file or built your container around your model, follow these steps:
- Rebuild your business logic container:
- Modify the source code to do whatever you want. See modifying the business logic container for detailed instructions.
- Build the new container
- Push the new container to an ACR.
- Update your .env file's
BUSINESS_LOGIC_IMAGE_URI
with the new image.
- Regenerate your deployment manifest from the deployment manifest template (in VS Code, right click the template and select "Generate IoT Edge Deployment Manifest").
- Update your deployment (in VS Code, right click the generated deployment manifest and select "Create Deployment for Single Device").