资讯 社区 文档
技术能力
语音技术
文字识别
人脸与人体
图像技术
语言与知识
视频技术

EasyEdge平台EdgeBoard适配SDK开发文档

简介

本文档介绍 EasyEdge/EasyDL在EdgeBoard®边缘计算盒/Lite计算卡上的专用软件的使用流程。

EdgeBoard系列硬件可直接应用于AI项目研发与部署,具有高性能、易携带、通用性强、开发简单等四大优点。

详细硬件参数请在AI市场浏览。

EdgeBoard产品使用手册:https://ai.baidu.com/ai-doc/HWCE/Yk3b86gvp

软核版本

CPP-SDK版本 对应软核版本
1.3.2、1.3.4、1.3.5 1.8.1
1.3.0、1.3.1、1.3.2、1.3.4 1.8
0.5.7-1.2.1 1.5
0.5.2+ 1.4

SDK升级需配合EdgeBoard硬件软核升级,建议升级软核为SDK对应版本,否则可能出现结果错误或者其他异常。

可以通过dmesg | grep "DRIVER Version"命令获取EdgeBoard当前的软核版本

获取序列号

在EasyEdge/EasyDL生成SDK后,点击获取序列号进入控制台获取。EasyEdge控制台、EasyDL控制台、BML控制台

如果是在AI市场购买的EdgeBoard-FZ软硬一体方案,也可以在AI市场订单详情获取序列号。

更换序列号、更换设备时,首次使用需要联网激活。激活成功之后,有效期内可离线使用。

Release Notes

注意*:升级完成相应的软核之后需要重启机器生效。 sdk对应的软核说明:

部署包中包含多版本SDK:

  • baidueasyedge_linux_cpp_aarch64_EdgeBoardFZ1.8*:适用于EdgeBoard 1.5+软核
  • baidueasyedge_linux_cpp_aarch64_EdgeBoardFZ1.4*:适用于EdgeBoard 1.4的软核

如果客户使用的软核是mobile版本的,需要使用FZ1.4的SDK;如果不是mobile 版本,可以选择FZ1.5+(目前最高版本更新至1.8.1)版本的SDK使用。

1.5+版本的软核以及cpp-sdk更新情况如下表所示:

时间 cpp-sdk版本 说明 EdgeBoardFZ1.5+(非mobile版本)对应的软核版本以及特性
2021.12.20 1.3.5 升级预测引擎为PaddleLite 1.8.1 https://ai.baidu.com/ai-doc/HWCE/Fkuqounlk
2021.10.15 1.3.2、1.3.4、1.3.4 推理库支持了Ubuntu18.04文件系统 https://ai.baidu.com/ai-doc/HWCE/Fkuqounlk(含有EB升级Ubuntu18.04系统的步骤)
2021.06.29 1.3.1 视频流解析支持分辨率调整;预测引擎升级 https://ai.baidu.com/ai-doc/HWCE/Ikqgcqt5x
2021.05.14 1.3.0 新增视频流接入支持;展示已发布模型性能评估报告 https://ai.baidu.com/ai-doc/HWCE/Ikqgcqt5x
2021.05.14 1.2.1 功能无更新 https://ai.baidu.com/ai-doc/HWCE/okqiwkm32
2020.10.29 0.5.7 预测引擎切换为PaddleLite 1.5 -
2019.12.27 0.4.5 引擎升级,支持zu5/zu3,支持EasyDL 高精度检测模型 -
2019.07.25 0.4.0 EdgeBoard SDK Release! -

mobile软核以及cpp-sdk更新情况如下表所示: | 时间 | cpp-sdk版本 | 说明 |EdgeBoardFZ1.4(mobile版本)对应的软核版本以及特性 | | --- | ---- | ---- |---- |---- | |2021.05.14|1.2.1|功能无更新|https://ai.baidu.com/ai-doc/HWCE/okqiwkm32|https://ai.baidu.com/ai-doc/HWCE/Lkqiwlziw||

快速开始

开发者从EasyEdge/EasyDL下载的软件部署包中,包含了简单易用的SDK和Demo。只需简单的几个步骤,即可快速部署运行EdgeBoard计算盒。

SDK文件结构

