地平线机器人 · 2022年11月29日 · 江苏

旭日X3派BPU部署教程系列之带你轻松走出模型部署新手村

安装准备

本部分主要介绍在使用工具链前必须的环境准备工作,包含开发机部署(个人电脑)和开发板部署(例如旭日开发板等包含BPU设备)两个部分。

开发机部署(个人电脑)

官方的示例教程的开发机都是Linux系统,实际上Windows系统也是可以的。最建议的方式是利用docker,模型转换过程主要还是基于CPU,用不到GPU,所以用docker就够了。

(1)安装docker

考虑到用户多数是基于个人电脑,所以相关环境的配置都是基于Windows的。相关文档内提供了Docker Desktop Installer.exe安装文件(见地平线开发者社区),安装之后,用管理员方式启动得到如下界面。

1.png

我们可以从地平线天工开物cpu docker hub获取部署所需要的CentOS Docker镜像。使用最新镜像v1.13.6,以管理员模式运行CMD,输入docker,可以显示出docker的帮助信息。

选择最新版本,则在cmd中输入命令docker pull openexplorer/ai_toolchain_centos_7:v1.13.6,即可自动开始docker的安装。

2.png

安装成功之后,即可在docker中查看成功安装的工具链镜像:

3.png

(2)配置天工开物OpenExplorer

OpenExplorer工具包的下载,需要wget支持,wget的下载链接为GNU Wget for Windows,安装好之后即可在cmd中通过如下命令下载工具包,解压后,工具包的内容如下所示,如果需要其他版本的,可以参考官网信息资料下载专区。

4.png

docker除了要挂载OpenExplorer工具包,还要挂载数据集文件夹,通过如下指令可以下载官方提供的数据集,或者从相关文档中的的OpenExplorer/dataset文件夹中下载,下载之后记得解压。

# cifar
wget -c ftp://vrftp.horizon.ai/Open_Explorer/eval_dataset/cifar-10.tar.gz 
# cityscapes
wget -c ftp://vrftp.horizon.ai/Open_Explorer/eval_dataset/cityscapes.tar.gz 
# coco
wget -c ftp://vrftp.horizon.ai/Open_Explorer/eval_dataset/coco.tar.gz 
# imagenet
wget -c ftp://vrftp.horizon.ai/Open_Explorer/eval_dataset/imagenet.tar.gz 
# VOC
wget -c ftp://vrftp.horizon.ai/Open_Explorer/eval_dataset/VOC.tar.gz

(PS.由于作者在Windows下解压导致部分软连接消失,因此补充一些必要的软连接更新)

# 重新构建model_zoo软连接
rm /open_explorer/ddk/samples/ai_toolchain/horizon_model_convert_sample/01_common/model_zoo
ln -s /open_explorer/ddk/samples/ai_toolchain/model_zoo /open_explorer/ddk/samples/ai_toolchain/horizon_model_convert_sample/01_common/model_zoo

(3) 启动Docker

按照教程,启动docker要执行run_docker.sh,可直接按照本文教程直接配置好指令即可。在进入docker之前,先记录两个内容:

5.png

天工开物OpenExplorer根目录:我的环境下是"D:\05 - 项目\01 - 旭日x3派\horizon_xj3_open_explorer_v2.2.3_20220617",记得加双引号防止出现空格,该目录要挂载在docker中/open_explorer目录下;

dataset根目录:我的环境下是"D:\01 - datasets",记得加双引号防止出现空格,该目录需要挂载在docker中的/data/horizon_x3/data目录下;

*辅助文件夹根目录:官方教程其实是没有这个过程的,我把这个挂载在docker里,就是充当个类似U盘的介质。比如在我的环境下是"D:\05 - 项目\01 - 旭日x3派\BPUCodes",我可以在windows里面往这个文件夹拷贝数据,这些数据就可以在docker中使用,在docker中的路径为/data/horizon_x3/codes。

那么,在cmd(管理员)中输入如下指令即可进入docker(切记要确保刚刚安装的软件docker desktop是开启的),值得注意的是CMD不支持换行,记得删掉后面的\然后整理为一行,这时我们可以看到由命令行挂载的3个目录。

import cv2
# 打开摄像头并显示
docker run -it --rm \
-v "D:\05 - 项目\01 - 旭日x3派\horizon_xj3_open_explorer_v2.2.3_20220617":/open_explorer \
-v "D:\01 - datasets":/data/horizon_x3/data \
-v "D:\05 - 项目\01 - 旭日x3派\BPUCodes":/data/horizon_x3/codes \
openexplorer/ai_toolchain_centos_7:v1.13.6

6.png

至此,已经成功通过Docker镜像进入了完整的工具链开发环境。可以键入 hb_mapper --help 命令验证下是否可以正常得到帮助信息,hb_mapper 是工具链的一个常用工具, 在后文的模型转换部分对其有详细介绍。

7.png

