ModelBox
新手入门
【2022 ModelBox实战营】通用Python功能单元

发布于30个月以前

  • 1
  • 0
  • 1520

发布于30个月以前

ModelBox AI应用开发——通用Python功能单元

在上一教程基础上,本文将使用ModelBox开发如下视频应用:读取摄像头,解码出视频帧,在画面左上方写上“Hello World”,再输出画面到本地屏幕。读者将了解到ModelBox中通用Python功能单元的基本结构和开发流程。

1. 传统开发模式

对于这样的应用,如果采用Python + OpenCV,代码如下:

import cv2

# 初始化摄像头,参数0表示设备的第一个摄像头
cap = cv2.VideoCapture(0)

# 判断初始化是否成功
if not cap.isOpened():
    print('failed to open camera 0')
    exit()

while True:
    # 读取一帧视频图像,ret表示读取是否成功
    ret, frame = cap.read()

    # 在图像左上方写上“Hello World”
    cv2.putText(frame, 'Hello World', (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)

    # 打开一个名为hello_world的窗口,显示图像
    cv2.imshow('hello_world', frame)

    # 阻塞等待键盘响应1ms,如果按下q键则跳出循环
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

# 释放摄像头资源
cap.release()

# 关闭所有窗口
cv2.destroyAllWindows()

这个应用在运行时包含下面几个步骤:

  • 初始化摄像头
  • 解码,读取视频帧
  • 在图像上写“Hello World”
  • 窗口显示
  • 释放摄像头

相比上一个应用,代码中添加了cv2.putText调用那一行代码。

2. ModelBox Pipeline模式

这个应用对应的ModelBox流程图如下所示:

接下来我们给出该应用在ModelBox中的完整开发过程:

1)创建工程

ModelBox sdk目录下使用create.bat创建hello_world工程

PS ███> .\create.bat -t server -n hello_world
sdk version is modelbox-xxx
success: create hello_world in ███\modelbox\workspace

workspace目录下将创建出hello_world工程,工程目录结构在前面教程有介绍。

2)添加功能单元

hello_world应用需要添加一个在图像上写字的功能单元,我们使用Python开发此功能单元,ModelBox中功能单元的开发流程为:

a. 创建功能单元

功能单元的创建可以使用create.bat工具,命令如下:

PS ███> .\create.bat -t python -n draw_text -p hello_world
sdk version is modelbox-xxx
success: create python draw_text in ███\modelbox\workspace\hello_world\etc\flowunit\draw_text

create.bat工具使用时,-t python 即表示创建的是Python功能单元;-n draw_text 表示创建的Python功能单元名称为draw_text-p hello_world 表示所创建的功能单元属于hello_world应用。

可以看到,在hello_world工程的etc/flowunit目录(此目录将作为Python功能单元的默认存放目录)下,生成了draw_text文件夹,其中自动生成了Python功能单元所需的py文件和toml配置文件,即Python功能单元目录结构如下:

[flowunit_name]
|--[flowunit_name].toml    # 功能单元属性配置文件
|--[flowunit_name].py      # 功能单元接口实现文件

b. 设置功能单元属性

ModelBox框架在初始化时,会扫描所配置的功能单元路径中后缀为toml的文件,并读取其中的信息,因此功能单元的属性配置文件需要先配置好,我们将draw_text功能单元的配置文件draw_text.toml中的内容修改为:

# 基础配置
[base]
name = "draw_text"                           # 功能单元名称
device = "cpu"                               # 功能单元运行的设备类型,Python功能单元仅支持cpu类型
version = "1.0.0"                            # 功能单元版本号
type = "python"                              # 功能单元类型,Python功能单元此处为固定值python
group_type = "generic"                       # 功能单元分组信息, 包括input/output/image/video/inference/generic等,默认为generic
description = "post-process for yolo3 model" # 功能单元的功能描述信息
entry = "draw_text@draw_textFlowUnit"        # Python功能单元入口

# 工作模式,以下配置项默认全为false,表示通用功能单元;且配置项之前互斥,即最多只能设置其中一个为true
stream = false    # 是否是Stream类型功能单元
condition = false # 是否是条件功能单元
collapse = false  # 是否是合并功能单元
expand = false    # 是否是展开功能单元