baidu_easyedge_linux_cpp_aarch64_EdgeBoardFZ1.8_*
├── ReadMe.txt
├── bin
│   ├── easyedge_image_inference
│   ├── easyedge_serving
│   └── easyedge_video_inference
├── include
│   └── easyedge
├── lib
│   ├── libeasyedge.so -> libeasyedge.so.1
│   ├── libeasyedge.so.1 -> libeasyedge.so.1.3.1
│   ├── libeasyedge.so.1.3.1
│   ├── libeasyedge_static.a
│   ├── libeasyedge_videoio.so -> libeasyedge_videoio.so.1
│   ├── libeasyedge_videoio.so.1 -> libeasyedge_videoio.so.1.3.1
│   ├── libeasyedge_videoio.so.1.3.1
│   ├── libeasyedge_videoio_static.a
│   ├── libpaddle_full_api_shared.so -> libpaddle_full_api_shared.so.1.8.1
│   ├── libpaddle_full_api_shared.so.1.8.1
│   ├── libverify.so -> libverify.so.1
│   ├── libverify.so.1 -> libverify.so.1.0.0
│   └── libverify.so.1.0.0
├── now_sre.log
├── src
│   ├── CMakeLists.txt
│   ├── cmake
│   ├── common
│   ├── demo_image_inference
│   ├── demo_serving
│   └── demo_video_inference
└── thirdparty
    └── opencv

1.1.0+的SDK自带OpenCV,src编译的时候会引用thirdparty/opencv路径下的头文件和库文件。

Demo使用流程

用户在AI市场购买计算盒之后,请参考以下步骤进行集成和试用。

1. 将计算盒连接电源

指示灯亮起,等待约1分钟。

  • 参考EdgeBoard使用文档配置网口或串口连接。登录EdgeBoard计算盒。
  • 加载驱动(开机加载一次即可)。
insmod /home/root/workspace/driver/{zu9|zu5|zu3}/fpgadrv.ko

根据购买的版本,选择合适的驱动。若未加载驱动,可能报错:

Failed to to fpga device: -1
  • 设置系统时间(系统时间必须正确)
date --set "2019-5-18 20:48:00"

2. (可选)启动HTTP服务

部署包中附带了HTTP服务功能,开发者可以进入SDK根目录,运行easyedge_serving程序启动HTTP服务。

# ./easyedge_serving {RES目录} {序列号} {绑定的host,默认0.0.0.0} {绑定的端口,默认24401}
cd ${SDK_ROOT}
export LD_LIBRARY_PATH=./lib
./demo/easyedge_serving ../../../RES  "1111-1111-1111-1111"

日志显示

2019-07-18 13:27:05,941 INFO [EasyEdge] [http_server.cpp:136] 547974369280 Serving at 0.0.0.0:24401

则启动成功。此时可直接在浏览器中输入http://{EdgeBoard计算盒ip地址}:24401/,在h5中测试模型效果。

同时,可以调用HTTP接口来访问盒子。具体参考下文接口说明。

EdgeBoard HTTP Server 目前使用的是单线程处理请求。

3. 使用序列号激活

修改demo/demo.cpp,将前面申请的序列号填入:

global_controller()->set_licence_key

4. 编译运行Demo

编译:

cd src
mkdir build && cd build
cmake .. && make

运行

./easyedge_image_inference  {RES资源文件夹路径}  {测试图片路径}

便可看到识别结果。

使用说明

使用流程

首次使用需要联网激活。激活成功之后,有效期内可离线使用。

  1. 设置序列号global_controller()->set_licence_key
  2. 配置PaddleFluidConfig
  3. 新建Predictor :global_controller()->CreateEdgePredictor(config);
  4. 初始化 predictor->init()
  5. 传入图片开始识别predictor->infer(img, ...);

目前EdgeBoard暂不支持并行多模型计算。

接口说明

预测图片

    /**
     * @brief 同步预测接口
     * inference synchronous
     * Supported by most chip and engine
     * @param image: must be BGR , HWC format (opencv default)
     * @param result
     * @param threshold
     * @return
     */
    virtual int infer(
            cv::Mat &image, std::vector<EdgeResultData> &result, float threshold = 0.1
    ) = 0;

识别结果说明

EdgeResultData中可以获取对应的分类信息、位置信息。

