实操丨如何在EAIDK上部署Tengine开发AI应用之物体检测应用入门(C++)
作者:   浏览量:255次   发表时间:2020-05-18 16:42:15

前言:

近期推出的嵌入式AI系列直播公开课受到广大开发者的喜爱,并收到非常多的反馈信息,其中对如何在EAIDK上面部署Tengine开发AI应用感兴趣的开发者不在少数,我们将分2期以案例实操的形式详细介绍。

Tengine开源版本GitHub链接 https://github.com/OAID/Tengine

欢迎Git Clone!技术交流QQ群829565581,群里各位大佬坐镇,日常技术讨(shui)论(qun)~~

AI推动了社会智能化的发展进程,极大的提高了人们的工作效率,节省了工作时间。在AI赋能社会的同时,作为开发者也要了解AI如何从0到1,1到2,乃至2到3等的过程。

作为刚接触AI开发的开发者而言,了解AI的开发框架是至关重要的。AI的开发是一项庞大的系统性的工程,开发人员需要掌握一定的数学原理,编程能力,建模能力与数据分析能力等。AI开发的整体框架如图1所示:


图1 AI开发框架图

对于新开发者而言,先从相对简单的部署模型阶段学习,可以快速了解整个AI系统的运作,以及了解AI如何在人们的日常生活中产生实际作用。

一、EAIDK常见的模型部署方式

对于现在市场上的AI框架而言,具有群雄争锋的局面,有Caffe,PyTorch,TensorFlow,MxNet,ONNX等一系列耳熟能详的训练与推理框架,但经过这些训练框架所训练出的模型在性能,精度,以及不同平台的适应性也各有千秋。对于初学者而言,从推理框架入手是一种很好的选择,在学习推理框架的同时,可以帮助初学者了解AI的整套体系是如何运作的。

在选择推理框架的时候需要结合现今模型部署的环境进行考虑。往日的AI模型训练常见于PC端,其往往忽略运行模型对于资源的调度,模型对于其加载设备功耗的使用。在AI的发展路程上,越来越多的开发者希望AI能融入我们的生活,所以对于PC端的推理框架部署则越来越不被人们所接受。AI从往日的PC端,需要渐渐的转化到可移动设备端。在现今生活中,通过社会各界不断的努力,AI逐渐被人们所熟悉,例如人脸门禁检测系统,工业自动化中的物体分拣检测系统,以及现在众所周知的无人驾驶项目等一系列贴近人们生活的具体项目。

EAIDK 常见的模型部署方式如下:

部署原始训练框架

直接部署原始训练框架具有如下特点:

1) 需安装如TensorFlow/PyTorch/Caffe等环境;

2) 推理性能差;

3) 很多冗余功能;

4) 内存占用大;

手动模型重构

不依赖框架,手写C/C++代码,实现计算图,并导入权重数据。该种方法需要构造者对模型有充分的了解。对初学者而言,有一定的技术难度。

使用TensorFlow-Lite/PyTorch-Mobile等训练框架的部署引擎

除了直接部署原始的训练框架,训练框架也有一些部署引擎,如TensorFlow 训练框架有TensorFlow-Lite ,PyTorch 训练框架有PyTorch-Mobile. 使用这些部署引擎具有如下问题:

1) 只支持自己框架训练的模型;

2) 需要进行模型转换;

3) 支持硬件/OS有限;

使用Tengine推理框架进行部署

Tengine推理框架具有如下特点:

1) 可以直接加载主流框架模型,也支持主流框架模型直接转换为Tengine模型, Tengine提供一键转换工具;

2) 只依赖C/C++库,无任何第三方库依赖;

3) 自带图像处理已支持图像缩放、图像像素格式转换、旋转、镜像翻转、映射、腐蚀膨胀、阈值处理、高斯模糊处理、图像编解码等;

4) 自带语音处理(Roadmap)支持FFT/IFFT、MFCC等信号处理方式,方便完成噪声抑制、回声清除等语音处理工作;

5) 支持Android/Linux/RTOS/裸板环境;