除了通过挂载个额外的文件夹来实现文件的拷贝,还有一个方法可以直接将文件拷贝到目标目录。

假如我们要拷贝一个文件"C:\Users\Zhaoxi-Li\Downloads\Pangolin-0.8.tar.gz"到docker中的/root/downloads下(目录要存在),那么用管理员权限新开一个cmd,输入docker ps,记录CONTAINER ID,然后按照docker cp 本地文件的路径 container_id:<docker容器内的路径>的方式输入docker cp "C:\Users\Zhaoxi-Li\Downloads\Pangolin-0.8.tar.gz" 677de3a8b719:/root/downloads即可完成文件拷贝。

8.png

开发板部署(旭日3派为例)

在使用之前,一定要按照教程多方位玩转《地平线新发布AIoT开发板——旭日X3派(Sunrise x3 Pi)》(可于「地平线开发者社区」-「开发者论坛」搜索查看)完成系统的启动。

工具链的部分补充工具未包含在系统镜像中,这些工具已经放置在Open Explorer发布包中。因此在我们刚刚拉取的docker中,输入cd /open_explorer/ddk/package/board/,执行命令bash install.sh 192.168.0.104,其中192.168.0.104为开发板IP地址,可用ifconfig查看。

这个功能主要就是拷贝hrt_bin_dump和hrt_model_exec到开发板,并在开发板的/etc/profile里面添加几个环境,添加内容如下所示。

#Horizon Open Explorer ENV
export PATH=/userdata/.horizon/:/userdata/.horizon/ai_express_webservice_display/sbin/:$PATH
export HORIZON_APP_PATH=/userdata/.horizon/:$HORIZON_APP_PATH
#Horizon Open Explorer ENV

在开发板里输入hrt_model_exec,如果有如下输出,说明开发板部署完成。

9.png

模型部署

BPU的工具链是非常长的,在部署之前一定要先理解下每个流程的含义。

模型准备

支持Caffe模型和ONNX模型,Caffe模型的支持度是最高的。咱们常用的Pytorch模型是可以转为ONNX模型的。实际上,OpenCV内部集成的dnn模块也是以caffe为主的,所以尽管Caffe在学术圈不火了,但它在工业圈一直广泛使用。

验证模型

验证模型中所用的层是否可以在BPU中使用。需要利用hb_mapper checker后面跟一堆参数来对模型进行配置。配置信息如下:

--model-type:输入的模型类型,onnx或caffe ;

--march:芯片类型,这个板子只能填bernoulli2;

--proto:若模型为caffe,则填入caffe所需的prototxt文件。onnx模型就不用写这个参数;

--model:模型文件,caffe就是*.caffemodel,onnx模型就是.onnx;

--input-shape:模型数据输入的名称和维度,比如输入层名称叫input1,维度为1x3x128x128,那么该参数就可以写为--input-shape input1 1x3x128x128。如果我们的模型有多个输入,比如第二个输入层名称叫input2,维度为1x96x28x28,那么参数设置就写为--input-shape input1 1x3x128x128 --input-shape input2 1x96x28x28(该参数可选,不写的话程序会自动识别参数,如果指定以指定为主);

--output:设置输出日志文件(已经移除,默认存在根目录的hb_mapper_checker.log中);

注意:如果模型检查不通过,控制台会有明显的ERROR信息,一般都会检查出某些层不支持BPU,这时候可以写个自定义层来解决,后面会提供个例子来展示不通过情况的处理办法。

转换模型

模型检查通过之后,就可以通过配置一个yaml文件来将模型文件转为可以在BPU上运行的文件了,后面配置模型时候会进行详细介绍。

--model-type:根据模型类型指定caffe或onnx;

--config:模型编译的配置文件,内容采用yaml格式,文件名使用.yaml后缀。

模型性能、精度分析与调优

初时BPU的时候都会疑惑,为什么转换模型后精度会有变化?因为模型转换后是由float转为int8计算的,这个过程必有精度损失。如果精度差异较大,就需要按照官方教程进行调优。

Yolov3部署示例

将yolov3放置在docker文件中的/open_explorer/ddk/samples/ai_toolchain/horizon_model_convert_sample/04_detection/02_yolov3_darknet53/mapper路径下,以官方示例,来初步了解下BPU的相关操作流程。

10.png

模型准备

prototxt和caffemodel文件放置在docker中的/open_explorer/ddk/samples/ai_toolchain/model_zoo/mapper/detection/yolov3_darknet53路径下。

11.png

验证模型

/open_explorer/ddk/samples/ai_toolchain/horizon_model_convert_sample/04_detection/02_yolov3_darknet53/mapper,进入该路径后,输入 ./01_check.sh,遇到下述这些输出,就代表转换完成了。

12.png

前面已经介绍了,模型验证需要利用hb_mapper checker后面跟一堆参数来对模型进行配置,下面这些就是 ./01_check.sh的主要内容。

