如果您已经在使用ANTGO的开发者镜像的话可以忽略。
- 开发镜像
registry.cn-hangzhou.aliyuncs.com/vibstring/antgo-env-dev:latest
- 模型转换镜像(涉及模型多平台部署时需要)
# snpe 引擎 registry.cn-hangzhou.aliyuncs.com/vibstring/snpeconvert:latest # tnn 引擎 registry.cn-hangzhou.aliyuncs.com/vibstring/tnnconvert:latest # rknn 引擎 registry.cn-hangzhou.aliyuncs.com/vibstring/rknnconvert:latest
- 启动开发环境
docker run -d --shm-size="20G" -p 8080:8080 -e PASSWORD=123 -v /data:/dataset -v /tmp:/tmp -v $(pwd):/workspace -v /var/run/docker.sock:/var/run/docker.sock -v /usr/bin/docker:/usr/bin/docker --gpus all --privileged registry.cn-hangzhou.aliyuncs.com/vibstring/antgo-env-dev /opt/code-server-4.92.2-linux-amd64/bin/code-server --host 0.0.0.0 --auth password
```
wget https://file.vibstring.com/yolo.tar && tar -xf yolo.tar
cd yolo
```
graph LR;
id1[(采集)]-->无标签数据
在这里,我们以YOLO目标检测算法为例,介绍如何搭建管线
from antgo.pipeline import *
import cv2
import numpy as np
import math
# 加载自定义C++算子
op.load('yolodecode2op', './')
# 创建输出文件夹
os.makedirs('./output/', exist_ok=True)
def debug_show(image,obj_bboxes,obj_masks):
image_h, image_w = image.shape[:2]
obj_num = len(obj_bboxes)
mask_h, mask_w = obj_masks.shape[1:3]
obj_seg_canvas = np.zeros((obj_num*mask_h, mask_w), np.uint8)
for obj_i, obj_bbox in enumerate(obj_bboxes):
x0,y0,x1,y1,_,label = obj_bbox
cv2.rectangle(image, (int(x0),int(y0)), (int(x1),int(y1)),2)
obj_mask = obj_masks[obj_i]
obj_seg_canvas[obj_i*mask_h:(obj_i+1)*mask_h, :] = (obj_mask*255).astype(np.uint8)
cv2.imwrite("./output/mask.png", obj_seg_canvas)
cv2.imwrite("./output/image.png", image)
return image, obj_seg_canvas
# 基础管线搭建
# 目标:
# 1. 对管线设计有初步认识
# 2. 数据源概念(图像数据源)
# imread_dc['image']('./data/demo.png')
# 3. 管线驱动
imread_dc['image']('./data/demo.png'). \
resize_op['image', 'resized_image'](out_size=(640,640)). \
inference_onnx_op['resized_image', ('output_0', 'output_1', 'output_2', 'output_3', 'output_4', 'output_5', 'output_6', 'output_7', 'output_8', 'output_9', 'output_10', 'output_11', 'output_12')](
onnx_path='./models/yolov8n-seg.onnx',
mean=[0, 0, 0],
std=[255, 255, 255],
). \
deploy.YoloDecodeOp[('image', 'output_0', 'output_1', 'output_2', 'output_3', 'output_4', 'output_5', 'output_6', 'output_7', 'output_8', 'output_9', 'output_10', 'output_11', 'output_12'), ('obj_bboxes', 'obj_masks')](). \
runas_op[('image', 'obj_bboxes', 'obj_masks'), ('image_det', 'mask_det')](func=debug_show).run()
# 替换为视频源,管线搭建
# 目标:
# 1. 数据源概念(视频数据源)
# video_dc['image', 'index']('./data/99_1727154910.mp4')
# 2. 视频保存节点使用
video_dc['image', 'index']('./data/99_1727154910.mp4'). \
resize_op['image', 'resized_image'](out_size=(640,640)). \
inference_onnx_op['resized_image', ('output_0', 'output_1', 'output_2', 'output_3', 'output_4', 'output_5', 'output_6', 'output_7', 'output_8', 'output_9', 'output_10', 'output_11', 'output_12')](
onnx_path='./models/yolov8n-seg.onnx',
mean=[0, 0, 0],
std=[255, 255, 255],
). \
deploy.YoloDecodeOp[('image', 'output_0', 'output_1', 'output_2', 'output_3', 'output_4', 'output_5', 'output_6', 'output_7', 'output_8', 'output_9', 'output_10', 'output_11', 'output_12'), ('obj_bboxes', 'obj_masks')](). \
runas_op[('image', 'obj_bboxes', 'obj_masks'), ('image_det', 'mask_det')](func=debug_show). \
select('image_det'). \
to_video('./output/out.mp4')
# 管线WEB服务
# 目标:
# 1. WEB服务上下文
# 2. 设定输入输入类型
# 输入类型支持'image', 'video', 'text', 'slider', 'checkbox', 'select', 'image-search'
# 输出类型支持'image', 'video', 'text', 'number', 'file'
with web['image'](name='demo') as handler:
app = handler.resize_op['image', 'resized_image'](out_size=(640,640)). \
inference_onnx_op['resized_image', ('output_0', 'output_1', 'output_2', 'output_3', 'output_4', 'output_5', 'output_6', 'output_7', 'output_8', 'output_9', 'output_10', 'output_11', 'output_12')](
onnx_path='./models/yolov8n-seg.onnx',
mean=[0, 0, 0],
std=[255, 255, 255],
). \
deploy.YoloDecodeOp[('image', 'output_0', 'output_1', 'output_2', 'output_3', 'output_4', 'output_5', 'output_6', 'output_7', 'output_8', 'output_9', 'output_10', 'output_11', 'output_12'), ('obj_bboxes', 'obj_masks')](). \
runas_op[('image', 'obj_bboxes', 'obj_masks'), ('image_det', 'mask_det')](func=debug_show). \
demo(
title="YOLO-Seg DEMO",
description="YOLO-Seg DEMO",
input=[
{'data': 'image', 'type': 'image'},
],
output=[
{'data': 'image_det', 'type': 'image'},
{'data': 'mask_det', 'type': 'image'}
]
)
在当前目录下,执行如下代码即可启动服务
antgo web --main=yolo_plugin:app --ip=0.0.0.0 --port=9002
python模式下执行常常仅用于算法原型验证阶段使用。企业级交付,考虑到计算效率和上层应用集成问题常需要C++部署方案。 在这里,我们先试试在x86-64下的部署输出。x86-64易于测试内存泄漏等工程型问题,建议可以优先在此平台测试工程 执行后,会生成编译好的so库和demo可执行程序。如果涉及模型运行,在x86平台,需要指定tensorrt作为推理引擎。不同平台和推理引擎的对应关系,见下表格。
平台和推理引擎关系对应表
平台 | 推理引擎 |
---|---|
linux/x86-64 | tensorrt(Nvidia平台) |
linux/arm64 | rknn(RK芯片平台) |
android/arm64-v8a | tnn(通用基于OPENCL推理)/snpe(高通平台)/rknn(RK芯片平台) |
# x86-64编译部署(SDK模式)
# 目标
# 1. 使用build节点进行部署
# 2. 设置工程编译配置,
# 目标平台(platform),目前支持linux/x86-64,linux/arm64,android/arm64-v8a
# 项目配置(project_config),包括输入和输出类型设置,工程名字(name="",需要采用project/pipeline_name格式),模式(mode='app/server'),调用模式(call_mode='sync/asyn')
# 环境配置(eagleeye_config),基础环境依赖项,包括opencv, rk, ffmpeg
placeholder['image'](np.zeros((640,640,3), dtype=np.uint8)). \
resize_op['image', 'resized_image'](out_size=(640,640)). \
inference_onnx_op['resized_image', ('output_0', 'output_1', 'output_2', 'output_3', 'output_4', 'output_5', 'output_6', 'output_7', 'output_8', 'output_9', 'output_10', 'output_11', 'output_12')](
onnx_path='./models/yolov8n-seg.onnx',
mean=[0, 0, 0],
std=[255, 255, 255],
engine='tensorrt',
). \
deploy.YoloDecodeOp[('image', 'output_0', 'output_1', 'output_2', 'output_3', 'output_4', 'output_5', 'output_6', 'output_7', 'output_8', 'output_9', 'output_10', 'output_11', 'output_12'), ('obj_bboxes', 'obj_masks')](). \
build(
platform='linux/x86-64',
project_config={
'input': [
('image', 'EAGLEEYE_SIGNAL_BGR_IMAGE'),
],
'output': [
('obj_bboxes', 'EAGLEEYE_SIGNAL_TENSOR'),
('obj_masks', 'EAGLEEYE_SIGNAL_TENSOR')
],
'name': 'leshislam/yolo',
'git': '',
'mode': 'app',
'call_mode': 'sync',
},
eagleeye_config={
'opencv': None,
}
)
在运行完后,你会发现在当前目录下出现了deploy目录,在这个目录下是编译后的工程
在这个目录下,需要解释一下自动创建的子目录含义,
-
linux_x86_64_build.sh
编译整个项目
-
CMakeLists.txt
编译时使用的CmakeLists文件。可以看一下文件内容,你会有更清晰的认识,整个项目依赖了哪些库。
-
cmake
常用的cmake文件。用于简化依赖库的发现。
-
yolo_plugin.h/cpp
根据python脚本里构建管线,转换成的C++代码构建管线。
-
leshislam_demo.cpp
创建的如何调用管线的示例代码,可以帮助开发同学快速集成管线到自己的项目中。
-
extent
extent文件夹里存放的是开发者自定义的C++计算节点代码
-
config
用于存放管线基础信息的配置文件。
-
models
用于存放转换后的模型文件(在端上平台运行,必须针对不同的预测引擎,将onnx模型转换成引擎接受的模型格式。)。
-
bin,build
编译后的信息存放位置
-
setup.sh
整理项目所有依赖so库打包到bin文件夹下,从而简化交付。
-
run.sh
运行DEMO。支持图像输入。
进入,deploy/leshislam_plugin,运行
bash setup.sh
会完成依赖库打包,将所有依赖信息均搜集到bin文件夹下。现在可以开始测试运行了,
bash run.sh image demo.png
执行完成后,你将在bin/x86-64/data/output/目录下发现保存的管线输出结果。
在这里,我们可以继续详细分析一下,管线的对外接口。管线设计采用统一标准接口,也就是说无论什么项目接口模式完全一致。 注意管线的输入和输出配置,在接口范例中有所涉及。
接口范例如下:
// 第一步:初始化
const char* config_folder = NULL;
eagleeye_{project}_initialize(config_folder);
// 第二步:设置管线输入(管线设计时设置了几个输入,这里就要为几个输入赋值)
unsigned char* in_data_0 = ...; // 内存指针
std::vector<size_t> in_data_0_size = {image_h, image_w, 3}; // tensor dims
int in_data_0_type = 0; // 数据类型
eagleeye_{project}_set_input(“placeholder_0”, in_data_0, in_data_0_size.data(), in_data_0_size.size(), 0, in_data_0_type);
// 第三步:管线执行
eagleeye_{project}_run();
// 第四步:获得返回目标框结果
// 这里可以获得管线的所有的输入数据
void* out_data; // 内存指针
size_t* out_data_size; // tensor dims
int out_data_dims=0; // dim size
int out_data_type=0; // 数据类型
eagleeye_{project}_get_output(“nnnode/0”,out_data, out_data_size, out_data_dims, out_data_type);
// 第五步:销毁管线
eagleeye_{project}_release();
注意,所涉及的数据类型,有如下定义
enum{
EAGLEEYE_CHAR = 0, // int8
EAGLEEYE_UCHAR = 1, // uint8
EAGLEEYE_SHORT = 2, // int16
EAGLEEYE_USHORT = 3, // uint16
EAGLEEYE_INT = 4, // int32
EAGLEEYE_UINT = 5, // uint32
EAGLEEYE_FLOAT = 6, // float32
EAGLEEYE_DOUBLE = 7,
}
可以结合如上通用接口,见本例中的leshislam_demo.cpp中的代码范例。
看看如何在不改任何代码的情况下,直接交付服务
# 目标
# 1. 了解发布server模式,mode='server'
# 2. 了解生成GRPC服务的通用proto定义
# 3. 了解同步模式和异步模式区别和接口使用,call_mode='sync/asyn'
placeholder['image'](np.zeros((640,640,3), dtype=np.uint8)). \
resize_op['image', 'resized_image'](out_size=(640,640)). \
inference_onnx_op['resized_image', ('output_0', 'output_1', 'output_2', 'output_3', 'output_4', 'output_5', 'output_6', 'output_7', 'output_8', 'output_9', 'output_10', 'output_11', 'output_12')](
onnx_path='./models/yolov8n-seg.onnx',
mean=[0, 0, 0],
std=[255, 255, 255],
engine='tensorrt',
). \
deploy.YoloDecodeOp[('image', 'output_0', 'output_1', 'output_2', 'output_3', 'output_4', 'output_5', 'output_6', 'output_7', 'output_8', 'output_9', 'output_10', 'output_11', 'output_12'), ('obj_bboxes', 'obj_masks')](). \
build(
platform='linux/x86-64',
project_config={
'input': [
('image', 'EAGLEEYE_SIGNAL_BGR_IMAGE'),
],
'output': [
('obj_bboxes', 'EAGLEEYE_SIGNAL_TENSOR'),
('obj_masks', 'EAGLEEYE_SIGNAL_TENSOR')
],
'name': 'leshislam/yolo',
'git': '',
'mode': 'server',
'call_mode': 'sync',
},
eagleeye_config={
'opencv': None,
}
)
进入,deploy/leshislam_plugin,运行
bash setup.sh
会完成依赖库打包,将所有依赖信息均搜集到bin文件夹下。现在开始运行并指定服务端口
bash run.sh 9002
不出意外,你将会看到如下信息输出,
恭喜你,服务已经成功启动,又可以交付服务了。工作如此简单。
下面可以详细解释一下,服务通用protobuf的定义见deploy/leshislam_plugin/proto/leshislam.proto。所有项目均采用统一proto定义。 框架也自动为您生成了python版本的调用示例,见deploy/leshislam_plugin/grpc_client.py。接下来为您解释标准接口定义,
# 同步服务接口(依赖构建服务时指定的call_mode决定)
# 服务启动接口
{project}GrpcSyncStart
所涉及的请求和响应参数定义如下
message {project}GrpcSyncStartRequest {
string serverpipeline = 1; // 需要设置管线名称(对于本教程中的例子,则为yolo)
string serverid = 2; // 唯一ID(用于标识服务组,允许用户自己设定)
string servercfg = 3; // 服务配置(用于配置服务启动参数,普通用户可忽略)
}
message {project}GrpcSyncStartReply {
string message = 1; // 返回的消息例如 success
int32 code = 2; // 返回的code 0 成功,非0失败
string serverkey = 3; // 服务关键字(成功启动后,返回此服务唯一编码。在代用服务时需要使用。)
}
# 服务调用接口
{project}GrpcSyncCall
所涉及的请求和响应参数定义如下
message {project}GrpcSyncCallRequest{
string serverkey = 1; // 服务关键字(在启动服务时,返回的服务唯一编码)
string serverrequest = 2; // 服务请求(构建json字符串,用于为管线赋值)
}
message {project}GrpcSyncCallReply{
string message = 1; // 返回的消息例如 success
int32 code = 2; // 返回的code 0 成功,非0失败
string data = 3; // (服务返回数据,json格式)
}
# 服务停止接口
{project}GrpcSyncStop
所涉及的请求和响应参数定义如下
message {project}GrpcSyncStopRequest {
string serverkey = 1; // 服务关键字(在启动服务时,返回的服务唯一编码)
}
message {project}GrpcSyncStopReply {
string message = 1; // 返回的消息例如 success
int32 code = 2; // 返回的code 0 成功,非0失败
}
# 异步服务接口(依赖构建服务时指定的call_mode决定)
# 服务启动接口
{project}GrpcAsynStart
所涉及的请求和响应参数定义如下
message {project}GrpcAsynStartRequest {
string serverpipeline = 1; // 服务管线(对于本教程中的例子,则为yolo)
string serverid = 2; // 唯一ID(用于标识服务组,允许用户自己设定)
string servercfg = 3; // 服务配置(用于配置服务启动参数,普通用户可忽略)
}
message {project}GrpcAsynStartReply {
string message = 1; // 返回的消息例如 success
int32 code = 2; // 返回的code 0 成功,非0失败
string serverkey = 3; // 服务关键字(成功启动后,返回此服务唯一编码。在代用服务时需要使用。)
}
# 数据传输接口
{project}GrpcAsynPush
所涉及的请求和响应参数定义如下
message {project}GrpcAsynPushRequest{
string serverkey = 1; // 服务关键字(在启动服务时,返回的服务唯一编码)
string serverrequest = 2; // 服务请求(构建json字符串,用于为管线赋值)
}
message {project}GrpcAsynPushReply{
string message = 1; // 返回的消息例如 success
int32 code = 2; // 返回的code 0 成功,非0失败
string data = 3; // 忽略
}
# 消息接收接口
{project}GrpcAsynMessage
所涉及的请求和响应参数定义如下
message {project}GrpcAsynMessageRequest{
string serverkey = 1; // 服务关键字(在启动服务时,返回的服务唯一编码)
string serverrequest = 2; // 服务请求
}
message {project}GrpcAsynMessageReply{
string message = 1; // 返回的消息例如 success
int32 code = 2; // 返回的code 0 成功,非0失败
string data = 3; // (服务返回数据,json格式)
}
# 服务停止接口
{project}GrpcAsynStop
所涉及的请求和响应参数定义如下
message {project}GrpcAsynStopRequest {
string serverkey = 1; // 服务关键字(在启动服务时,返回的服务唯一编码)
}
message {project}GrpcAsynStopReply {
string message = 1; // 返回的消息例如 success
int32 code = 2; // 返回的code 0 成功,非0失败
}
其中{project}会根据你构建时设置的project_config下的name(格式含义project/pipeline)自动替换。 接下来我们看一下,调用服务时参数是如何传递的。参数体采用json格式进行传递,
info = {
'data': [
# 图像参数(需要编码成base64)
{
'type': 'image',
'content': base64.b64encode(image bytes).decode()
},
# 字符串参数
{
'type': 'string',
'content': 'xxx'
},
# 整数参数(需要以矩阵格式给出)
{
'type': 'matrix/int32',
'width': 1,
'height': 1,
'content': [1],
}
]
}
下面以python语言进行举例,可以在自动生成的grpc_client.py基础上进行修改
with grpc.insecure_channel(f'{ip}:{port}') as channel:
stup = leshislam_pb2_grpc.LeshislamGrpcStub(channel)
# step 1: 启动服务
response = stup.LeshislamGrpcSyncStart(leshislam_pb2.LeshislamGrpcSyncStartRequest(serverpipeline="yolo",serverid="xyz", servercfg=""))
serverkey = response.ListFields()[0][1]
print(f'server key {serverkey}')
# step 2: 调用服务
# 读取图像,并构造接送请求串
with open('./data/demo.png', 'rb') as fp:
image_content = fp.read()
info = {
'data': [
{
'type': 'image',
'content': base64.b64encode(image_content).decode()
},
]
}
serverrequest = json.dumps(info)
response = stup.LeshislamGrpcSyncCall(leshislam_pb2.LeshislamGrpcSyncCallRequest(serverkey=serverkey, serverrequest=serverrequest))
print(response)
接下来我们运行吧
python3 grpc_client.py --ip=127.0.0.1 --port=9002
需要注意,如果涉及模型运行,则要指定服务目标平台的预测引擎。具体平台和预测引擎对应关系需要见上表。
# 目标
# 1. 了解指定rknn引擎
# 2. 了解指定目标平台
placeholder['image'](np.zeros((640,640,3), dtype=np.uint8)). \
resize_op['image', 'resized_image'](out_size=(640,640)). \
inference_onnx_op['resized_image', ('output_0', 'output_1', 'output_2', 'output_3', 'output_4', 'output_5', 'output_6', 'output_7', 'output_8', 'output_9', 'output_10', 'output_11', 'output_12')](
onnx_path='./models/yolov8n-seg.onnx',
mean=[0, 0, 0],
std=[255, 255, 255],
engine='rknn',
engine_args={
'device':'rk3588',
}
). \
deploy.YoloDecodeOp[('image', 'output_0', 'output_1', 'output_2', 'output_3', 'output_4', 'output_5', 'output_6', 'output_7', 'output_8', 'output_9', 'output_10', 'output_11', 'output_12'), ('obj_bboxes', 'obj_masks')](). \
build(
platform='linux/arm64', # 需要支持linux/arm64
project_config={
'input': [
('image', 'EAGLEEYE_SIGNAL_BGR_IMAGE'),
],
'output': [
('obj_bboxes', 'EAGLEEYE_SIGNAL_TENSOR'),
('obj_masks', 'EAGLEEYE_SIGNAL_TENSOR')
],
'name': 'leshislam/yolo',
'git': '',
'mode': 'app',
'call_mode': 'sync',
},
eagleeye_config={
'opencv': None,
'rk': None
}
)
进入到deploy/leshislam_plugin后,运行
bash setup.sh
将所有依赖项打包到bin/目录下。此时将整个项目leshislam_plugin推送到目标平台,就可以直接运行了。快快试试吧!在目标平台下,依然通过如下代码执行
bash run.sh image demo.png
执行完成后,你将在bin/arm64-v8a/data/output/目录下发现保存的管线输出结果。
# 目标
# 1. 了解指定rknn引擎
# 2. 了解指定目标平台
# 3. 了解android平台so的测试过程
placeholder['image'](np.zeros((640,640,3), dtype=np.uint8)). \
resize_op['image', 'resized_image'](out_size=(640,640)). \
inference_onnx_op['resized_image', ('output_0', 'output_1', 'output_2', 'output_3', 'output_4', 'output_5', 'output_6', 'output_7', 'output_8', 'output_9', 'output_10', 'output_11', 'output_12')](
onnx_path='./models/yolov8n-seg.onnx',
mean=[0, 0, 0],
std=[255, 255, 255],
engine='rknn',
engine_args={
'device':'rk3588',
}
). \
deploy.YoloDecodeOp[('image', 'output_0', 'output_1', 'output_2', 'output_3', 'output_4', 'output_5', 'output_6', 'output_7', 'output_8', 'output_9', 'output_10', 'output_11', 'output_12'), ('obj_bboxes', 'obj_masks')](). \
build(
platform='android/arm64-v8a',
project_config={
'input': [
('image', 'EAGLEEYE_SIGNAL_BGR_IMAGE'),
],
'output': [
('obj_bboxes', 'EAGLEEYE_SIGNAL_TENSOR'),
('obj_masks', 'EAGLEEYE_SIGNAL_TENSOR')
],
'name': 'leshislam/yolo',
'git': '',
'mode': 'app',
'call_mode': 'sync',
},
eagleeye_config={
'opencv': None,
'rk': None
}
)
进入到deploy/leshislam_plugin后,我们看一下setup.sh脚本
adb shell "if [ ! -d '/data/local/tmp/leshislam' ]; then mkdir /data/local/tmp/leshislam; fi;"
adb push /root/.3rd/eagleeye/android-install/libs/arm64-v8a/* /data/local/tmp/leshislam/
adb push /root/.3rd/eagleeye/android-install/3rd/opencv/lib/arm64-v8a/* /data/local/tmp/leshislam/
adb push /root/.3rd/eagleeye/android-install/3rd/libyuv/lib/arm64-v8a/* /data/local/tmp/leshislam/
adb push ./bin/arm64-v8a/* /data/local/tmp/leshislam/
adb push ./3rd/arm64-v8a/* /data/local/tmp/leshislam/
adb shell "if [ ! -d '/data/local/tmp/leshislam/data' ]; then mkdir /data/local/tmp/leshislam/data; fi;"
可以看到,运行此setup.sh脚本后,会搜集所有依赖库并把项目编译产物推送到真机平台的/data/local/tmp目录下。然后可以主机上直接运行(前提是,需要把测试图片同时推送到目标目录)
bash run.sh image demo.png
如果,运行成功,便可以放心的将所有编译产物进行交付了。
相信至此,作为算法研发人员的你便可以无任何障碍进行高性能算法交付了。