# 输入端口描述
[input]
[input.input1] # 输入数据为uint8格式的原始图像
name = "in_frame" # 输入端口名称
type = "uint8"    # 输入端口数据类型
device = "cpu"    # 输入端口数据位于哪种设备

# 输出端口描述
[output]
[output.output1] # 输出数据为写字后的uint8格式的图像
name = "out_frame" # 输出端口名称
type = "uint8"     # 输出端口数据类型

draw_text是一个运行在cpu上的通用Python功能单元,其入口为draw_text.py中的draw_textFlowUnit类,它包含一个名为in_frame、接收uint8格式数据的输入端口,一个名为out_frame、输出uint8格式数据的输出端口。

c. 实现功能单元接口

ModelBox功能单元定义了如下接口,开发者需要根据功能单元类型按需实现:

接口 功能说明 是否必须 使用说明
FlowUnit::open 功能单元初始化 实现功能单元的初始化,资源申请,配置参数获取等
FlowUnit::close 功能单元关闭 实现资源的释放
FlowUnit::process 功能单元数据处理 实现核心的数据处理逻辑
FlowUnit::data_pre 功能单元Stream流开始 实现Stream流开始时的处理逻辑,功能单元属性 base.stream = true 时生效,即Stream功能单元才有此接口
FlowUnit::data_post 功能单元Stream流结束 实现Stream流结束时的处理逻辑,功能单元数据处理类型是base.stream = true 时生效,即Stream功能单元才有此接口

draw_text是一个通用Python功能单元,需要实现FlowUnit::openFlowUnit::closeFlowUnit::process三个接口。

  • FlowUnit::open接口
    open接口将在图初始化的时候调用,通常内容是调用相关的接口获取配置文件中自定义的配置项,最后返回modelbox.Status.StatusCode.STATUS_SUCCESS,表示初始化成功,否则初始化失败。
    def open(self, config):
        # 获取流程图中功能单元配置参数值,进行功能单元的初始化
        config_item = config.get_string("key", "default_value")
        ...
        return modelbox.Status.StatusCode.STATUS_SUCCESS

draw_text功能单元没有自定义配置项,此接口无须实现。

  • FlowUnit::close接口
    close接口通常用于释放功能单元的公共资源,draw_text功能单元不涉及此问题。

  • FlowUnit::process接口
    process接口是功能单元最核心函数,输入数据的处理、输出数据的构造都在此函数中实现。接口处理流程大致如下:

  1. 通过配置的输入输出端口名,从DataContext中获取输入BufferList、输出BufferList对象
  2. 循环处理每一个输入Buffer数据,如果是STREAM功能单元,默认一次只处理一个数据,不必循环
  3. 将输入Buffer转换为numpy、string等常用对象,并编写业务处理逻辑(Buffer类型为ModelBox特有类型,在Python中不通用,所以Python功能单元获取输入Buffer后需要将Data内容转换为基础类型、string、numpy等常用数据类型,再进行操作)
  4. 将业务处理结果返回的结果数据调用self.create_buffer转换为Buffer
  5. 设置输出Buffer的Meta信息
  6. 将输出Buffer放入输出BufferList中
  7. 返回成功标志,ModelBox框架会将数据发送到后续的功能单元

draw_text功能单元的process函数实现如下(此功能单元会用到OpenCV与NumPy,已内置到ModelBox sdk中):

# 添加OpenCV与NumPy的引用
import cv2
import numpy as np
import _flowunit as modelbox