13.png

下面带各位来理解这些参数:

--model-type:我们这些模型是Caffe,所以填caffe;

--march:旭日X3派只能填bernoulli2;

--proto:填prototxt文件路径

即../../../01_common/model_zoo/mapper/detection/yolov3_darknet53/yolov3_transposed.prototxt;

--model:填caffemodel文件路径

即../../../01_common/model_zoo/mapper/detection/yolov3_darknet53/yolov3.caffemodel;

--input-shape:这里没有指定,代码可以自动去查找。
14.png

转换模型

在转换模型之前需要准备校准数据,输入./02_preprocess.sh会自动从docker的open_explorer包中抽取数据;再输入./03_build.sh,输出一大堆的命令行,等待一段时间之后会输出。

15.png

这里我们可以发现每一层网络都要评估一个相似度,这也是为什么要准备校准数据,因为BPU是INT8计算,所以注定会有精度损失。而且这些误差也是可以传递的,所以到后面精度是越来越低的。如果网络深度过高,也会导致整体精度的下降。

16.png

为了更好的理解这些转换流程,将对其中的准备校准数据、模型转换过程进行一个完全解读。

(1)原理解读:准备校准数据

这个过程调用了脚本./02_preprocess.sh,这个脚本核心调用的是python文件,data_preprocess.py的源码可以自行去查看。

python3 ../../../data_preprocess.py \
  --src_dir ../../../01_common/calibration_data/coco \
  --dst_dir ./calibration_data_rgb_f32 \
  --pic_ext .rgb \
  --read_mode opencv

然而data_preprocess.py并不适合初学者进行阅读,因为其兼容了太多东西,很简单的一些功能硬是写复杂了,那么就围绕这个模型,给各位缕缕校准数据到底要准备啥。

首先要搞清楚,我们要准备的校准数据是什么样的: 校准数据要将图像数据按照目标尺寸、目标颜色(rgb or bgr等)、目标排布(CHW or HWC)进行存储。那么下面,带着这些问题进行处理,先构建一个基本处理流程:

①加载一个文件夹下的所有图像地址信息。图像目录为/open_explorer/ddk/samples/ai_toolchain/horizon_model_convert_sample/01_common/calibration_data/coco;

②对每个图像按照校准格式进行输出。从prototxt我们知道图像的尺寸为416x416,从./03_build.sh调用的yaml文件可知图像输入格式为rgb,数据排布为CHW;

③将转换后的图像利用numpy.tofile函数存到目标文件夹下(你在哪转换的,就要在哪个目录存校准数据文件夹calibration_data)。

开始写我们自己的Python代码,每个步骤都写了注释,各位可以直接理解。

# prepare_calibration_data.py
import os
import cv2
import numpy as np

src_root = '/open_explorer/ddk/samples/ai_toolchain/horizon_model_convert_sample/01_common/calibration_data/coco'
cal_img_num = 100  # 想要的图像个数
dst_root = '/open_explorer/ddk/samples/ai_toolchain/horizon_model_convert_sample/04_detection/02_yolov3_darknet53/mapper/calibration_data'


## 1. 从原始图像文件夹中获取100个图像作为校准数据
num_count = 0
img_names = []
for src_name in sorted(os.listdir(src_root)):
    if num_count > cal_img_num:
        break
    img_names.append(src_name)
    num_count += 1

# 检查目标文件夹是否存在,如果不存在就创建
if not os.path.exists(dst_root):
    os.system('mkdir {0}'.format(dst_root))

## 2 为每个图像转换
# 参考了OE中/open_explorer/ddk/samples/ai_toolchain/horizon_model_convert_sample/01_common/python/data/下的相关代码
# 转换代码写的很棒,很智能,考虑它并不是官方python包,所以我打算换一种写法

## 2.1 定义图像缩放函数,返回为np.float32
# 图像缩放为目标尺寸(W, H)
# 值得注意的是,缩放时候,长宽等比例缩放,空白的区域填充颜色为pad_value, 默认127
def imequalresize(img, target_size, pad_value=127.):
    target_w, target_h = target_size
    image_h, image_w = img.shape[:2]
    img_channel = 3 if len(img.shape) > 2 else 1

    # 确定缩放尺度,确定最终目标尺寸
    scale = min(target_w * 1.0 / image_w, target_h * 1.0 / image_h)
    new_h, new_w = int(scale * image_h), int(scale * image_w)

    resize_image = cv2.resize(img, (new_w, new_h))

    # 准备待返回图像
    pad_image = np.full(shape=[target_h, target_w, img_channel], fill_value=pad_value)

    # 将图像resize_image放置在pad_image的中间
    dw, dh = (target_w - new_w) // 2, (target_h - new_h) // 2
    pad_image[dh:new_h + dh, dw:new_w + dw, :] = resize_image

    return pad_image