struct EdgeResultData {
    int index;  // 分类结果的index
    std::string label;  // 分类结果的label
    float prob;  // 置信度

    // object detection field
    float x1, y1, x2, y2;  // (x1, y1): 左上角, (x2, y2): 右下角; 均为0~1的长宽比例值。
};

关于矩形坐标

x1 * 图片宽度 = 检测框的左上角的横坐标

y1 * 图片高度 = 检测框的左上角的纵坐标

x2 * 图片宽度 = 检测框的右下角的横坐标

y2 * 图片高度 = 检测框的右下角的纵坐标

可以参考demo文件中使用opencv绘制矩形的逻辑。

HTTP 私有服务请求说明

http 请求参数

URL中的get参数:

参数 说明 默认值
threshold 阈值过滤, 0~1 0.1

HTTP POST Body即为图片的二进制内容(无需base64, 无需json)

~~~codeset
import requests

with open('./1.jpg', 'rb') as f:
    img = f.read()
    result = requests.post(
	    'http://127.0.0.1:24401/',
	    params={'threshold': 0.1},
	    data=img).json()
    
    FileStream fs = new FileStream("./img.jpg", FileMode.Open);
    BinaryReader br = new BinaryReader(fs);
    byte[] img = br.ReadBytes((int)fs.Length);
    br.Close();
    fs.Close();
    string url = "http://127.0.0.1:8402?threshold=0.1";
    HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(url);
    request.Method = "POST";
    Stream stream = request.GetRequestStream();
    stream.Write(img, 0, img.Length);
    stream.Close();
    
    WebResponse response = request.GetResponse();
    StreamReader sr = new StreamReader(response.GetResponseStream());
    Console.WriteLine(sr.ReadToEnd());
    sr.Close();
    response.Close();