6) PyThon/C++/AAP 等API接口,方便不同语言调用;

7) 高性能计算,在板子上也可以跑出炫酷的Demo;

8) 无需手动安装,EAIDK自带Tengine环境,直接使用即可。


二、Tengine 环境搭建

a. Tengine预编译库

EAIDK自带Tengine预编译库,无需手动编译。

Tengine预编译库路径:

/usr/local/AID/Tengine/

查看预编译库:

[openailab@localhost ~]$ ls /usr/local/AID/Tengine/lib/

libhclcpu.so libtengine.so

[openailab@localhost ~]$ ls /usr/local/AID/Tengine/include/

cpu_device.h tengine_c_api.h tengine_c_compat.h tengine_operations.h


b. Tengine版本信息

输入下列命令可以查看Tengine版本信息:

sudo dnf info Tengine

详细信息如下:

Name : tengine

Version : 1.7.1

Release : 1.openailab.fc28

Arch : aarch64

Size : 5.8 M

Source : tengine-1.7.1-1.openailab.fc28.src.rpm

Repo : @System

Summary : openailab tengine library

URL : https://github.com/OAID/Tengine

EAIDK自带Tengine版本为1.7.1,后续Demo也是基于Tengine1.7.1版本,如果查询版本较低,需要输入下列命令升级Tengine版本:

sudo dnf updateTengine


三、Tengine物体检测Demo(C++)

a. Demo 简介

对EAIDK初学者进行物体检测的环境搭建与运行,其目的是让初学者对Tengine在物体检测方面进行学习,为后期开发做好铺垫.

此指导教学为运用EAIDK自带后方摄像头进行图像的拍摄,可对图像中的物体进行检测,例如人,瓶子等.在此教学中运用了Tengine相关的函数以及运用OpenCV对图像检测方面的前后处理进行基础的教学指导.


b. 运行环境

安装环境需要在root权限下:

sudo su

i. 安装OpenCV

依据以下指令安装OpenCV依赖库

sudo yum install opencv

sudo yum install opencv-devel

ii. 安装Protobuf

查询相关protobuf:rpm -qa | grep protobuf,应显示如下:

protobuf-3.5.0-4.fc28.aarch64

protobuf-compiler-3.5.0-4.fc28.aarch64

protobuf-devel-3.5.0-4.fc28.aarch64

如无上述显示,则运行如下指令:

sudo dnf install protobuf-compiler-3.5.0-4.fc28.aarch64

sudo dnf install protobuf-3.5.0-4.fc28.aarch64

sudo dnf install protobuf-devel-3.5.0-4.fc28.aarch64

iii. 安装OpenCV Video关联库

yum install v4l-utils


c. Demo 编译与运行效果

i. 创建目录

目录Demo下进行编写,其路径:

/home/openailab/Demo

在上述文件夹下创建文件目录如下:

[openailab@localhost ~]$ mkdir mobilenetSSD

在文件夹中放入获取到的Tengine_Demo包中的mobilenet_ssd.cpp文件;

创建models文件夹.将模型MobileNetSSD_deploy.caffemodel MobileNetSSD_deploy.prototxt 以及convert_model_to_tm 工具放入到models文件夹中。

[openailab@localhost ~]$ cd mobilenetSSD

[openailab@localhost ~]$ mkdir models

ii. 转换模型

在models 文件夹中,执行如下命令,将caffe模型转换为Tengine模型。

export LD_LIBRARY_PATH=/usr/local/AID/Tengine/lib/

./convert_model_to_tm -f caffe -p models/MobileNetSSD_deploy.prototxt -m

models/MobileNetSSD_deploy.caffemodel -o models/tm_mssd.tmfile

转换成功后的Tengine模型在models目录下。

iii. 编译

回到 mobilenetSSD 目录,编译可执行文件。

g++ mobilenet_ssd.cpp `pkg-config --libs --cflags opencv` -I

/usr/local/AID/Tengine/include/ -L /usr/local/AID/Tengine/lib -l tengine -o mssd