## 2.2 开始转换
for each_imgname in img_names:
    img_path = os.path.join(src_root, each_imgname)

    img = cv2.imread(img_path)  # BRG, HWC
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)  # RGB, HWC
    img = imequalresize(img, (416, 416))
    img = np.transpose(img, (2, 0, 1))  # RGB, CHW

    # 将图像保存到目标文件夹下
    dst_path = os.path.join(dst_root, each_imgname + '.rgbchw')
    print("write:%s" % dst_path)
    # 图像加载默认就是uint8,但是不加这个astype的话转换模型就会出错
    # 转换模型时候,加载进来的数据竟然是float64,不清楚内部是怎么加载的。
    img.astype(np.uint8).tofile(dst_path) 

print('finish')

(2)原理解读:转换配置

模型转换的核心在于配置目标的yaml文件,官方也提供了一个yolov3_darknet53_config.yaml可供用户直接试用,每个参数都给了注释,我能感受到开发者的诚意。然而模型转换的配置文件参数太多,如果想改参数都不知道如何下手。

本节目的是引导各位快速上手,因此一些参数我暂时不解释意义,用默认即可。该模板可将待配置的30多个参数压缩到9个参数,方便各位快速的配置简单模型。本yaml模板适用于的模型具有如下属性:

  • 无自定义层,换句话说,BPU支持该模型的所有层;
  • 输入节点只有1个,且输入是图像。

先复制这个模板到代码根目录,命名为"yolov3_simple.yaml",然后根据后面的思维导图进行配置具体参数。

model_parameters:
  # [待配置参数],见思维导图"模型参数组"部分
  prototxt: '***.prototxt'
  caffe_model: '****.caffemodel'
  onnx_model: '****.onnx'
  output_model_file_prefix: 'mobilenetv1'
  
  # 默认参数,暂不需要理解
  march: 'bernoulli2'

input_parameters:
  # [待配置参数],见思维导图"输入信息参数组/原始模型参数"部分
  input_type_train: 'bgr'
  input_layout_train: 'NCHW'

  # [待配置参数],见思维导图"输入信息参数组/转换后模型参数"部分
  input_type_rt: 'yuv444'

  # [待配置参数],见思维导图"输入信息参数组/输入数据预处理"部分
  norm_type: 'data_mean_and_scale'
  mean_value: '103.94 116.78 123.68'
  scale_value: '0.017'
  
  # 默认参数,暂不需要理解
  input_layout_rt: 'NHWC'
  

# 校准参数组,全部默认
calibration_parameters:
  cal_data_dir: './calibration_data'
  calibration_type: 'max'
  max_percentile: 0.9999

# 编译参数组,全部默认
compiler_parameters:
  compile_mode: 'latency'
  optimize_level: 'O3'
  debug: False # 别看官网写的可选,实际上不写这个出bug

思维导图如下所示,带着这个图,请各位耐心地跟我一步步配置,仅需要配置9个即可。

17.png

模型参数组参数model_parameters配置:

output_model_file_prefix:给转换后的模型起个名,这里叫做'yolov3_selfyaml',注意字符串前后都要有个单引号;

prototxt:caffe的prototxt,这里为'../../../01_common/model_zoo/mapper/detection/yolov3_darknet53/yolov3_transposed.prototxt';

caffe_model:caffe的模型文件,这里为'../../../01_common/model_zoo/mapper/detection/yolov3_darknet53/yolov3.caffemodel';

onnx_model:删掉。因为我们用的是Caffe。

输入信息组参数配置input_parameters:

input_type_train:原始浮点模型的输入数据格式,支持多种图像格式,这里设置为'rgb'(这就是前文校准模型时为什么要将BGR转为RGB);

input_layout_train:从前文的prototxt可以看出,数据输入排布为'NCHW'(所以在模型校准时我们将图像数据由HWC转为CHW)

input_type_rt:模型转换后,我们期望输入的图像格式。我们在训练模型和部署模型的时候,图像输入格式是可以变的,NV12是一些相机返回的原始数据格式,作为尝试设置为'nv12';

norm_type:网络不可能拿原始图像数据作为输入的,一般都要进行一个归一化操作。这里用的模型对应的归一化代码为inpBlob = cv2.dnn.blobFromImage(frame, 1.0 / 255, (inWidth, inHeight), (0, 0, 0), swapRB=False, crop=False),无减均值项,只有尺度项。因此,该属性设置为'data_scale';

mean_value:删掉,因为网络没有均值项;

scale_value:尺度为1.0 / 255,因此设置为0.003921568627451。

最终,我们的yaml文件内容如下所示:

model_parameters:
  prototxt: '../../../01_common/model_zoo/mapper/detection/yolov3_darknet53/yolov3_transposed.prototxt'
  caffe_model: '../../../01_common/model_zoo/mapper/detection/yolov3_darknet53/yolov3.caffemodel'
  output_model_file_prefix: 'yolov3_selfyaml'
  march: 'bernoulli2'
