- 资产集市
- 教学
- 实践
- AI说
- 案例库
- 生态合作
- 专区
中国站
简体中文发布于30个月以前
本文将介绍两类特殊的功能单元——展开/合并功能单元,两者总是结合在一起使用,我们将在多手关键点识别应用基础上,使用ModelBox
开发一个多手关键点识别的AI应用:读取摄像头,解码出视频帧,在画面中检测出所有的手部并识别出每只手上的21个关键点,再将关键点结果输出画面到本地屏幕。读者将了解到ModelBox
中展开/合并功能单元的基本结构和开发流程。
对于这样的应用,在手部检测之后,需要另一个循环语句对每只手进行关键点识别,伪代码如下:
import cv2
import onnx_model
# 初始化摄像头,参数0表示设备的第一个摄像头
cap = cv2.VideoCapture(0)
# 判断初始化是否成功
if not cap.isOpened():
print('failed to open camera 0')
exit()
# 初始化手部检测模型
hand_det = onnx_model('./hand_det.onnx')
# 初始化手部关键点检测模型
hand_pose = onnx_model('./hand_pose.onnx')
while True:
# 读取一帧视频图像,ret表示读取是否成功
ret, frame = cap.read()
# 图像预处理
img_pre = hand_det.preprocess(frame)
# 手部检测推理
output = hand_det.infer(img_pre)
# 检测结果后处理
hand_bboxes = hand_det.postprocess(output)
# 对所有的手部检测框循环处理
for hand_bbox in hand_bboxes:
# 从画面中裁剪出手部区域
hand_img = crop_hand_img(frame, hand_bbox)
# 手部图像预处理
img_pre2 = hand_pose.preprocess(hand_img)
# 手部关键点检测推理
output2 = hand_pose.infer(img_pre2)
# 关键点检测结果后处理
hand_landmarks = hand_pose.postprocess(output2)
# 在原始图像上画出手部关键点信息
draw_hand_landmarks(frame, hand_landmarks)
# 打开一个名为hand_pose的窗口,显示图像
cv2.imshow('hand_pose', frame)
# 阻塞等待键盘响应1ms,如果按下q键则跳出循环
if cv2.waitKey(1) & 0xFF == ord('q'):
break
# 释放摄像头资源
cap.release()
# 关闭所有窗口
cv2.destroyAllWindows()
对于这类一条数据产生多个输出,每个输出采用相同处理方式的应用,ModelBox
设计了特殊的功能单元——展开/合并功能单元来处理。接下来我们看看使用ModelBox
如何构建这一应用。
这个应用对应的ModelBox
版本已经做成模板放在华为云OBS中,可以用sdk中的solution.bat
工具下载,接下来我们给出该应用在ModelBox
中的完整开发过程:
执行.\solution.bat -l
可看到当前公开的技能模板:
PS ███> .\solution.bat -l
...
Solutions name:
mask_det_yolo3
...
multi_hand_pose_yolox_mbv2
结果中的multi_hand_pose_yolox_mbv2即为多手关键点识别应用模板,可使用如下命令下载模板:
PS ███> .\solution.bat -s multi_hand_pose_yolox_mbv2
...
solution.bat
工具的参数中,-l
代表list
,即列出当前已有的模板名称;-s
代表solution-name
,即下载对应名称的模板。下载下来的模板资源,将存放在ModelBox
核心库的solution
目录下。
在ModelBox
sdk目录下使用create.bat
创建multi_hand_pose
工程
PS ███> .\create.bat -t server -n multi_hand_pose -s multi_hand_pose_yolox_mbv2
sdk version is modelbox-xxx
success: create multi_hand_pose in ███\modelbox\workspace
与之前创建工程的命令相比,末尾多了-s
参数,表示将使用后面参数值代表的模板创建工程,而不是创建空的工程。
workspace
目录下将创建出multi_hand_pose
工程,工程内容如下所示:
multi_hand_pose
|--bin
│ |--main.bat:应用执行入口
│ |--mock_task.toml:应用在本地执行时的输入输出配置,此应用默认使用本地视频文件为输入源,最终结果输出到另一本地视频文件,可根据需要修改
|--CMake:存放一些自定义CMake函数
|--data:存放应用运行所需要的图片、视频、文本、配置等数据
│ |--hand_test_video.mp4:多手关键点识别测试用视频文件
|--dependence
│ |--modelbox_requirements.txt:应用运行依赖的外部库在此文件定义
|--etc
│ |--flowunit:应用所需的功能单元存放在此目录
│ │ |--cpp:存放C++功能单元编译后的动态链接库,此应用没有C++功能单元
│ │ |--collapse_hand_landmarks:合并功能单元,将同一张图的多个手部关键点数据进行合并输出
│ │ |--draw_hand_landmarks:手部关键点画图功能单元
│ │ |--expand_hand_images:展开功能单元,将同一张图的多个手部检测框展开为多个输出
│ │ |--hand_condition:条件功能单元,根据图中是否检测到手输出不同的分支
│ │ |--landmarks_post:手部关键点检测模型的后处理功能单元
│ │ |--yolox_post:手部检测使用的是YOLOX模型,此处即为后处理功能单元
|--flowunit_cpp:存放C++功能单元的源代码,此应用没有C++功能单元
|--graph:存放流程图
│ |--multi_hand_pose.toml:默认流程图,使用本地视频文件作为输入源
│ |--multi_hand_pose_camera.toml:摄像头输入对应的流程图
│ |--modelbox.conf:modelbox相关配置
|--hilens_data_dir:存放应用输出的结果文件、日志、性能统计信息
|--model:推理功能单元目录
│ |--detect_hand:手部检测推理功能单元
│ │ |--detect_hand.toml:手部检测推理功能单元的配置文件
│ │ |--yolox_hand.onnx:手部检测onnx模型
│ |--detect_landmarks:手部关键点检测推理功能单元
│ │ |--detect_landmarks.toml:手部关键点检测推理功能单元的配置文件
│ │ |--pose_mbv2.onnx:手部关键点检测onnx模型
|--build_project.sh:应用构建脚本
|--CMakeLists.txt
|--rpm:打包rpm时生成的目录,将存放rpm包所需数据
|--rpm_copyothers.sh:rpm打包时的辅助脚本
与单手关键点识别应用相同,multi_hand_pose
需要根据检测结果选择不同的分支进行后续操作:如果没有检测到手,直接输出原始图像;如果检测到手,需要进行手部关键点检测。不同的是,multi_hand_pose
需要对图中检测到的每只手都识别关键点,因此增加了展开功能单元expand_hand_images
,将图中的所有检测框展开为多个输出,传递到后面的功能单元分别做关键点识别;最后又增加了合并功能单元collapse_hand_landmarks
,对同一张图的多个手部关键点数据进行合并输出,使得后面的画图功能单元能收集到同一张图片的完整数据。
展开/合并功能单元的开发流程与通用功能单元是相同的:
本应用已经包含了展开/合并功能单元,如果需要创建新的展开/合并功能单元,创建操作与通用功能单元相同,功能单元目录结构也完全一样(以Python版本的展开/合并功能单元为例):
[flowunit_name]
|--[flowunit_name].toml # 功能单元属性配置文件
|--[flowunit_name].py # 功能单元接口实现文件
打开展开功能单元的配置文件expand_hand_images.toml
,看到其内容为:
# 基础配置
[base]
name = "expand_hand_images" # 功能单元名称
device = "cpu" # 功能单元运行的设备类型,Python功能单元仅支持cpu类型
version = "1.0.0" # 功能单元版本号
type = "python" # 功能单元类型,Python功能单元此处为固定值python
group_type = "generic" # 功能单元分组信息, 包括input/output/image/video/inference/generic等,默认为generic
description = "expand hand roi images from a frame" # 功能单元的功能描述信息
entry = "expand_hand_images@expand_hand_imagesFlowUnit" # Python功能单元入口
# 工作模式,以下配置项默认全为false,表示通用功能单元;且配置项之间互斥,即最多只能设置其中一个为true
stream = false # 是否是Stream类型功能单元
condition = false # 是否是条件功能单元
collapse = false # 是否是合并功能单元
expand = true # 是否是展开功能单元,此处设置为true,即表示expand_hand_images是一个展开功能单元
# 输入端口描述
[input]
[input.input1] # 输入数据
name = "in_image" # 原图 + 手部检测框(检测框以属性方式附加在原图上)
type = "uint8" # 原图数据格式为 uint8
# 输出端口描述
[output]
[output.output1] # 输出数据
name = "roi_images" # 裁剪出的手部roi图片
type = "uint8" # roi图片数据格式为 uint8
打开合并功能单元的配置文件collapse_hand_landmarks.toml
,看到其内容为:
# 基础配置
[base]
name = "collapse_hand_landmarks" # 功能单元名称
device = "cpu" # 功能单元运行的设备类型,Python功能单元仅支持cpu类型
version = "1.0.0" # 功能单元版本号
type = "python" # 功能单元类型,Python功能单元此处为固定值python
group_type = "generic" # 功能单元分组信息, 包括input/output/image/video/inference/generic等,默认为generic
description = "collapse all human joints in a frame" # 功能单元的功能描述信息
entry = "collapse_hand_landmarks@collapse_hand_landmarksFlowUnit" # Python功能单元入口
# 工作模式,以下配置项默认全为false,表示通用功能单元;且配置项之间互斥,即最多只能设置其中一个为true
stream = false # 是否是Stream类型功能单元
condition = false # 是否是条件功能单元
collapse = true # 是否是合并功能单元,此处设置为true,即表示collapse_hand_landmarks是一个合并功能单元
expand = false # 是否是展开功能单元
# 输入端口描述:转为json字符串格式的手部关键点数据
[input]
[input.input1]
name = "in_landmarks"
type = "string"
# 输出端口描述:一帧图像中多个手部的关键点合并后的数据
[output]
[output.output1]
name = "out_data"
type = "float"
可以看到,与通用功能单元不同的是,展开功能单元的base.expand属性设置成了true。合并功能单元的base.collapse属性设置成了true。
通用功能单元
通用功能单元输出的数据与输入的数据属于同一层级,例如图像缩放功能单元Resize,输入的是图片流,产生的也是图片流;另外,通用功能单元一次可处理多个Buffer(即batch size可以大于1),数据按batch size设置分成多组,多组数据并发处理,生成的结果数据与输入一致。下图展示了通用功能单元的数据流图:
展开功能单元
展开功能单元一次只处理一个Buffer(即batch size限制为1),多个Buffer并发处理,每个Buffer展开成一个子数据流。下图展示了展开功能单元的数据流图:
合并功能单元
多个子数据流进入合并功能单元处理,一次处理一个子流,多个子流的数据并发处理,每个子流只能合并输出一个Buffer。下图展示了合并功能单元的数据流图:
展开/合并功能单元需要实现FlowUnit::open,FlowUnit::close,FlowUnit::process三个接口,与通用功能单元相同。open、close接口的处理逻辑也与通用功能单元一样,只有process接口中的数据处理有所不同。
def process(self, data_context):
# 1. 从DataContext中获取输入输出BufferList对象
in_image = data_context.input("in_image")
roi_images = data_context.output("roi_images")
# 2. 循环处理每一个输入Buffer数据
for buffer_img in in_image:
# 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(), dtype=np.uint8, copy=False)
img_data = img_data.reshape(height, width, channel)
bboxes = buffer_img.get("bboxes")
bboxes = np.array(bboxes).reshape(-1,4)
# 3.3. 业务处理,根据检测框裁剪出多个手部图像,展开成一个子数据流
# 展开功能单元的输出处理在for循环中,即展开为多个输出Buffer
for bbox in bboxes:
img_roi = self.crop_bbox_img(bbox, img_data)
h, w, c = img_roi.shape
img_roi = img_roi.flatten()
# 4. 将业务处理返回的结果数据转换为Buffer,这里可以使用modelbox.Buffer构造函数
img_buffer = modelbox.Buffer(self.get_bind_device(), img_roi)
# 5. 设置输出Buffer的Meta信息,此处拷贝输入Buffer的部分Meta信息,其余差异的部分再进行设置
img_buffer.copy_meta(buffer_img)
img_buffer.set("pix_fmt", "bgr")
img_buffer.set("width", w)
img_buffer.set("height", h)
img_buffer.set("channel", c)
img_buffer.set("width_stride", w)
img_buffer.set("height_stride", h)
# 6. 将输出Buffer放入输出BufferList中
roi_images.push_back(img_buffer)
# 7. 返回成功标志,ModelBox框架会将数据发送到后续的功能单元
return modelbox.Status.StatusCode.STATUS_SUCCESS
可以看到对于每一个Buffer(第一层for循环内),展开功能单元将其展开为多个Buffer进行输出(第二层for循环),即单张图片中的多个手部检测框,裁剪出多张手部图片,作为多条数据分别输出,这样后面的手部关键点检测等功能单元就可以针对每个Buffer分别处理。
def process(self, data_context):
# 1. 从DataContext中获取输入输出BufferList对象
in_landmarks = data_context.input("in_landmarks")
out_data = data_context.output("out_data")
# 2. 循环处理每一个输入Buffer数据,但输出合并到一个对象中(multi_landmarks)
multi_landmarks = []
for buffer_landmarks in in_landmarks:
# 3.1. 将输入Buffer转换为字符串,从json字符串中解码出手部关键点数据
landmarks_str = buffer_landmarks.as_object()
landmarks = self.decode_landmarks(landmarks_str)
# 3.2. 业务处理:将一帧图像中多个手部的关键点数据合并在一起
multi_landmarks.append(landmarks)
# 4. 将业务处理返回的结果数据转换为Buffer
multi_landmarks = np.array(multi_landmarks)
out_buffer = modelbox.Buffer(self.get_bind_device(), multi_landmarks)
# 5/6. 将输出Buffer放入输出BufferList中,此处没有Meta设置
out_data.push_back(out_buffer)
# 7. 返回成功标志,ModelBox框架会将数据发送到后续的功能单元
return modelbox.Status.StatusCode.STATUS_SUCCESS
可以看到它将接收到的所有Buffer收拢到一起,只产生一个Buffer的输出,这样就将单张图片中的多个手部关键点信息合并到一条数据中,后面的画图功能单元就可以画出所有手的关键点。
multi_hand_pose
工程graph
目录下包含两个流程图,其中与工程同名的multi_hand_pose.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 = "multi-hand pose estimation example using yolox and mobilenet-v2 for local video or rtsp video stream" # 应用的简单描述
[graph]
format = "graphviz" # 流程图的格式,当前仅支持graphviz
graphconf = """digraph multi_hand_pose {
node [shape=Mrecord]
queue_size = 4
batch_size = 1
# 定义节点,即功能单元及其属性
input1[type=input, flowunit=input, device=cpu, deviceid=0]
data_source_parser[type=flowunit, flowunit=data_source_parser, device=cpu, deviceid=0]
video_demuxer[type=flowunit, flowunit=video_demuxer, device=cpu, deviceid=0]
video_decoder[type=flowunit, flowunit=video_decoder, device=cpu, deviceid=0, pix_fmt="bgr"]
image_resize[type=flowunit, flowunit=resize, device=cpu, deviceid=0, image_width=320, image_height=320]
color_transpose[type=flowunit flowunit=packed_planar_transpose device=cpu deviceid="0"]
normalize[type=flowunit flowunit=normalize device=cpu deviceid="0" standard_deviation_inverse="1.,1.,1."]
detect_hand[type=flowunit, flowunit=detect_hand, device=cpu, deviceid=0, batch_size = 1]
yolox_post[type=flowunit, flowunit=yolox_post, device=cpu, deviceid=0]
hand_condition[type=flowunit, flowunit=hand_condition, device=cpu, deviceid=0]
expand_hand_images[type=flowunit, flowunit=expand_hand_images, device=cpu, deviceid=0]
image_resize2[type=flowunit, flowunit=resize, device=cpu, deviceid=0, image_width=256, image_height=256]
color_transpose2[type=flowunit flowunit=packed_planar_transpose device=cpu deviceid="0"]
mean[type=flowunit flowunit=mean device=cpu deviceid="0" mean="116.28,103.53,123.68"]
normalize2[type=flowunit flowunit=normalize device=cpu deviceid="0" standard_deviation_inverse="0.0175070,0.01742919,0.01712475"]
detect_landmarks[type=flowunit, flowunit=detect_landmarks, device=cpu, deviceid=0, batch_size = 1]
landmarks_post[type=flowunit, flowunit=landmarks_post, device=cpu, deviceid=0]
collapse_hand_landmarks[type=flowunit, flowunit=collapse_hand_landmarks, device=cpu, deviceid=0]
draw_hand_landmarks[type=flowunit, flowunit=draw_hand_landmarks, device=cpu, deviceid=0]
video_out[type=flowunit, flowunit=video_out, device=cpu, deviceid=0]
# 定义边,即功能间的数据传递关系
input1:input -> data_source_parser:in_data
data_source_parser:out_video_url -> video_demuxer:in_video_url
video_demuxer:out_video_packet -> video_decoder:in_video_packet
video_decoder:out_video_frame -> image_resize:in_image
image_resize:out_image -> color_transpose:in_image
color_transpose:out_image -> normalize:in_data
normalize:out_data -> detect_hand:input
detect_hand:output -> yolox_post:in_feat
yolox_post:out_data -> hand_condition:in_bbox
video_decoder:out_video_frame -> hand_condition:in_image
hand_condition:no_hand -> video_out:in_video_frame
hand_condition:has_hand -> expand_hand_images:in_image
expand_hand_images:roi_images -> image_resize2:in_image
image_resize2:out_image -> color_transpose2:in_image
color_transpose2:out_image -> mean:in_data
mean:out_data -> normalize2:in_data
normalize2:out_data -> detect_landmarks:input
detect_landmarks:output -> landmarks_post:in_feat
landmarks_post:out_data -> collapse_hand_landmarks:in_landmarks
hand_condition:has_hand -> draw_hand_landmarks:in_image
collapse_hand_landmarks:out_data -> draw_hand_landmarks:in_landmarks
draw_hand_landmarks:out_image -> video_out:in_video_frame
}"""
图中,可以看到条件功能单元hand_condition的两个输出分别对接到不同的功能单元,在未检测到手部时,no_hand分支直接对接到video_out进行视频编码;检测到手部时,has_hand对接到之后的展开/合并功能单元做全部手的关键点识别。而展开功能单元expand_hand_images与合并功能单元collapse_hand_landmarks之间其他功能单元的使用方式,与正常流程并无不同。
另外,可以看到预处理功能单元resize、packed_planar_transpose、normalize等分别使用了两次(两次的属性不同),每种功能单元在图中也相应的定义了两个实例,使用不同的节点名称进行区分。
查看任务配置文件bin/mock_task.toml
,可以看到其中的任务输入和任务输出配置为如下内容::
[input]
type = "url"
url = "${HILENS_APP_ROOT}/data/hand_test_video.mp4" # 表示输入源为本地视频文件
[output]
type = "local"
url = "${HILENS_APP_ROOT}/hilens_data_dir/hand_test_result.mp4" # 表示输出为本地视频文件
即,使用本地视频文件data/hand_test_video.mp4
作为输入,解码、预处理、手部检测、后处理、手部关键点检测后,画图输出到本地视频文件data/hand_test_result.mp4
中。
启动应用前执行.\build_project.sh
进行工程构建,该脚本将编译自定义的C++功能单元(本应用不涉及)、将应用运行时会用到的配置文件转码为Unix格式(防止执行过程中的格式错误):
PS ███> .\build_project.sh
...
PS ███>
然后执行.\bin\main.bat
运行应用:
PS ███> .\bin\main.bat
...
运行结束后在hilens_data_dir
目录下生成了hand_test_result.mp4
文件,可以打开查看:
除了测试视频文件外,我们还可以使用PC自带或者外接的USB摄像头进行手部关键点检测,对应的流程图为multi_hand_pose_camera.toml
,其内容如下:
[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 = "multi-hand pose estimation example using yolox and mobilenet-v2 for local USB camera" # 应用的简单描述
[graph]
format = "graphviz" # 流程图的格式,当前仅支持graphviz
graphconf = """digraph multi_hand_pose {
node [shape=Mrecord]
queue_size = 4
batch_size = 1
# 定义节点,即功能单元及其属性
input1[type=input, flowunit=input, device=cpu, deviceid=0]
data_source_parser[type=flowunit, flowunit=data_source_parser, device=cpu, deviceid=0]
local_camera[type=flowunit, flowunit=local_camera, device=cpu, deviceid=0, pix_fmt=bgr, cam_width=1280, cam_height=720]
image_resize[type=flowunit, flowunit=resize, device=cpu, deviceid=0, image_width=320, image_height=320]
color_transpose[type=flowunit flowunit=packed_planar_transpose device=cpu deviceid="0"]
normalize[type=flowunit flowunit=normalize device=cpu deviceid="0" standard_deviation_inverse="1.,1.,1."]
detect_hand[type=flowunit, flowunit=detect_hand, device=cpu, deviceid=0, batch_size = 1]
yolox_post[type=flowunit, flowunit=yolox_post, device=cpu, deviceid=0]
hand_condition[type=flowunit, flowunit=hand_condition, device=cpu, deviceid=0]
expand_hand_images[type=flowunit, flowunit=expand_hand_images, device=cpu, deviceid=0]
image_resize2[type=flowunit, flowunit=resize, device=cpu, deviceid=0, image_width=256, image_height=256]
color_transpose2[type=flowunit flowunit=packed_planar_transpose device=cpu deviceid="0"]
mean[type=flowunit flowunit=mean device=cpu deviceid="0" mean="116.28,103.53,123.68"]
normalize2[type=flowunit flowunit=normalize device=cpu deviceid="0" standard_deviation_inverse="0.0175070,0.01742919,0.01712475"]
detect_landmarks[type=flowunit, flowunit=detect_landmarks, device=cpu, deviceid=0, batch_size = 1]
landmarks_post[type=flowunit, flowunit=landmarks_post, device=cpu, deviceid=0]
collapse_hand_landmarks[type=flowunit, flowunit=collapse_hand_landmarks, device=cpu, deviceid=0]
draw_hand_landmarks[type=flowunit, flowunit=draw_hand_landmarks, device=cpu, deviceid=0]
video_out[type=flowunit, flowunit=video_out, device=cpu, deviceid=0]
# 定义边,即功能间的数据传递关系
input1:input -> data_source_parser:in_data
data_source_parser:out_video_url -> local_camera:in_camera_packet
local_camera:out_camera_frame -> image_resize:in_image
image_resize:out_image -> color_transpose:in_image
color_transpose:out_image -> normalize:in_data
normalize:out_data -> detect_hand:input
detect_hand:output -> yolox_post:in_feat
yolox_post:out_data -> hand_condition:in_bbox
local_camera:out_camera_frame -> hand_condition:in_image
hand_condition:no_hand -> video_out:in_video_frame
hand_condition:has_hand -> expand_hand_images:in_image
expand_hand_images:roi_images -> image_resize2:in_image
image_resize2:out_image -> color_transpose2:in_image
color_transpose2:out_image -> mean:in_data
mean:out_data -> normalize2:in_data
normalize2:out_data -> detect_landmarks:input
detect_landmarks:output -> landmarks_post:in_feat
landmarks_post:out_data -> collapse_hand_landmarks:in_landmarks
hand_condition:has_hand -> draw_hand_landmarks:in_image
collapse_hand_landmarks:out_data -> draw_hand_landmarks:in_landmarks
draw_hand_landmarks:out_image -> video_out:in_video_frame
}"""
与multi_hand_pose.toml
相比,这个流程图使用local_camera替换了video_demuxer和video_decoder功能单元,其他部分是一致的。
打开工程目录下bin/mock_task.toml
文件,修改其中的任务输入和任务输出配置为如下内容:
[input]
type = "url"
url = "0" # 表示0号摄像头,即PC自带摄像头,若PC无摄像头需外接USB摄像头
[output]
type = "local"
url = "0:multi_hand_pose" # 表示名为```multi_hand_pose```的本地窗口
即使用编号为0的摄像头(默认为PC自带的摄像头),输出画面显示到名为multi_hand_pose
的本地屏幕窗口中。
执行.\bin\main.bat camera
运行应用,将会自动弹出实时的手部关键点检测的画面:
通过本教程,我们学习了ModelBox
中展开/合并功能单元的开发,有一类常见的视频AI应用是检测+分类,或者检测+识别,需要对检测到的所有对象分别处理,如人脸识别、车牌识别等,此时可以使用ModelBox
中的展开/合并功能单元,它能保证大部分模块的处理逻辑与正常数据流相同,又能对展开的子数据流进行高效的并发处理,提高运行速度。
根据本次课程学习的内容,在自己的环境进行复现,并将复现的程序运行界面,连同网页右上方的华为云账号截图发至打卡贴上,详细打卡规则参见活动页面或参考2022ModelBox实战营活动打卡 (huaweicloud.com)