iv. 设置多核运行

设置高性能运行程序可用以下环境变量来设定,默认为1核运行,可设置成6核运行该程序:

export TENGINE_CPU_LIST=0,1,2,3,4,5

v. 运行

运行:

./mssd


运行结果在EAIDK上显示,如图3所示:



d. Demo 代码解析

此Demo通过调用Tengine各接口进行读取模型、图的创建等功能,其软件流程图如图4所示:


图 4 C++ Demo 流程图

库文件包含

#include

#include

#include

#include

#include "opencv2/opencv.hpp"

#include "opencv2/core/core.hpp"

#include "opencv2/highgui/highgui.hpp"

#include

#include "tengine_c_api.h"

所需要包含的头文件,可以对此Demo文件中所有到的函数进行支持

指定文件路径

int main(int argc, char* argv[])

{

int ret = -1;

std::string model_file = "models/tm_mssd.tmfile ";

model_file 为Tengine模型路径,由convert tool将caffe模型转换为Tengine模型后放在与运行文件同级的文件夹models下

初始化Tengine

if(init_tengine() < 0) {

std::cout << " init tengine failed\n";

return 1; }

运用init_Tengine函数来初始化Tengine,如果运行失败,则会返回-1,if函数则是判断执行此函数是否成功,不成功则返回 init Tengine failed

创建Tengine图

graph_t graph = create_graph(nullptr, "tengine", model_file.c_str());

if(graph == nullptr) {

std::cout << "Create graph failed\n";

std::cout << " ,errno: " << get_tengine_errno() << "\n";

return 1; }

create_graph为创建Tengine框架的运行图,可适用于多种框架。

nullptr:内容为空,首先建立空图,然后导入解析模型后的信息至Tengine的图中

tengine:模型类型,当前测试为tengine模型

proto_file:模型文件,如caffe为prototxt文件

model_file:   模型文件,如caffe为 caffemodel文件

获取Tegnine 输入节点的信息

int node_idx = 0;

int tensor_idx = 0;

tensor_t input_tensor = get_graph_input_tensor(graph, node_idx, tensor_idx);

if(input_tensor == nullptr) {

std::printf("Cannot find input tensor,node_idx: %d,tensor_idx: %d\n", node_idx, tenso

r_idx);

return -1; }

get_graph_input_tensor函数是获取Tengine input节点信息函数,其返回类型为tensor_t,包含tensor中所具有的所有信息,例如节点号,节点尺寸,节点数据等,如返回为空,则表示无此节点,获取失败

Node_idx: Tengine graph节点标号,起始输入节点为0;

Tensor_idx: Tengine graph节点tensor标号,如果只有单输入,则为0.

设置节点信息与内容

int img_h = 300;

int img_w = 300;

int channel = 3;

int img_size = img_h * img_w * channel;

float* input_data = ( float* )malloc(sizeof(float) * img_size);

img_h, img_w, channel 为图片信息:宽,高,层数信息

img_size: 像素点所占总字节数

input_data为输入到模型中的输入数据变量,malloc为为input_data申请内存

int dims[] = {1, channel, img_h, img_w};

set_tensor_shape(input_tensor, dims, 4);

ret = prerun_graph(graph);

if(ret != 0) {

std::cout << "Prerun graph failed, errno: " << get_tengine_errno() << "\n";

return 1; }

dims[] 为输入数据维度信息,因为模型要求为4维输入数据,所以在原图像维度上增加一维,补1即可。

当设定好维度信息后,可用set_tensor_shape来设置输入数据尺寸,input_tensor为所需要设置tensor,dims为维度信息,4表示设置多少个维度。

prerun_graph函数则是对Tengine graph所有节点所需要的内存空间进行内存申请。

获取图像数据

cv::Mat frame;

cv::VideoCapture capture(0);

运用cv库来读取摄像头数据。Frame为cv图像数据变量名。VideoCapture函数接口可获取摄像头数据。capture(index)为连接摄像头端口号,此Demo中index = 0;如不知道开发板摄像头信息,可用如下命令查询查询所有端口:

ls /dev/video*



用命令来查询端口设备信息:

v4l2-ctl -d /dev/video4 --all

再用如下命令指定摄像头设备端口:

v4l2-ctl -d /dev/video4

注:具体端口号依照当前环境来设置

运用while(1)来连续获取图像数据,如下所示:

while(1){

capture >> frame;

其中capture>>frame为逐帧读取图像数据,然后就是对图像进行帧处理。

图像帧处理

图像帧处理为单独函数,在main函数外编写,在main函数内调用,调用函数如下:

get_input_data_ssd(frame, input_data, img_h, img_w);

传入数据为frame一帧图像,input_data 所需要保存处理后数据的内存,img_h,img_w所需图像的宽,高.

void get_input_data_ssd(cv::Mat img, float* input_data, int img_h, int img_w) {

cv::resize(img, img, cv::Size(img_h, img_w));

img.convertTo(img, CV_32FC3);

float* img_data = ( float* )img.data;

int hw = img_h * img_w;

float mean[3] = {127.5, 127.5, 127.5};

for(int h = 0; h < img_h; h++) {

for(int w = 0; w < img_w; w++) {

for(int c = 0; c < 3; c++) {

input_data[c * hw + h * img_w + w] = 0.007843 * (*img_data - mean[c]);

img_data++; } } } }

int main(){

...

}

上述函数为图像的单帧处理。因为摄像头读入的数据尺寸不是符合模型要求,所以运用resize函数来进行尺寸调整。运用OpenCV所读入的数据类型为uint8,但是Tengine所需要的为float类型数据,运用convertTo函数可转换数据类型至float类型。

对数据进行图像预处理,进行图像每行,每列,每一层的循环来对单个数据进行处理。上图中的mean中有三个数值,此三个数值代表每一层中像素点所需要减去的数值,随后对每一个像素点进行扩大或者缩小操作,乘以一个因子,此Demo中的因子为0.007843。

每个模型的mean值与扩大缩小因子都不同,因模型而定。

传入数据至Tengine框架

set_tensor_buffer(input_tensor, input_data, img_size*sizeof(float));

此函数为设置Tengine框架数据,input_tensor为输入tensor变量,input_data为输入数据,img_size*sizeof(float)为所传入数据的总体大小。

运行Tengine

run_graph(graph, 1);

用run_graph来启动Tengine 框架。Graph为前面所创建的Tengine框架。函数中的1为阻塞(需要配置Tengine graph信息),0为非阻塞(运用wait graph来获取Tengine graph结果)


获取输出结果的tensor信息

tensor_t out_tensor = get_graph_output_tensor(graph, 0, 0);

int out_dim[4];

ret = get_tensor_shape(out_tensor, out_dim, 4);

if(ret <= 0) {

std::cout << "get tensor shape failed, errno: " << get_tengine_errno() << "\n";

return 1; }

运用get_graph_output_tensor函数来获取输出结果的tensor信息。Graph为Tengine graph名字,0,0为所输出节点的节点号。

Out_dim为输出数据的尺寸信息,在get_tensor_shape中告知此函数获取多少维度的数据。

输出数据处理

Mobilenet_ssd模型为检测模型网络,对输出数据进行后处理可用于物体的检测及其标定。同预处理相似,在main函数中为调用函数,具体实现在main函数之外。

float* outdata = ( float* )get_tensor_buffer(out_tensor);

int num = out_dim[1];

float show_threshold = 0.5;

post_process_ssd(frame, show_threshold, outdata, num);

通过get_tensor_buffer输出结果,赋值到outdata变量。Show_threahold为检测阈值,当大于此阈值时保留其结果,反之则剔除输出结果。其具体数据处理在post_process_ssd函数中


后处理函数-标记框

#include "tengine_c_api.h"

struct Box

{

float x0;

float y0;

float x1;

float y1;

int class_idx;

float score;

};

在函数外申明一个结构体,结构体中x0,y0为检测框的左上角,x1,y1为检测框的右下角。Class_idx为框的类型,score为标记目标的得分情况,与阈值相比较

void post_process_ssd(cv::Mat img, float threshold, float* outdata, int num) {const char* class_names[] = {"background", "aeroplane", "bicycle", "bird","boat","bottle","bus", "car", "cat", "chair","cow", "diningtable", "dog", "horse","motorbike", "person", "pottedplant", "sheep", "sofa",  “train", "tvmonitor"};int raw_h = img.size().height;int raw_w = img.size().width;std::vector boxes;int line_width = raw_w * 0.005;

Class_names为所能检测的类型种类。Raw_h与raw_w为原图像的高与宽。Boxes存储了最终结果的检测信息。Line_width为所画框的粗细

for(int i = 0; i < num; i++) {

if(outdata[1] >= threshold)

{

Box box;

box.class_idx = outdata[0];

box.score = outdata[1];

box.x0 = outdata[2] * raw_w;

box.y0 = outdata[3] * raw_h;

box.x1 = outdata[4] * raw_w;

box.y1 = outdata[5] * raw_h;

boxes.push_back(box);

printf("%s\t:%.0f%%\n", class_names[box.class_idx], box.score * 100);

printf("BOX:( %g , %g ),( %g , %g )\n", box.x0, box.y0, box.x1, box.y1);

}

outdata += 6; }

此循环遍历所有的输出数据,当输出数据中的分数部分大于阈值时,会把输出数据中 相应的标记框的信息存入boxes变量中。

后处理函数-画框

for(int i = 0; i < ( int )boxes.size(); i++) {

Box box = boxes[i];

cv::rectangle(img, cv::Rect(box.x0, box.y0, (box.x1 - box.x0), (box.y1 - box.y0)), cv::Scalar(255, 255, 0),

line_width);

std::ostringstream score_str;

score_str << box.score;

std::string label = std::string(class_names[box.class_idx]) + ": " + score_str.str();

int baseLine = 0;

cv::Size label_size = cv::getTextSize(label, cv::FONT_HERSHEY_SIMPLEX, 0.5, 1, &baseLine);

cv::rectangle(img,

cv::Rect(cv::Point(box.x0, box.y0 - label_size.height),

cv::Size(label_size.width, label_size.height + baseLine)),

cv::Scalar(255, 255, 0), CV_FILLED);

cv::putText(img, label, cv::Point(box.x0, box.y0), cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 0, 0));

} }

int main(){

...

}

首先遍历boxes变量,对每一个结果进行处理。

对目标用rectangle函数进行画框标记Rect(x0,y0,长,宽,Scalar(框的颜色),框的粗细)

对所标的框进行类型的字符显示,运用string函数进行字符串与分数的合并其结果类似 person:0.982323。随后对字体进行大小,字符的设定,具体可对OpenCV中getTextSize进行查询。随后指定字符所在的位置与具体区域。最后运用putText函数把字符信息打印到图像上。

完成1帧图像处理

release_graph_tensor(out_tensor);

imshow("Mssd", frame);

cv::waitKey(10);

}

当处理完数据后则返回到main函数,随后进行输出节点的内存释放,运用release_graph_tensor对指定tensor的内存进行释放。

Imshow函数则是OpenCV中显示图像的接口函数,waitKey则是每帧间隔所需的间隔时间


程序内存释放

release_graph_tensor(input_tensor);

ret = postrun_graph(graph);

if(ret != 0) {

std::cout << "Postrun graph failed, errno: " << get_tengine_errno() << "\n";

return 1; }

free(input_data);

destroy_graph(graph);

release_tengine();

return 0; }

当程序结束时,则需要对前期申请的内存进行释放。release_graph_tensor对指定tensor进行内存释放。postrun_graph函数则是对prerun_graph函数运行时所申请的内存进行释放。free函数则是对当时申请 的输入数据内存进行释放。destroy_graph函数是对Tengine graph进行销毁处理,最后运用release_Tengine对Tengine所有内存进行释放。