input_parameters:
  input_type_train: 'rgb'
  input_layout_train: 'NCHW'
  input_type_rt: 'nv12'
  norm_type: 'data_scale'
  scale_value: 0.003921568627451
  input_layout_rt: 'NHWC'
calibration_parameters:
  cal_data_dir: './calibration_data'
  calibration_type: 'max'
  max_percentile: 0.9999
compiler_parameters:
  compile_mode: 'latency'
  optimize_level: 'O3'
  debug: False

之后,用我们亲手准备的校准数据和配置的轻量yaml进行模型转换,在控制台输入指令hb_mapper makertbin --config yolov3_simple.yaml --model-type caffe。

模型推理

在官方给的demo中,04_inference.sh可以直接调用执行好的模型进行推理,但是为了我觉得这种方案对于未来要如何部署自己的模型是无意义的。因此我阅读了官方推理的demo之后,自己写个完整的推理过程。模型推理流程主要可以分为以下三个步骤:

①数据预处理,生成推理所需数据;

②利用处理好的数据进行模型推理,得到输出;

③将输出转换成最终数据,也就是后处理过程。

(PS.使用的测试图像路径为/open_explorer/ddk/samples/ai_toolchain/horizon_model_convert_sample/01_common/test_data/det_images/kite.jpg)

在上一节中,模型转换后有三个关键文件:

yolov3_selfyaml_original_float_model.onnx:图像量化前的模型;

yolov3_selfyaml_quantized_model.onnx:图像量化后的模型;

yolov3_selfyaml.bin:在BPU上用于推理的模型文件,输出结果与yolov3_selfyaml_quantized_model.onnx一致。

下面将给出推理一张图像的相关代码,其中我把图像格式转换,以及yolo后处理的细节封装在一个包里,相关的代码已经放在社区里供大家参考。

18.png

以下是inference_model.py的代码细节,在每个关键过程中都给出了相关的注释:

19.png

从代码可以获知模型会输出三层,每层的维度为(1, 13, 13, 255) (1, 26, 26, 255) (1, 52, 52, 255),对着下图,可以很容易对应的是网络的哪一层。

import numpy as np
import cv2
import os
from horizon_tc_ui import HB_ONNXRuntime
from bputools.format_convert import imequalresize, bgr2nv12_opencv, nv122yuv444
from bputools.yolo_postproc import modelout2predbbox, recover_boxes, nms, draw_bboxs

modelpath_prefix = '/open_explorer/ddk/samples/ai_toolchain/horizon_model_convert_sample'

# img_path 图像完整路径
img_path = os.path.join(modelpath_prefix, '01_common/test_data/det_images/kite.jpg')
# model_path 量化模型完整路径
model_root = os.path.join(modelpath_prefix, '04_detection/02_yolov3_darknet53/mapper/model_output')
model_path = os.path.join(model_root, 'yolov3_selfyaml_quantized_model.onnx')

# 1. 加载模型,获取所需输出HW
sess = HB_ONNXRuntime(model_file=model_path)
sess.set_dim_param(0, 0, '?')
model_h, model_w = sess.get_hw()

# 2 加载图像,根据前面模型,转换后的模型是以NV12作为输入的
# 但在OE验证的时候,需要将图像再由NV12转为YUV444
imgOri = cv2.imread(img_path)
img = imequalresize(imgOri, (model_w, model_h))
nv12 = bgr2nv12_opencv(img)
yuv444 = nv122yuv444(nv12, [model_w, model_h])

# 3 模型推理
input_name = sess.input_names[0]
output_name = sess.output_names
output = sess.run(output_name, {input_name: np.array([yuv444])}, input_offset=128)
print(output_name)
print(output[0].shape, output[1].shape, output[2].shape)
# ['layer82-conv-transposed', 'layer94-conv-transposed', 'layer106-conv-transposed']
# (1, 13, 13, 255) (1, 26, 26, 255) (1, 52, 52, 255)

# 4 检测结果后处理
# 由output恢复416*416模式下的目标框
pred_bbox = modelout2predbbox(output)
# 将目标框恢复到原始分辨率
bboxes = recover_boxes(pred_bbox, (imgOri.shape[0], imgOri.shape[1]),
                       input_shape=(model_h, model_w), score_threshold=0.3)
# 对检测出的框进行非极大值抑制,抑制后得到的框就是最终检测框
nms_bboxes = nms(bboxes, 0.45)
print("detected item num: {0}".format(len(nms_bboxes)))

# 绘制检测框
draw_bboxs(imgOri, nms_bboxes)
cv2.imwrite('detected.png', imgOri)

上板运行

我们将下图所示的一些文件拖到旭日X3派开发板中,注意inference_model_bpu.py跟docker中是有微小的改动的。

20.png

