6.2. C/C++编程详解

这个章节将会选取 sophon-demo 中的 YOLOV5 检测算法作为示例,说明各个步骤的接口调用和注意事项。

注解

样例代码路径:sophon-demo/sample/YOLOV5

因为SDK支持多种接口风格,因此一个简洁的示例代码不可能面面俱到。故而这个示例程序采用了OPENCV解码 + BMCV图片预处理的组合进行开发,这个组合兼顾了高效和简洁。

我们按照算法的执行先后顺序展开介绍:

  1. 加载bmodel模型

  2. 预处理

  3. 推理

  4. 注意事项

6.2.1. 加载bmodel

 1...
 2
 3BMNNContext(BMNNHandlePtr handle, const char* bmodel_file):m_handlePtr(handle){
 4
 5    bm_handle_t hdev = m_handlePtr->handle();
 6
 7    // init bmruntime contxt
 8    m_bmrt = bmrt_create(hdev);
 9    if (NULL == m_bmrt) {
10    std::cout << "bmrt_create() failed!" << std::endl;
11    exit(-1);
12    }
13
14    // load bmodel from file
15    if (!bmrt_load_bmodel(m_bmrt, bmodel_file)) {
16    std::cout << "load bmodel(" << bmodel_file << ") failed" << std::endl;
17    }
18
19    load_network_names();
20
21}
22
23...
24
25void load_network_names() {
26
27    const char **names;
28    int num;
29
30    // get network info
31    num = bmrt_get_network_number(m_bmrt);
32    bmrt_get_network_names(m_bmrt, &names);
33
34    for(int i=0;i < num; ++i) {
35    m_network_names.push_back(names[i]);
36    }
37
38    free(names);
39}
40
41...
42
43BMNNNetwork(void *bmrt, const std::string& name):m_bmrt(bmrt) {
44    m_handle = static_cast<bm_handle_t>(bmrt_get_bm_handle(bmrt));
45
46    // get model info by model name
47    m_netinfo = bmrt_get_network_info(bmrt, name.c_str());
48
49    m_max_batch = -1;
50    std::vector<int> batches;
51    for(int i=0; i<m_netinfo->stage_num; i++){
52        batches.push_back(m_netinfo->stages[i].input_shapes[0].dims[0]);
53        if(m_max_batch<batches.back()){
54            m_max_batch = batches.back();
55        }
56    }
57    m_batches.insert(batches.begin(), batches.end());
58    m_inputTensors = new bm_tensor_t[m_netinfo->input_num];
59    m_outputTensors = new bm_tensor_t[m_netinfo->output_num];
60    for(int i = 0; i < m_netinfo->input_num; ++i) {
61
62        // get data type
63        m_inputTensors[i].dtype = m_netinfo->input_dtypes[i];
64        m_inputTensors[i].shape = m_netinfo->stages[0].input_shapes[i];
65        m_inputTensors[i].st_mode = BM_STORE_1N;
66        m_inputTensors[i].device_mem = bm_mem_null();
67    }
68
69...
70
71}
72
73...

这个几个函数的用法比较简单和固定,用户可以参考《 BMRUNTIME开发参考手册 》了解更详细的信息。唯一需要强调的是name字符串变量的用法:在推理代码中,模型的唯一标识就是他的name字符串,这个name需要在compile阶段就进行指定,算法程序也需要基于这个name开发;例如,在调用inference接口时,需要使用模型的name作为入参,让runtime作为索引去查询对应的模型,错误的name会造成inference失败。

6.2.2. 预处理

6.2.2.1. 预处理初始化

预处理初始化时,需要提前创建适当的bm_image对象保存中间结果,这样可以节省反复内存申请释放造成的开销,提高算法效率,具体代码如下:

 1...
 2
 3int aligned_net_w = FFALIGN(m_net_w, 64);
 4int strides[3] = {aligned_net_w, aligned_net_w, aligned_net_w};
 5for(int i=0; i<max_batch; i++){
 6
 7    // init bm images for storing results
 8    auto ret= bm_image_create(m_bmContext->handle(), m_net_h, m_net_w,
 9        FORMAT_RGB_PLANAR,
10        DATA_TYPE_EXT_1N_BYTE,
11        &m_resized_imgs[i], strides);
12    assert(BM_SUCCESS == ret);
13}
14bm_image_alloc_contiguous_mem (max_batch, m_resized_imgs.data());
15
16// bm images for storing inference inputs
17bm_image_data_format_ext img_dtype = DATA_TYPE_EXT_FLOAT32;   //FP32
18
19
20if (tensor->get_dtype() == BM_INT8) {   // INT8
21    img_dtype = DATA_TYPE_EXT_1N_BYTE_SIGNED;
22}
23
24auto ret = bm_image_create_batch(m_bmContext->handle(), m_net_h, m_net_w,
25    FORMAT_RGB_PLANAR,
26    img_dtype,
27    m_converto_imgs.data(), max_batch);
28assert(BM_SUCCESS == ret);
29
30...

不同于bm_image_create()函数只创建一个bm_image对象,bm_image_create_batch()会根据最后一个参数batch,创建一组bm_image对象,而且这组对象所使用的data域是物理连续的。使用物理连续的内存是硬件加速器的特殊需求,在析构函数,可以使用bm_image_destroy_batch()对内存进行释放。

接下来是输入的处理过程,这个示例算法同时支持图片和视频作为输入,在main.cpp的main()函数中,我们以视频为例,详细的写法如下:

6.2.2.2. 打开视频流

 1...
 2
 3// open stream
 4cv::VideoCapture cap(input_url, cv::CAP_ANY, dev_id);
 5if (!cap.isOpened()) {
 6  std::cout << "open stream " << input_url << " failed!" << std::endl;
 7  exit(1);
 8}
 9