"""
需要安装curl
"""
#include <sys/stat.h>
    #include <curl/curl.h>
    #include <iostream>
    #include <string>
    #define S_ISREG(m) (((m) & 0170000) == (0100000))   
    #define S_ISDIR(m) (((m) & 0170000) == (0040000))   
    
    size_t write_callback(void *ptr, size_t size, size_t num, void *data) {
        std::string *str = dynamic_cast<std::string *>((std::string *)data);
        str->append((char *)ptr, size*num);
        return size*num;
    }
    
    int main(int argc, char *argv[]) {
        const char *post_data_filename = "./img.jpg";
        FILE *fp = NULL;
        std::string response;
        struct stat stbuf = { 0, };
        fp = fopen(post_data_filename, "rb");
        if (!fp) {
            fprintf(stderr, "Error: failed to open file "%s"
    ", post_data_filename);
            return -1;
        }
        if (fstat(fileno(fp), &stbuf) || !S_ISREG(stbuf.st_mode)) {
            fprintf(stderr, "Error: unknown file size "%s"
    ", post_data_filename);
            return -1;
        }
        CURL *curl;
        CURLcode res;
        curl_global_init(CURL_GLOBAL_ALL);
        curl = curl_easy_init();
        if (curl != NULL) {
            curl_easy_setopt(curl, CURLOPT_URL, "http://127.0.0.1:24401?threshold=0.1");
            curl_easy_setopt(curl, CURLOPT_POST, 1L);
            curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE,(curl_off_t)stbuf.st_size);
            curl_easy_setopt(curl, CURLOPT_READDATA, (void *)fp);
    		curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
            curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
            res = curl_easy_perform(curl);
    
            if (res != CURLE_OK) {
                fprintf(stderr, "curl_easy_perform() failed: %s
    ", curl_easy_strerror(res));
            }
            std::cout << response << std::endl; // response即为返回的json数据
            curl_easy_cleanup(curl);
        }
        curl_global_cleanup();
        fclose(fp);
        return 0;
    }
[Java请求示例](http://ai.baidu.com/forum/topic/show/943765)

####  http 返回数据

| 字段       | 类型说明 | 其他                                                         |
| ---------- | -------- | ------------------------------------------------------------ |
| error_code | Number   | 0为成功,非0参考message获得具体错误信息                       |
| results    | Array    | 内容为具体的识别结果。其中字段的具体含义请参考`预测图像-返回格式`一节 |
| cost_ms    | Number   | 预测耗时ms,不含网络交互时间                                 |



返回示例

```json
{
    "cost_ms": 52,
    "error_code": 0,
    "results": [
        {
            "confidence": 0.94482421875,
            "index": 1,
            "label": "IronMan",
            "x1": 0.059185408055782318,
            "x2": 0.18795496225357056,
            "y1": 0.14762254059314728,
            "y2": 0.52510076761245728
        },
        {
            "confidence": 0.94091796875,
            "index": 1,
            "label": "IronMan",
            "x1": 0.79151463508605957,
            "x2": 0.92310667037963867,
            "y1": 0.045728668570518494,
            "y2": 0.42920106649398804
        }
      ]
}
```

### 预测视频

SDK 提供了支持摄像头读取、视频文件和网络视频流的解析工具类`VideoDecoding`,此类提供了获取视频帧数据的便利函数。通过`VideoConfig`结构体可以控制视频/摄像头的解析策略、抽帧策略、分辨率调整、结果视频存储等功能。对于抽取到的视频帧可以直接作为SDK infer 接口的参数进行预测。

* 接口

class`VideoDecoding`:
```
    /**
     * @brief 获取输入源的下一帧
     * @param frame_tensor
     * @return
     */
    virtual int next(FrameTensor &frame_tensor) = 0;

    /**
     * @brief 显示当前frame_tensor中的视频帧
     * @param frame_tensor
     * @return
     */
    virtual int display(const FrameTensor &frame_tensor) = 0;

    /**
     * @brief 将当前frame_tensor中的视频帧写为本地视频文件
     * @param frame_tensor
     * @return
     */
    virtual int save(FrameTensor &frame_tensor) = 0;

    /**
     * @brief 获取视频的fps属性
     * @return
     */
    virtual int get_fps() = 0;
     /**
      * @brief 获取视频的width属性
      * @return
      */
    virtual int get_width() = 0;

    /**
     * @brief 获取视频的height属性
     * @return
     */
    virtual int get_height() = 0;
```

struct `VideoConfig`
```
/**
 * @brief 视频源、抽帧策略、存储策略的设置选项
 */
struct VideoConfig {
    SourceType source_type;            // 输入源类型
    std::string source_value;          // 输入源地址,如视频文件路径、摄像头index、网络流地址
    int skip_frames{0};                // 设置跳帧,每隔skip_frames帧抽取一帧,并把该抽取帧的is_needed置为true
    int retrieve_all{false};           // 是否抽取所有frame以便于作为显示和存储,对于不满足skip_frames策略的frame,把所抽取帧的is_needed置为false
    int input_fps{0};                  // 在采取抽帧之前设置视频的fps
    Resolution resolution{Resolution::kAuto}; // 采样分辨率,只对camera有效

    bool enable_display{false};
    std::string window_name{"EasyEdge"};
    bool display_all{false};           // 是否显示所有frame,若为false,仅显示根据skip_frames抽取的frame

    bool enable_save{false};
    std::string save_path;             // frame存储为视频文件的路径
    bool save_all{false};              // 是否存储所有frame,若为false,仅存储根据skip_frames抽取的frame

    std::map::string, std::string> conf;
};
```

`source_type`:输入源类型,支持视频文件、摄像头、网络视频流三种,值分别为1、2、3。
`source_value`: 若`source_type`为视频文件,该值为指向视频文件的完整路径;若`source_type`为摄像头,该值为摄像头的index,如对于`/dev/video0`的摄像头,则index为0;若`source_type`为网络视频流,则为该视频流的完整地址。
`skip_frames`:设置跳帧,每隔skip_frames帧抽取一帧,并把该抽取帧的is_needed置为true,标记为is_needed的帧是用来做预测的帧。反之,直接跳过该帧,不经过预测。
`retrieve_all`:若置该项为true,则无论是否设置跳帧,所有的帧都会被抽取返回,以作为显示或存储用。
`input_fps`:用于抽帧前设置fps。
`resolution`:设置摄像头采样的分辨率,其值请参考`easyedge_video.h`中的定义,注意该分辨率调整仅对输入源为摄像头时有效。
`conf`:高级选项。部分配置会通过该map来设置。

*** 注意:*** 
1. 如果使用`VideoConfig`的`display`功能,需要自行编译带有GTK选项的opencv,默认打包的opencv不包含此项。
2. 使用摄像头抽帧时,如果通过`resolution`设置了分辨率调整,但是不起作用,请添加如下选项:
```
video_config.conf["backend"] = "2";
```
3.部分设备上的CSI摄像头尚未兼容,如遇到问题,可以通过工单、QQ交流群或微信交流群反馈。

具体接口调用流程,可以参考SDK中的`demo_video_inference`。

### 错误说明

SDK所有主动报出的错误,均覆盖在`EdgeStatus`枚举中。同时SDK会有详细的错误日志,开发者可以打开Debug日志查看额外说明:
```cpp
EdgeLogConfig log_config;
log_config.enable_debug = true;
global_controller()->set_log_config(log_config);
```

## FAQ

### 1. 如何处理一些 undefined reference?

> 如:undefined reference to `curl_easy_setopt@CURL_OPENSSL_3'

可以通过安装`libcurl3 libcurl-openssl1.0-dev`来解决。
如果开发者想不想使用低版本的openssl(如Ubuntu 18.04), 可以link静态库`easyedge_static.a`,自己指定需要的Library的版本。

示例:修改CMakeList.txt

```bash
find_package(CURL REQUIRED)
target_link_libraries(easyedge_demo  ${OpenCV_LIBS} easyedge_static pthread ${CURL_LIBRARIES} paddle-mobile)
```
### 2. error while loading shared libraries: libeasyedge.so.0.4.0: cannot open shared object file: No such file or directory

类似错误包括`libpaddle-mobile.so`找不到。

直接运行SDK自带的二进制可能会有这个问题,设置LD_LIBRARY_PATH为SDK部署包中的lib目录即可。
开发者自行使用CMake编译的二进制可以有效管理.so的依赖。

### 3. 使用libcurl请求http服务时,速度明显变慢

这是因为libcurl请求continue导致server等待数据的问题,添加空的header即可
```bash
headers = curl_slist_append(headers, "Expect:");
```
### 4. 预测过程中报内存不足“Killed”

此问题仅出现在ZU5,因为FZ5A带vcu,给他预留的内存过大导致,如果用不到VCU可以把这部分改小。修改/run/media/mmcblk1p1/uEnv.txt:
```
ethaddr=00:0a:35:00:00:09
uenvcmd=fatload mmc 1 0x3000000 image.ub && bootm 0x3000000

bootargs=earlycon console=ttyPS0,115200 clk_ignore_unused cpuidle.off=1 root=/dev/mmcblk1p2 rw rootwait cma=128M
```
注意中间空行要保留。

### 5. 预测结果异常

如果购买的计算盒较早,驱动文件较旧,而SDK比较新(或SDK比较旧,但是计算盒较新),可能出现结果异常,如结果均为空或者`nan`。
请参考“内核版本”小节更新内核和驱动版本。

### 6. 编译过程报错file format not recognized

```
libeasyedge.so: file format not recognized; treating as linker script
```
下载的SDK zip包需要放到板子内部后,再解压、编译。

### 7. 提示 driver_version(1.4.0) not match paddle_lite_version(1.5.1)
需更新驱动,否则可能导致结果异常。参考“内核版本”小节。

### 8. 运行SDK报错 Authorization failed

#### 情况一:日志显示 `Http perform failed: null respond`

在新的硬件上首次运行,必须联网激活。

SDK 能够接受`HTTP_PROXY` 的环境变量通过代理处理自己的网络请求。如
```bash
export HTTP_PROXY="http://192.168.1.100:8888"
./easyedge_demo ...
```

#### 情况二:日志显示`failed to get/check device id(xxx)`或者`Device fingerprint mismatch(xxx)`

此类情况一般是设备指纹发生了变更,包括(但不局限于)以下可能的情况:

  * MAC地址变化
  * 磁盘变更
  * BIOS重刷

以及系统相关信息。

遇到这类情况,请确保硬件无变更,如果想更换序列号,请先删除  `~/.baidu/easyedge 目录`,再重新激活。
上一篇
Linux-Python
下一篇
EasyEdge平台Atlas-300适配SDK开发文档