注意,在执行前要安装一些包sudo pip3 install EasyDict pycocotools,切记要加sudo,这样安装的路径不是用户目录,在运行BPU模型时候,也是必须要加sudo的。

21.png

inference_model_bpu.py的源码如下所示,与在docker中不同,nv12不需要再转为yuv444了,模型的运行也有一些差别,而后处理几乎没有变化。

import numpy as np
import cv2
import os
from hobot_dnn import pyeasy_dnn as dnn
from bputools.format_convert import imequalresize, bgr2nv12_opencv, nv122yuv444
from bputools.yolo_postproc import modelout2predbbox, recover_boxes, nms, draw_bboxs

def get_hw(pro):
    if pro.layout == "NCHW":
        return pro.shape[2], pro.shape[3]
    else:
        return pro.shape[1], pro.shape[2]

modelpath_prefix = ''

# img_path 图像完整路径
img_path = 'COCO_val2014_000000181265.jpg'
# model_path 量化模型完整路径
model_path = 'yolov3_selfyaml.bin'

# 1. 加载模型,获取所需输出HW
models = dnn.load(model_path)
model_h, model_w = get_hw(models[0].inputs[0].properties)

# 2 加载图像,根据前面模型,转换后的模型是以NV12作为输入的
# 但在OE验证的时候,需要将图像再由NV12转为YUV444
imgOri = cv2.imread(img_path)
img = imequalresize(imgOri, (model_w, model_h))
nv12 = bgr2nv12_opencv(img)

# 3 模型推理
t1 = cv2.getTickCount()
outputs = models[0].forward(nv12)
t2 = cv2.getTickCount()
outputs = (outputs[0].buffer, outputs[1].buffer, outputs[2].buffer)
print(outputs[0].shape, outputs[1].shape, outputs[2].shape)
# (1, 13, 13, 255) (1, 26, 26, 255) (1, 52, 52, 255)
print('time consumption {0} ms'.format((t2-t1)*1000/cv2.getTickFrequency()))

# 4 检测结果后处理
# 由output恢复416*416模式下的目标框
pred_bbox = modelout2predbbox(outputs)
# 将目标框恢复到原始分辨率
bboxes = recover_boxes(pred_bbox, (imgOri.shape[0], imgOri.shape[1]),
                       input_shape=(model_h, model_w), score_threshold=0.3)
# 对检测出的框进行非极大值抑制,抑制后得到的框就是最终检测框
nms_bboxes = nms(bboxes, 0.45)
print("detected item num: {0}".format(len(nms_bboxes)))

# 绘制检测框
draw_bboxs(imgOri, nms_bboxes)
cv2.imwrite('detected.png', imgOri)

手部关键点检测网络

手部关键点检测是做手势识别的一个关键过程,该代码基于Caffe,而且无自定义层,因此作为个引子,带领各位先初步使用BPU。

模型准备

在前期安装准备中,我们挂载了一个目录-v "D:\05 - 项目\01 - 旭日x3派\BPUCodes":/data/horizon_x3/codes,下载好代码后,按照如下方式放置相关文件,此时可以发现docker中也有这些文件。

22.png

验证模型

验证前,先将docker根目录切换到模型根目录下cd /data/horizon_x3/codes/HandKeypointDetection/hand/。模型验证需要利用hb_mapper checker后面跟一堆参数来对模型进行配置。下面带各位来配置这些参数:

--model-type:我们这些模型是Caffe,所以填caffe

--march:旭日3派只能填bernoulli2

--proto:填prototxt文件名,即pose_deploy.prototxt

--model:填caffemodel文件名,即pose_iter_102000.caffemodel

--input-shape:打开prototxt文件,查找input属性,可以发现模型只有一个输入,输入层的名称为image,输入图像的维度大小为1x3x368x368,那么这个参数设置就写为image 1x3x368x368。
23.png

综上所述,在docker中需要输入如下指令来完成模型验证过程:

hb_mapper checker \
--model-type caffe \
--march bernoulli2 \
--proto pose_deploy.prototxt \
--model pose_iter_102000.caffemodel \
--input-shape image 1x3x368x368

输出结果如下所示,可以看到整个流程的转换状态以及每个节点是在BPU还是CPU上运行的。整个控制台的运行结果默认存在根目录的hb_mapper_checker.log中。

24.png

转换模型

与前面流程不同,这里先配置yaml文件,再准备校准数据。

(1)配置yaml文件

模型参数组参数model_parameters配置:

output_model_file_prefix:给转换后的模型起个名,这里叫做'handkpdet'(hand keypoint detection);

prototxt:caffe的prototxt,这里为'pose_deploy.prototxt';

caffe_model:caffe的模型文件,这里为'pose_iter_102000.caffemodel'

onnx_model:删掉。因为我们用的是Caffe。

输入信息组参数配置input_parameters:

input_type_train:原始浮点模型的输入数据格式,支持多种图像格式。我们这个模型,输入的是彩色图,考虑到opencv加载图像默认是BGR通道,因此这里设置为'bgr';

