• 实操丨如何在EAIDK上部署Tengine开发AI应用之物体检测应用入门(C++)

    前言:

    近期推出的嵌入式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所有内存进行释放。



  • EAIDK-610-人脸检测系统

    概述:人脸检测系统是以嵌入式人工智能开发套件(EAIDK-610)为基础平台,网络摄像头拍摄区域画面,EAIDK-610实时分析并找出图像中所有的人脸对应的位置,输出人脸图像的位置,并实时跟踪人脸。

    人脸识别是以人脸特征提取方案为例,展示完整的深度学习算法及应用开发过程,以 Light CNN 1 为例 ,从算法的原理及特点开始,不仅介绍了算法训练的过程,也展示了算法在嵌入式平台(EAI610-P0)的部署 。

    人脸检测技术,一般仅指检测出图像中有没有人脸、人脸的位置在哪,而并不包括认出这个人是谁、区分一个人和另一个人等,这些是人脸识别技术要做的进一步工作。从工程的角度讲,人脸检测实际上就是输入一张图片(当然, 也可以是视频流中的一帧图像),然后利用计算机或嵌入式设备如EAIDK等通过各种算法进行运算后,输出一系列的矩形框/椭圆框来标记人脸的位置,输出很多时候会直接显示在原图像上,如下图所示:




    这些人脸框的位置、尺寸、形状等通常也会被输出(比如用一个数组[x,y,width,height]来代表一个矩形框),用来实现各种附加功能。如人脸识别,即是在通过人脸检测技术得到图片中的人脸框后,再对框内的人脸进行识别。



  • EAIDK-610-物体分类检测

    概述:物体分类检测方案是以嵌入式人工智能开发套件(EAIDK-610)为基础平台,通过网络摄像机(IPC)采集目标物体数据,EAIDK-610是对数据进行深度学习,推理运算,并在终端设备上显示结果。

    物体分类检测是在嵌入式平台上进行机器视觉的目标检测,通过深度学习的方法实现对多目标物体进行检测并识别。

    使用Arm的嵌入式人工智能开发套件(EAIDK-610)为基础平台,通过网络摄像机进行视频数据采集,EAIDK-610作为边缘智能处理对视频进行分析,并实时显示分析结果。

    展示方案由三个单元组成:

    网络摄像机(IPC):高清1080P视频编码支持,作为为视频采集单元将采集到的视频信息进行编码后通过网络传输给边缘智能处理单元EAIDK-610;

    EAIDK-610:边缘测智能单元,执行功能包括:接收IPC的视频流并进行解码,视频分析,检测并识别视频中的目标,通过HDMI接口进行实时显示;

    显示终端:对用户显示视频分析(目标检测)结果。


    此方案中显示终端的显示效果如下:

  • EAIDK-610-双路门禁系统

    概述:双路门禁系统是以嵌入式人工智能开发套件(EAIDK-610)为基础平台,人员进门时, 网络摄像机(IPC)连续抓取多张人脸信息,并毫秒级地比对抓取照片和底库信息,比对成功后触发开门动作,人脸抓取、比对结果和人员进门的信息在显示设备上实时展示。

    双路门禁实战方案如下图所示:


    网络摄像机(IPC):高清1080P视频编码支持,作为为视频采集单元将采集到的视频信息进行编码后通过网络传输给边缘智能处理单元EAIDK-610;

    EAIDK-610:边缘测智能单元,执行功能包括:接收IPC的视频流并进行解码,视频分析,检测并识别视频中的目标,通过HDMI接口进行实时显示,通过音响设备播放相应提示音,通过驱动电机开关门;

    显示终端:实时显示捕捉到的人脸图像和人脸对比结果以及视频图像。

    音响:实时播放语音提示音。

    机电控制器:控制门禁开关。





留下您宝贵的建议