10// get resolution
11int w = int(cap.get(cv::CAP_PROP_FRAME_WIDTH));
12int h = int(cap.get(cv::CAP_PROP_FRAME_HEIGHT));
13std::cout << "resolution of input stream: " << h << "," << w << std::endl;
14
15...

上面这段代码和标准的opencv处理视频流程几乎相同。

6.2.2.3. 解码视频帧

 1...
 2
 3// get one mat
 4cv::Mat img;
 5if (!cap.read(img)) { //check
 6    std::cout << "Read frame failed or end of file!" << std::endl;
 7    exit(1);
 8}
 9
10std::vector<cv::Mat> images;
11images.push_back(img);
12
13...

6.2.2.4. Mat 转换 bm_image

由于BMCV预处理接口和网络推理接口都需要使用bm_image对象作为输入,因此解码后的视频帧需要转换到bm_image对象。推理完成之后,再使用bm_image_destroy()接口进行释放。需要注意的是,这个转换过程没有发生内存拷贝。

 1...
 2
 3// mat -> bm_image
 4CV_Assert(0 == cv::bmcv::toBMI((cv::Mat&)images[i], &image1, true));
 5
 6...
 7
 8//destroy
 9bm_image_destroy(image1);
10
11...

6.2.2.5. 预处理

bmcv_image_vpp_convert_padding()函数使用VPP硬件资源,是预处理过程加速的关键,需要配置参数 padding_attr。 bmcv_image_convert_to()函数用于进行线性变换,需要配置参数 converto_attr。

 1...
 2
 3// set padding_attr
 4bmcv_padding_atrr_t padding_attr;
 5memset(&padding_attr, 0, sizeof(padding_attr));
 6padding_attr.dst_crop_sty = 0;
 7padding_attr.dst_crop_stx = 0;
 8padding_attr.padding_b = 114;
 9padding_attr.padding_g = 114;
10padding_attr.padding_r = 114;
11padding_attr.if_memset = 1;
12if (isAlignWidth) {
13  padding_attr.dst_crop_h = images[i].rows*ratio;
14  padding_attr.dst_crop_w = m_net_w;
15
16  int ty1 = (int)((m_net_h - padding_attr.dst_crop_h) / 2);
17  padding_attr.dst_crop_sty = ty1;
18  padding_attr.dst_crop_stx = 0;
19}else{
20  padding_attr.dst_crop_h = m_net_h;
21  padding_attr.dst_crop_w = images[i].cols*ratio;
22
23  int tx1 = (int)((m_net_w - padding_attr.dst_crop_w) / 2);
24  padding_attr.dst_crop_sty = 0;
25  padding_attr.dst_crop_stx = tx1;
26}
27
28// do not crop
29bmcv_rect_t crop_rect{0, 0, image1.width, image1.height};
30
31auto ret = bmcv_image_vpp_convert_padding(m_bmContext->handle(), 1, image_aligned, &m_resized_imgs[i],
32    &padding_attr, &crop_rect);
33
34...
35
36// set converto_attr
37float input_scale = input_tensor->get_scale();
38input_scale = input_scale* (float)1.0/255;
39bmcv_convert_to_attr converto_attr;
40converto_attr.alpha_0 = input_scale;
41converto_attr.beta_0 = 0;
42converto_attr.alpha_1 = input_scale;
43converto_attr.beta_1 = 0;
44converto_attr.alpha_2 = input_scale;
45converto_attr.beta_2 = 0;
46
47// do converto
48ret = bmcv_image_convert_to(m_bmContext->handle(), image_n, converto_attr, m_resized_imgs.data(), m_converto_imgs.data());
49
50// attach to tensor
51if(image_n != max_batch) image_n = m_bmNetwork->get_nearest_batch(image_n);
52bm_device_mem_t input_dev_mem;
53bm_image_get_contiguous_device_mem(image_n, m_converto_imgs.data(), &input_dev_mem);
54input_tensor->set_device_mem(&input_dev_mem);
55input_tensor->set_shape_by_dim(0, image_n);  // set real batch number
56
57...

6.2.3. 推理

预处理过程的output是推理过程的input,当推理过程的input数据准备好后,就可以进行推理。

1...
2
3ret = m_bmNetwork->forward();
4...

6.2.4. 后处理

后处理的过程因模型而异,而且大部分是cpu执行的代码,就不再这里赘述。需要注意的是我们在BMCV中也提供了一些可以用于加速的接口,如bmcv_sort、bmcv_nms等,对于其他需要使用硬件加速的情况,可根据需要使用TPUKernel进行开发。

以上就是 YOLOV5 示例的简单描述,关于涉及到的接口更加详细的描述,请查看相应模块文档。

6.2.5. 算法开发注意事项汇总

根据上面的讨论,我们把一些注意事项汇总如下:

  • 视频解码需要注意:

注解

我们支持使用YUV格式作为缓存原始帧的格式,解码后可通过cap.set()接口设置YUV格式。本示例中没有涉及,具体可以参考本章 解码模块 章节。

  • 预处理过程需要注意:

注解

  1. 预处理的操作对象是bm_image,bm_image对象可以类比Mat对象。

  2. 预处理流程中scale缩放是针对int8模型。在推理数据输入前需要乘scale系数。scale系数是在量化的过程中产生。

  3. 为多个bm_image对象申请连续物理内存:bm_image_create_batch()。

  4. resize默认双线性插值算法,具体参考bmcv接口说明。

  • 推理过程需要注意:

注解

  1. 推理过程在device memory上进行,所以推理前输入数据必须已经存储在input tensors的device mem里面,推理结束后的结果数据也是保存在output tensors的device mem里面。

  2. 推荐使用4batch优化性能