input_layout_train:从前文的prototxt可以看出,数据输入排布为'NCHW';

input_type_rt:模型转换后,我们期望输入的图像格式。我们在训练模型和部署模型的时候,图像输入格式是可以变的,NV12是一些相机返回的原始数据格式,考虑到我们测试仍然基于本地图像,因此这里仍然设置为'bgr';

norm_type:网络不可能拿原始图像数据作为输入的,一般都要进行一个归一化操作。这里用的模型对应的归一化代码为inpBlob = cv2.dnn.blobFromImage(frame, 1.0 / 255, (inWidth, inHeight), (0, 0, 0), swapRB=False, crop=False),无减均值项,只有尺度项。因此,该属性设置为'data_scale';

mean_value:删掉,因为网络没有均值项;

scale_value:尺度为1.0 / 255,因此设置为'0.0039'。

最终,我们的yaml文件handpoint.yaml内容为:

model_parameters:
  prototxt: 'pose_deploy.prototxt'
  caffe_model: 'pose_iter_102000.caffemodel'
  output_model_file_prefix: 'handkpdet'
  march: 'bernoulli2'
input_parameters:
  input_type_train: 'bgr'
  input_layout_train: 'NCHW'
  input_type_rt: 'bgr'
  norm_type: 'data_scale'
  scale_value: '0.0039'
  input_layout_rt: 'NHWC'
calibration_parameters:
  cal_data_dir: './calibration_data'
  calibration_type: 'max'
  max_percentile: 0.9999
compiler_parameters:
  compile_mode: 'latency'
  optimize_level: 'O3'
  debug: False

(2)准备校准数据

考虑到这个模型的输入只有一个,因此,准备校准数据部分的代码可以参考上一节的内容,需要修改的只有两个地方,原始数据地址,和颜色转换部分(取消了BGR转RGB的过程),数据集用的是FreiHAND_pub_v2_eval.zip。

25.png

docker中,校准数据形式如下图所示,共计100张。

26.png

(3)开始转换

数据准备就绪,输入命令hb_mapper makertbin --config handpoint.yaml --model-type caffe开始转换我们的模型!等待一段时间之后,模型转换成功,从结果可以看出来,模型的损失并不是很高!!感觉有戏,(☆▽☆)。

27.png

模型推理

由于该模型与前面的模型相似,都是以一张图像作为输入的,因此自己要补充的工作主要有两点:

  • 完成图像预处理部分。前面的yaml文件指明了,量化后的模型是以BGR、NHWC格式作为输入的。因此,只需要调用resize成目标模型大小就行,opencv加载图像时候默认是HWC格式。
  • 完成图像后处理部分。图像后处理一般与推理平台没有太大的关系,完整的流程都会有这个过程。

在docker中推理的完整代码如下所示:

import numpy as np
import cv2
import os
from horizon_tc_ui import HB_ONNXRuntime
import copy

# img_path 图像完整路径
img_path = '/data/horizon_x3/codes/HandKeypointDetection/hand/FreiHAND_pub_v2_eval/evaluation/rgb/00000253.jpg'
# model_path 量化模型完整路径
model_path = '/data/horizon_x3/codes/HandKeypointDetection/hand/model_output/handkpdet_quantized_model.onnx'

# 1. 加载模型,获取所需输出HW
sess = HB_ONNXRuntime(model_file=model_path)
sess.set_dim_param(0, 0, '?')
model_h, model_w = sess.get_hw()

# 2 加载图像,根据前面yaml,量化后的模型以BGR NHWC形式输入
imgOri = cv2.imread(img_path)
img = cv2.resize(imgOri, (model_w, model_h))

# 3 模型推理
input_name = sess.input_names[0]
output_name = sess.output_names
output = sess.run(output_name, {input_name: np.array([img])}, input_offset=128)
print(output_name)
print(output[0].shape)
# ['net_output']
# (1, 22, 46, 46)

# 4 检测结果后处理
# 绘制关键点
nPoints = 22
threshold = 0.1
POSE_PAIRS = [[0, 1], [1, 2], [2, 3], [3, 4], [0, 5], [5, 6], [6, 7], [7, 8], [0, 9], [9, 10], [10, 11], [11, 12],
              [0, 13], [13, 14], [14, 15], [15, 16], [0, 17], [17, 18], [18, 19], [19, 20]]

imgh, imgw = imgOri.shape[:2]
points = []
imgkp = copy.deepcopy(imgOri)
for i in range(nPoints):
    probMap = output[0][0, i, :, :]
    probMap = cv2.resize(probMap, (imgw, imgh))
    minVal, prob, minLoc, point = cv2.minMaxLoc(probMap)

    if prob > threshold:
        cv2.circle(imgkp, (int(point[0]), int(point[1])), 8, (0, 255, 255), thickness=-1, lineType=cv2.FILLED)
        cv2.putText(imgkp, "{}".format(i), (int(point[0]), int(point[1])), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2,
                    lineType=cv2.LINE_AA)
        points.append((int(point[0]), int(point[1])))
    else:
        points.append(None)

