算法C编程详解

这个章节将会选取BMNNSDK2中的SSD检测算法作为示例(examples/SSD_object/cpp_cv_bmcv_bmrt), 说明各个步骤的接口调用和注意事项。尽管SDK支持多种接口风格,但一个简洁的示例代码不可能面面俱到,因此这个算法程序采用了OPENCV解码 + BMCV图片预处理的组合进行开发,这个组合兼顾了高效和简洁。

我们按照算法的执行先后顺序展开介绍。首先是初始化流程,主要是在SSD类的构造函数完成的,主要任务有两个:

  1. 加载bmodel模型

  2. 初始化预处理相关的内存

  3. 推理

  4. 注意事项

1. 加载bmodel

  c语言接口示例
  ...
  const char *model_name = "VGG_VOC0712_SSD_300x300_deploy";
  ...
  // init bmruntime contxt
  p_bmrt_ = bmrt_create(bm_handle_);
  if (NULL == p_bmrt_) {
    cout << "ERROR: get handle failed!" << endl;
    exit(1);
  }

  // load bmodel from file
  ret = bmrt_load_bmodel(p_bmrt_, bmodel.c_str());
  if (!ret) {
    cout << "ERROR: Load bmodel[" << bmodel << "] failed" << endl;
    exit(1);
  }
  // get model info by model name
  net_info_ = bmrt_get_network_info(p_bmrt_, model_name);
  if (NULL == net_info_) {
    cout << "ERROR: get net-info failed!" << endl;
    exit(1);
  }
  ...

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

2. 预处理

  • 预处理初始化

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

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

除了提前申请物理连续内存,还可以在初始化过程中配置好预处理操作的参数,方法如下:

其中input_scale是INT8模型量化之后得到的参数,需要乘到每个像素,而FP32情况下就不再需要。 以上就是初始化的流程。接下来是输入的处理过程,这个示例算法同时支持图片和视频作为输入,在main.cpp的main()函数中,我们以视频为例,详细的写法:

  • 打开视频流

    除了最下面的cap.set()调用,上面这段代码和标准的opencv处理视频流程几乎相同。cap.set()调用会将解码帧的格式配置成YUV I420,一般我们会推荐使用YUV 格式作为缓存原始帧的格式,对比BGR格式,YUV格式既可以加速预处理流程,又可以减少内存消耗,是一个非常重要的优化。

  • 解码视频帧

当解码一帧图像,我们需要检查他的尺寸是否正确,对于YUV格式的Mat对象,我们通过avRows()和avCols()接口获取宽高。

  • Mat 转换 bm_image

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

  • 预处理

bmcvimage_vpp_convert()函数将resize / crop / yuv to bgr / split(transpose) 三个操作组合在了一个调用完成,是预处理过程加速的关键。 bmcv_convert_to()函数用于进行线性变换,使用的参数linear_trans_param在初始化阶段已经配置完成。

3. 推理

推理过程的input是预处理过程的output:lineartrans_bmcv,这个bm image对象是在初始化的时候就创建的物理连续内存。 推理需要指定input shape和model name,如果目标模型不支持指定的input shape,bm_inference()调用将会失败。

后处理的过程是cpu执行的代码,就不再这里赘述,以上就是SSD样例的简单描述,所涉及到的接口,会在后边的章节以及模块文档中有进行详细的描述。

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

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

视频解码需要注意:

  1. 通过cap.set()接口设置YUV格式接口设置YUV格式。

  2. YUV格式的Mat,其宽高从cols和rows变成avCols()和avRows()。

预处理过程需要注意:

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

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

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

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

推理注意点:

  1. 网络名称用于选择目标模型,需要在编译阶段就进行指定

  2. 调用推理接口的时候,input_shape要和bm_image匹配(n, c, h, w)

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

Last updated

Was this helpful?