...

    def process(self, data_context):
        # 1. 从DataContext中获取输入输出BufferList对象
        in_data = data_context.input("in_frame")
        out_data = data_context.output("out_frame")

        # 2. 循环处理每一个输入Buffer数据
        for buffer_img in in_data:
            # 3.1 获取输入Buffer的属性信息
            width = buffer_img.get('width')
            height = buffer_img.get('height')
            channel = buffer_img.get('channel')

            # 3.2 将输入Buffer转换为numpy对象
            img_data = np.array(buffer_img.as_object(), copy=False)
            img_data = img_data.reshape((height, width, channel))

            # 3.3 业务处理:在图像左上方写“Hello World”
            cv2.putText(img_data, 'Hello World', (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)

            # 4. 将业务处理返回的结果数据调用self.create_buffer转换为Buffer
            out_buffer = self.create_buffer(img_data)

            # 5. 设置输出Buffer的Meta信息,此处直接拷贝输入Buffer的Meta信息
            out_buffer.copy_meta(buffer_img)

            # 6. 将输出Buffer放入输出BufferList中
            out_data.push_back(out_buffer)

        # 7. 返回成功标志,ModelBox框架会将数据发送到后续的功能单元
        return modelbox.Status.StatusCode.STATUS_SUCCESS

可以看到,ModelBox中,节点的端口之间进行数据传递的载体是Buffer,因此功能单元的输入输出均需要与Buffer产生交互。Buffer由MetaData两个部分组成,Meta存放了每个Buffer的元信息,用于描述Buffer的数据;Data是Buffer的主体,用于存放数据,Data是具体设备上申请的存储空间,可以是GPU、CPU等硬件上的存储空间。因此功能单元的输入端口可以指定输入Buffer所在的设备,框架会帮忙检查并确保数据存放在指定的设备上。

功能单元获取输入Buffer时,首先会获取到BufferList,其中包含了一个batch的数据,BufferList与vector行为一致。输出的Buffer可以来自两种方式,一是将输入直接推到输出;一是在功能单元中构建。功能单元只能构建出功能单元声明的依赖设备上的内存,不能随意选择Buffer所在的设备。

功能单元在获取输入、输出端口的BufferList对象时,需要数据上下文DataContext,DataContext的生命周期是功能单元内部,从流数据进入功能单元到处理完成。当数据生命周期不再属于当前功能单元时,DataContext生命周期也随之结束。因此DataContext只能处理当前功能单元的数据,全局数据需要另外的上下文对象(会话上下文SessionContext,后面教程会介绍)。

d. 调试运行

Python功能单元无需编译,通常会直接将其应用到流程图中运行查看效果。

3)修改流程图

hello_world工程graph目录下默认生成了一个与工程同名的hello_world.toml流程图,将其内容修改为(以Windows版ModelBox为例):

[driver]
# 功能单元的扫描路径,包含在[]中,多个路径使用,分隔
# ${HILENS_APP_ROOT} 表示当前应用的实际路径
# ${HILENS_MB_SDK_PATH} 表示ModelBox核心库的实际路径
dir = [
    "${HILENS_APP_ROOT}/etc/flowunit",
    "${HILENS_APP_ROOT}/etc/flowunit/cpp",
    "${HILENS_APP_ROOT}/model",
    "${HILENS_MB_SDK_PATH}/flowunit",
]
skip-default = true

[profile]
# 通过配置profile和trace开关启用应用的性能统计
profile = false                       # 是否记录profile信息,每隔60s记录一次统计信息
trace = false                         # 是否记录trace信息,在任务执行过程中和结束时,输出统计信息
dir = "${HILENS_DATA_DIR}/mb_profile" # profile/trace信息的保存位置

[flow]
desc = "hello world for modelbox" # 应用的简单描述

[graph]
format = "graphviz" # 流程图的格式,当前仅支持graphviz
graphconf = """digraph hello_world {
    node [shape=Mrecord]
    queue_size = 1
    batch_size = 1

    # 定义节点,即功能单元及其属性
    input1[type=input, flowunit=input, device=cpu, deviceid=0]
    data_source_parser2[type=flowunit, flowunit=data_source_parser, device=cpu, deviceid=0]
    local_camera3[type=flowunit, flowunit=local_camera, device=cpu, deviceid=0, pix_fmt="bgr", cam_width=1280, cam_height=720]
    draw_text5[type=flowunit, flowunit=draw_text, device=cpu, deviceid=0]
    video_out4[type=flowunit, flowunit=video_out, device=cpu, deviceid=0]

    # 定义边,即功能间的数据传递关系
    input1:input -> data_source_parser2:in_data
    data_source_parser2:out_video_url -> local_camera3:in_camera_packet
    local_camera3:out_camera_frame -> draw_text5:in_frame
    draw_text5:out_frame -> video_out4:in_video_frame
}"""