# 绘制骨架
imgskeleton = copy.deepcopy(imgOri)
for pair in POSE_PAIRS:
    partA = pair[0]
    partB = pair[1]
    if points[partA] and points[partB]:
        cv2.line(imgskeleton, points[partA], points[partB], (0, 255, 255), 2)
        cv2.circle(imgskeleton, points[partA], 8, (0, 0, 255), thickness=-1, lineType=cv2.FILLED)
        cv2.circle(imgskeleton, points[partB], 8, (0, 0, 255), thickness=-1, lineType=cv2.FILLED)
# 保存关键点和骨架图
cv2.imwrite('handkeypoint.png', imgkp)
cv2.imwrite('imgskeleton.png', imgskeleton)

上板运行

在开发板运行的程序与上述推理代码差异不大,注意好模型的输入数据格式即可,这里要注意,输出的outputs与docker中有差异,要做output = (outputs[0].buffer,)转换,这样可以直接兼容后面的后处理部分,进而生成结果图。

import numpy as np
import cv2
import os
from hobot_dnn import pyeasy_dnn as dnn
import copy

def get_hw(pro):
    if pro.layout == "NCHW":
        return pro.shape[2], pro.shape[3]
    else:
        return pro.shape[1], pro.shape[2]

# img_path 图像完整路径
img_path = '20220806023323.jpg'
# model_path 量化模型完整路径
model_path = 'handkpdet.bin'

# 1. 加载模型,获取所需输出HW
models = dnn.load(model_path)
model_h, model_w = get_hw(models[0].inputs[0].properties)

# 2 加载图像,根据前面yaml,量化后的模型以BGR NHWC形式输入
imgOri = cv2.imread(img_path)
img = cv2.resize(imgOri, (model_w, model_h))

# 3 模型推理
t1 = cv2.getTickCount()
outputs = models[0].forward(img)
t2 = cv2.getTickCount()
output = (outputs[0].buffer,)
print(outputs[0].buffer.shape)
# (1, 22, 46, 46)
print('time consumption {0} ms'.format((t2-t1)*1000/cv2.getTickFrequency()))


# 4 检测结果后处理
# 绘制关键点
nPoints = 22
threshold = 0.1
POSE_PAIRS = [[0, 1], [1, 2], [2, 3], [3, 4], [0, 5], [5, 6], [6, 7], [7, 8], [0, 9], [9, 10], [10, 11], [11, 12],
              [0, 13], [13, 14], [14, 15], [15, 16], [0, 17], [17, 18], [18, 19], [19, 20]]

imgh, imgw = imgOri.shape[:2]
points = []
imgkp = copy.deepcopy(imgOri)
for i in range(nPoints):
    probMap = output[0][0, i, :, :]
    probMap = cv2.resize(probMap, (imgw, imgh))
    minVal, prob, minLoc, point = cv2.minMaxLoc(probMap)

    if prob > threshold:
        cv2.circle(imgkp, (int(point[0]), int(point[1])), 8, (0, 255, 255), thickness=-1, lineType=cv2.FILLED)
        cv2.putText(imgkp, "{}".format(i), (int(point[0]), int(point[1])), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2,
                    lineType=cv2.LINE_AA)
        points.append((int(point[0]), int(point[1])))
    else:
        points.append(None)

# 绘制骨架
imgskeleton = copy.deepcopy(imgOri)
for pair in POSE_PAIRS:
    partA = pair[0]
    partB = pair[1]
    if points[partA] and points[partB]:
        cv2.line(imgskeleton, points[partA], points[partB], (0, 255, 255), 2)
        cv2.circle(imgskeleton, points[partA], 8, (0, 0, 255), thickness=-1, lineType=cv2.FILLED)
        cv2.circle(imgskeleton, points[partB], 8, (0, 0, 255), thickness=-1, lineType=cv2.FILLED)
# 保存关键点和骨架图
cv2.imwrite('handkeypoint.png', imgkp)
cv2.imwrite('imgskeleton.png', imgskeleton)

我自己拍了两张图进行测试,第一排是晚上拍的,手指头有点串味哈哈,整体检测耗时在480ms左右,网络深度没有yolo高,也许是横向的特征比较多。

28.png

原作者:小玺玺
原链接:本文转自地平线开发者社区(详细文档及代码点击此处一键直达)
推荐阅读
关注数
1248
内容数
65
我们的使命是 AI 赋能万物 共创智能未来。我们致力于打造最实用、最好用、最易用的边缘 AI 开发平台。通过地平线自研的高算力 AI 芯片,开发工具,让更多的 AI 开发者,中小企业,学习和加速 AI 产品开发和创新!
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息