6.2. C/C++编程详解
这个章节将会选取 sophon-demo 中的 YOLOV5 检测算法作为示例,说明各个步骤的接口调用和注意事项。
备注
样例代码路径:sophon-demo/sample/YOLOV5
这个示例程序采用了 OPENCV解码+BMCV图片预处理 的组合进行开发,您也可以根据您的需要采用不同的接口开发。
我们按照算法的执行先后顺序展开介绍:
加载bmodel模型
预处理
推理
注意事项
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(©ToAttr, 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格式。
预处理过程需要注意:
备注
预处理的操作对象是bm_image,bm_image对象可以类比Mat对象。
预处理流程中scale缩放是针对int8模型。在推理数据输入前需要乘scale系数。scale系数是在量化的过程中产生。
为多个bm_image对象申请连续物理内存:bm_image_create_batch()。
resize默认双线性插值算法,具体参考bmcv接口说明。
推理过程需要注意:
备注
推理过程在device memory上进行,所以推理前输入数据必须已经存储在input tensors的device mem里面,推理结束后的结果数据也是保存在output tensors的device mem里面。
推荐使用4batch优化性能。