可以看到,与上一教程的应用相比,增加了draw_text节点定义和相关的边,graph中的内容即对应本章开头给出的流程图。

4)修改输入输出配置

本应用需要打开PC自带摄像头,解码出视频帧并输出到本地屏幕,我们打开工程目录下bin/mock_task.toml文件,修改其中的任务输入和任务输出配置为如下内容:

# 任务输入配置,当前支持以下几种输入方式:
# 1. rtsp摄像头或rtsp视频流:type="rtsp", url="rtsp://xxx.xxx"
# 2. 设备自带摄像头或者USB摄像头:type="url",url="${摄像头编号}" (比如 0 或者 1 等,需配合local_camera功能单元使用)
# 3. 本地视频文件:type="url",url="${视频文件路径}" (请使用${HILENS_APP_ROOT}宏,表示当前应用的实际路径)
# 4. http服务:type="url", url="http://xxx.xxx"(指的是任务作为http服务启动,此处需填写对外暴露的http服务地址,需配合httpserver类的功能单元使用)
[input]
type = "url"
url = "0"    # 表示0号摄像头,即PC自带摄像头,若PC无摄像头需外接USB摄像头

# 任务输出配置,当前支持以下几种输出方式:
# 1. rtsp视频流:type="local", url="rtsp://xxx.xxx" 
# 2. 本地屏幕:type="local", url="0:xxx" (设备需要接显示器,系统需要带桌面)
# 3. 本地视频文件:type="local",url="${视频文件路径}"(请使用${HILENS_APP_ROOT}宏,表示当前应用的实际路径)
# 4. http服务:type="webhook", url="http://xxx.xxx"(指的是任务产生的数据上报给某个http服务,此处需填写上传的http服务地址)
[output]
type = "local"
url = "0:hello_world" # 表示名为```hello_world```的本地窗口

该流程图在本地运行时的逻辑过程是:data_source_parser解析bin/mock_task.toml文件中输入配置的0号摄像头,local_camera打开该编号的摄像头并解码出视频帧,draw_text在图像上写字,最后video_out将图像输出到本地屏幕中名为hello_world的窗口中。

5)用启动脚本执行应用

启动应用前执行build_project.sh进行工程构建,该脚本将编译自定义的C++功能单元(本应用不涉及)、将应用运行时会用到的配置文件转码为Unix格式(防止执行过程中的格式错误):

PS ███> .\build_project.sh
...
PS ███>

然后执行bin/main.bat运行应用:

PS ███> .\bin\main.bat
...

将会自动弹出实时的摄像头视频画面,可以看到画面左上方有“Hello World”字样:

3. 小结

通过本教程,我们可以看到,如果应用所需的功能模块ModelBox暂未提供,可以使用Python进行功能单元开发,开发流程和接口实现都有其固定结构,很容易掌握。

课程打卡

根据本次课程学习的内容,在自己的环境进行复现,并将复现的程序运行界面,连同网页右上方的华为云账号截图发至打卡贴上,详细打卡规则参见活动页面或参考2022ModelBox实战营活动打卡 (huaweicloud.com)

LLM初学者

作者相关内容

【2022 ModelBox实战营】第一个应用
发布于30个月以前
【2022 ModelBox实战营】通用Python功能单元
发布于30个月以前
【ModelBox客流分析实战营】ModelBox端云协同AI开发套件(Windows)SDK安装篇
发布于28个月以前
【2022 ModelBox实战营】推理功能单元
发布于30个月以前
ModelBox开发案例 - 使用YOLO v3做口罩检测
发布于34个月以前

暂无数据

热门内容推荐

ModelArts JupyterLab常见问题解决办法
ModelArts开发者 发布于45个月以前
为医生打造专属数字分身!华为云联合万木健康打造医疗医学科普和患者教育数字人引擎
HWCloudAI 发布于20个月以前
图数据库 | 聊聊超级快的图上多跳过滤查询
弓役是也 发布于23个月以前
ModelArts准备工作_简易版
ModelArts开发者 发布于46个月以前
”智蔗见智·向新而生”广西第二届人工智能大赛baseline使用教程
追乐小王子 发布于31个月以前

暂无数据