6.2. C/C++编程详解

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

备注

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

这个示例程序采用了 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...
 2m_resized_imgs.resize(max_batch);
 3m_converto_imgs.resize(max_batch);
 4// some API only accept bm_image whose stride is aligned to 64
 5int aligned_net_w = FFALIGN(m_net_w, 64);
 6int strides[3] = {aligned_net_w, aligned_net_w, aligned_net_w};
 7for(int i=0; i<max_batch; i++){
 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());
15bm_image_data_format_ext img_dtype = DATA_TYPE_EXT_FLOAT32;
16if (tensor->get_dtype() == BM_INT8){
17    img_dtype = DATA_TYPE_EXT_1N_BYTE_SIGNED;
18}
19auto ret = bm_image_create_batch(m_bmContext->handle(), m_net_h, m_net_w,
20    FORMAT_RGB_PLANAR,
21    img_dtype,
22    m_converto_imgs.data(), max_batch);
23assert(BM_SUCCESS == ret);
24...

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

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

6.2.2.2. 打开视频流

在sophon-demo的YOLOv5示例中,我们基于FFMPEG封装了一个VideoDecFFM类打开视频流或图片,方式如下:

1...
2
3// open stream
4VideoDecFFM decoder;
5decoder.openDec(&h, input.c_str());
6
7...

详细信息可参考:YOLOv5/cpp/dependencies/include/ff_decode.hpp

您也可以通过OpenCV打开视频流

 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
 3bm_image *img = decoder.grab();
 4if (!img){
 5end_flag=true;
 6}else {
 7batch_imgs.push_back(*img);
 8delete img;
 9img = nullptr;
10}
11
12...

OpenCV

 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//resize image
 3int ret = 0;
 4for(int i = 0; i < image_n; ++i) {
 5    bm_image image1 = images[i];
 6    bm_image image_aligned;
 7    bool need_copy = image1.width & (64-1);
 8    if(need_copy){
 9        int stride1[3], stride2[3];
10        bm_image_get_stride(image1, stride1);
11        stride2[0] = FFALIGN(stride1[0], 64);
12        stride2[1] = FFALIGN(stride1[1], 64);
13        stride2[2] = FFALIGN(stride1[2], 64);
14        bm_image_create(m_bmContext->handle(), image1.height, image1.width,
15            image1.image_format, image1.data_type, &image_aligned, stride2);
16
17        bm_image_alloc_dev_mem(image_aligned, BMCV_IMAGE_FOR_IN);
18        bmcv_copy_to_atrr_t copyToAttr;
19        memset(&copyToAttr, 0, sizeof(copyToAttr));
20        copyToAttr.start_x = 0;
21        copyToAttr.start_y = 0;
22        copyToAttr.if_padding = 1;
23        bmcv_image_copy_to(m_bmContext->handle(), copyToAttr, image1, image_aligned);
24    } else {
25        image_aligned = image1;
26}
27// set padding_attr
28bmcv_padding_atrr_t padding_attr;
29memset(&padding_attr, 0, sizeof(padding_attr));
30padding_attr.dst_crop_sty = 0;
31padding_attr.dst_crop_stx = 0;
32padding_attr.padding_b = 114;
33padding_attr.padding_g = 114;
34padding_attr.padding_r = 114;
35padding_attr.if_memset = 1;
36if (isAlignWidth) {
37  padding_attr.dst_crop_h = images[i].rows*ratio;
38  padding_attr.dst_crop_w = m_net_w;
39
40  int ty1 = (int)((m_net_h - padding_attr.dst_crop_h) / 2);
41  padding_attr.dst_crop_sty = ty1;
42  padding_attr.dst_crop_stx = 0;
43}else{
44  padding_attr.dst_crop_h = m_net_h;
45  padding_attr.dst_crop_w = images[i].cols*ratio;
46
47  int tx1 = (int)((m_net_w - padding_attr.dst_crop_w) / 2);
48  padding_attr.dst_crop_sty = 0;
49  padding_attr.dst_crop_stx = tx1;
50}
51
52// do not crop
53bmcv_rect_t crop_rect{0, 0, image1.width, image1.height};
54
55auto ret = bmcv_image_vpp_convert_padding(m_bmContext->handle(), 1, image_aligned, &m_resized_imgs[i],
56    &padding_attr, &crop_rect);
57
58...
59
60// set converto_attr
61float input_scale = input_tensor->get_scale();
62input_scale = input_scale* (float)1.0/255;
63bmcv_convert_to_attr converto_attr;
64converto_attr.alpha_0 = input_scale;
65converto_attr.beta_0 = 0;
66converto_attr.alpha_1 = input_scale;
67converto_attr.beta_1 = 0;
68converto_attr.alpha_2 = input_scale;
69converto_attr.beta_2 = 0;
70
71// do converto
72ret = bmcv_image_convert_to(m_bmContext->handle(), image_n, converto_attr, m_resized_imgs.data(), m_converto_imgs.data());
73
74// attach to tensor
75if(image_n != max_batch) image_n = m_bmNetwork->get_nearest_batch(image_n);
76bm_device_mem_t input_dev_mem;
77bm_image_get_contiguous_device_mem(image_n, m_converto_imgs.data(), &input_dev_mem);
78input_tensor->set_device_mem(&input_dev_mem);
79input_tensor->set_shape_by_dim(0, image_n);  // set real batch number
80
81...

6.2.3. 推理

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

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

6.2.4. 后处理

后处理的过程因模型而异,而且大部分是中央处理器执行的代码,就不再这里赘述。需要注意的是我们在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优化性能。