基于OpenVINO 2022.1 C++ API部署YOLOv7预训练模型

任务背景

作为视觉应用中最常见的任务之一,目标检测一直是各类新模型刷榜的必争之地,其中就以YOLO系列的网络结构最为突出。YOLO的全称是you only look once,指只通过one-stage的方式需要“浏览一次”就可以识别出图中的物体的类别和位置。近期YOLO官方团队又放出新版本——YOLOv7,速度、精度都超越其他变体。本文将分享如何基于OpenVINO™ 2022.1工具套件部署YOLOv7官方提供的预训练模型。附C++/Python源码及使用方法。

代码仓库:https://github.com/OpenVINO-dev-contest/YOLOv7_OpenVINO_cpp-python

OpenVINO简介

用于高性能深度学习的英特尔发行版OpenVINO™工具套件基于oneAPI而开发,以期在从边缘到云的各种英特尔平台上,帮助用户更快地将更准确的真实世界结果部署到生产系统中。通过简化的开发工作流程,OpenVINO™可赋能开发者在现实世界中部署高性能应用程序和算法。

在推理后端,得益于 OpenVINO™ 工具套件提供的“一次编写,随处部署”特性,转换后的模型能够在不同的英特尔硬件平台上运行,无需重新构建,有效简化了构建与迁移过程。此外,为了支持更多的异构加速单元,OpenVINO™的runtime api底层采用了插件式的开发架构,基于oneAPI中的MKL-DNN、oneDNN等函数计算加速库,针对AVX-512等通用指令集进行优化,为不同的硬件执行单元分别实现了一套完整的高性能算子库,提升模型在推理运行时的整体性能表现。

YOLOv7简介

官方版的YOLOv7相同体量下比YOLOv5精度更高,速度快120%(FPS),比 YOLOX 快180%(FPS),比 Dual-Swin-T 快1200%(FPS),比 ConvNext 快550%(FPS),比 SWIN-L快500%(FPS)。在5FPS到160FPS的范围内,无论是速度或是精度,YOLOv7都超过了目前已知的检测器,并且在GPU V100上进行测试, 精度为56.8% AP的模型可达到30 FPS(batch=1)以上的检测速率,与此同时,这是目前唯一一款在如此高精度下仍能超过30FPS的检测器。

任务开发流程

我们先整体来看下 YOLOv7的输入输出结构,首先对输入的图片 resize 为 640x640 大小,输入到 backbone 网络中,然后经 head 层网络输出三层不同 size 大小的 feature map,并输出预测结果,这里以 coco 为例子,输出为 80 个类别,然后每个输出(x ,y, w, h, o) 即坐标位置和是否存在物体的置信度,3 是指的 anchor 数量,因此每一层的输出为 (80+5) x 3 = 255再乘上 feature map 的大小就是最终的输出了。整个开发流程可以分为数据处理模块定义、前处理任务、推理任务、后处理任务四部分组成。

图:YOLOv7官方预训练模型的输入输出结构

1.数据处理模块

定义Object结构体用来存放模型的输出数据,包含bounding box信息,类别标签,以及是否存在物体和类别的累计置信度。

定义class_names向量,用于存放coco数据集的所有标签。

 
  1.  
    struct Object
  2.  
    {
  3.  
        cv::Rect_<float> rect;
  4.  
        int label;
  5.  
        float prob;
  6.  
    };
  7.  
     
  8.  
    const std::vector<std::string> class_names = {
  9.  
        "person""bicycle""car""motorcycle""airplane""bus""train""truck""boat""traffic light",
  10.  
        "fire hydrant""stop sign""parking meter""bench""bird""cat""dog""horse""sheep""cow",
  11.  
        "elephant""bear""zebra""giraffe""backpack""umbrella""handbag""tie""suitcase""frisbee",
  12.  
        "skis""snowboard""sports ball""kite""baseball bat""baseball glove""skateboard""surfboard",
  13.  
        "tennis racket""bottle""wine glass""cup""fork""knife""spoon""bowl""banana""apple",
  14.  
        "sandwich""orange""broccoli""carrot""hot dog""pizza""donut""cake""chair""couch",
  15.  
        "potted plant""bed""dining table""toilet""tv""laptop""mouse""remote""keyboard""cell phone",
  16.  
        "microwave""oven""toaster""sink""refrigerator""book""clock""vase""scissors""teddy bear",
  17.  
        "hair drier""toothbrush"};
 

定义letterbox与scale_box模块,分别用于在图像前处理任务中为输入数据添加letterbox,以及在后处理任务还原letterbox带来坐标位置变换。这里特别值得注意的是我们增加了一个padd向量,用于存放在添加letterbox过程中letterbox的size信息以及相较原始图片的缩放比例信息,该组数据会用于在后处理任务中还原删除letterbox以后的结果。

 
  1.  
    cv::Mat letterbox(cv::Mat &src, int h, int w, std::vector<float> &padd)
  2.  
    {
  3.  
        // Resize and pad image while meeting stride-multiple constraints
  4.  
        int in_w = src.cols;
  5.  
        int in_h = src.rows;
  6.  
        int tar_w = w;
  7.  
        int tar_h = h;
  8.  
        float r = min(float(tar_h) / in_h, float(tar_w) / in_w);
  9.  
        int inside_w = round(in_w * r);
  10.  
        int inside_h = round(in_h * r);
  11.  
        int padd_w = tar_w - inside_w;
  12.  
        int padd_h = tar_h - inside_h;
  13.  
        cv::Mat resize_img;
  14.  
     
  15.  
        // resize
  16.  
        resize(src, resize_img, cv::Size(inside_w, inside_h));
  17.  
     
  18.  
        // divide padding into 2 sides
  19.  
        padd_w = padd_w / 2;
  20.  
        padd_h = padd_h / 2;
  21.  
        padd.push_back(padd_w);
  22.  
        padd.push_back(padd_h);
  23.  
     
  24.  
        // store the ratio
  25.  
        padd.push_back(r);
  26.  
        int top = int(round(padd_h - 0.1));
  27.  
        int bottom = int(round(padd_h + 0.1));
  28.  
        int left = int(round(padd_w - 0.1));
  29.  
        int right = int(round(padd_w + 0.1));
  30.  
     
  31.  
        // add border
  32.  
        copyMakeBorder(resize_img, resize_img, top, bottom, left, right, 0, cv::Scalar(114114114));
  33.  
        return resize_img;
  34.  
    }
  35.  
     
  36.  
    cv::Rect scale_box(cv::Rect box, std::vector<float> &padd)
  37.  
    {   
  38.  
        // remove the padding area
  39.  
        cv::Rect scaled_box;
  40.  
        scaled_box.x = box.x - padd[0];
  41.  
        scaled_box.y = box.y - padd[1];
  42.  
        scaled_box.width = box.width;
  43.  
        scaled_box.height = box.height;
  44.  
        return scaled_box;
  45.  
    }
 

定义generate_proposals模块,该模块具体有以下几个功能:

  1. 根据预定义的anchors,在输入图像中生成各类feature map的先验框;

  2. 根据输出结果调整先验框位置和大小,并将其作为bounding box还原到输入图像的坐标系中;

  3. 过滤置信度较低的分类结果,获取类别结果;

 
  1.  
    static void generate_proposals(int stride, const float *feat, float prob_threshold, std::vector<Object> &objects)
  2.  
    {   
  3.  
        // get the results from proposals
  4.  
        float anchors[18] = {1216193640283675765572146142110192243459401};
  5.  
        int anchor_num = 3;
  6.  
        int feat_w = 640 / stride;
  7.  
        int feat_h = 640 / stride;
  8.  
        int cls_num = 80;
  9.  
        int anchor_group = 0;
  10.  
        if (stride == 8)
  11.  
            anchor_group = 0;
  12.  
        if (stride == 16)
  13.  
            anchor_group = 1;
  14.  
        if (stride == 32)
  15.  
            anchor_group = 2;
  16.  
     
  17.  
        // 3 x h x w x (80 + 5)
  18.  
        for (int anchor = 0; anchor <= anchor_num - 1; anchor++)
  19.  
        {
  20.  
            for (int i = 0; i <= feat_h - 1; i++)
  21.  
            {
  22.  
                for (int j = 0; j <= feat_w - 1; j++)
  23.  
                {
  24.  
                    float box_prob = feat[anchor * feat_h * feat_w * (cls_num + 5) + i * feat_w * (cls_num + 5) + j * (cls_num + 5) + 4];
  25.  
                    box_prob = sigmoid(box_prob);
  26.  
     
  27.  
                    // filter the bounding box with low confidence
  28.  
                    if (box_prob < prob_threshold)
  29.  
                        continue;
  30.  
                    float x = feat[anchor * feat_h * feat_w * (cls_num + 5) + i * feat_w * (cls_num + 5) + j * (cls_num + 5) + 0];
  31.  
                    float y = feat[anchor * feat_h * feat_w * (cls_num + 5) + i * feat_w * (cls_num + 5) + j * (cls_num + 5) + 1];
  32.  
                    float w = feat[anchor * feat_h * feat_w * (cls_num + 5) + i * feat_w * (cls_num + 5) + j * (cls_num + 5) + 2];
  33.  
                    float h = feat[anchor * feat_h * feat_w * (cls_num + 5) + i * feat_w * (cls_num + 5) + j * (cls_num + 5) + 3];
  34.  
     
  35.  
                    double max_prob = 0;
  36.  
                    int idx = 0;
  37.  
     
  38.  
                    // get the class id with maximum confidence
  39.  
                    for (int t = 5; t < 85; ++t)
  40.  
                    {
  41.  
                        double tp = feat[anchor * feat_h * feat_w * (cls_num + 5) + i * feat_w * (cls_num + 5) + j * (cls_num + 5) + t];
  42.  
                        tp = sigmoid(tp);
  43.  
                        if (tp > max_prob)
  44.  
                        {
  45.  
                            max_prob = tp;
  46.  
                            idx = t;
  47.  
                        }
  48.  
                    }
  49.  
     
  50.  
                    // filter the class with low confidence
  51.  
                    float cof = box_prob * max_prob;
  52.  
                    if (cof < prob_threshold)
  53.  
                        continue;
  54.  
                    
  55.  
                    // convert results to xywh
  56.  
                    x = (sigmoid(x) * 2 - 0.5 + j) * stride;
  57.  
                    y = (sigmoid(y) * 2 - 0.5 + i) * stride;
  58.  
                    w = pow(sigmoid(w) * 22) * anchors[anchor_group * 6 + anchor * 2];
  59.  
                    h = pow(sigmoid(h) * 22) * anchors[anchor_group * 6 + anchor * 2 + 1];
  60.  
     
  61.  
                    float r_x = x - w / 2;
  62.  
                    float r_y = y - h / 2;
  63.  
     
  64.  
                    // store the results
  65.  
                    Object obj;
  66.  
                    obj.rect.x = r_x;
  67.  
                    obj.rect.y = r_y;
  68.  
                    obj.rect.width = w;
  69.  
                    obj.rect.height = h;
  70.  
                    obj.label = idx - 5;
  71.  
                    obj.prob = cof;
  72.  
                    objects.push_back(obj);
  73.  
                }
  74.  
            }
  75.  
        }
  76.  
    }
 

2.前处理任务

前处理主要包含以下几个步骤

  1. 使用OpenCV读取图片文件;

  2. 对于原始图片进行resize并添加letterbox;

  3. 将色彩通道从BGR转化为RGB;

  4. 将输入数据进行layout转置(NHWC=>NCHW),与归一化操作 (见模型推理部分代码);

 
  1.  
    cv::Mat src_img = cv::imread(image_path);
  2.  
        cv::Mat img;
  3.  
     
  4.  
        std::vector<float> padd;
  5.  
        cv::Mat boxed = letterbox(src_img, img_h, img_w, padd);
  6.  
        cv::cvtColor(boxed, img, cv::COLOR_BGR2RGB);
 

3.推理任务

图:OpenVINOTM工具套件runtime开发流程

模型推理部分主要调用OpenVINO™ 的C++ API进行实现,OpenVINO™ 推理接口的调用流程如上图所示(可参考官方说明:https://docs.openvino.ai/latest/openvino_docs_OV_UG_Integrate_OV_with_your_application.html),在这里第2步 Complie Model亦可分为模型读取与模型编译两个步骤执行,同时可以发现相较2021.4之前的版本,OpenVINO™ 2022.1在输入以及输出数据的接口调用方式上有了极大的简化,可以通过Tensor相关的构造函数,轻松实现模型数据的载入和读取。在整个过程中开发者需要将输入数据进行layout转置(NHWC=>NCHW),并填充到input tensor所对应的数据指针地址中。在结果数据提取部分,由于该模型有3个不同尺度feature map output,因此我们需要逐一获取他们结果数据指针。

 
  1.  
    // -------- Step 1. Initialize OpenVINO Runtime Core --------
  2.  
        ov::Core core;
  3.  
     
  4.  
        // -------- Step 2. Read a model --------
  5.  
        std::shared_ptr<ov::Model> model = core.read_model(model_path);
  6.  
     
  7.  
        // -------- Step 3. Loading a model to the device --------
  8.  
        ov::CompiledModel compiled_model = core.compile_model(model, device_name);
  9.  
     
  10.  
        // Get input port for model with one input
  11.  
        auto input_port = compiled_model.input();
  12.  
        // Create tensor from external memory
  13.  
        // ov::Tensor input_tensor(input_port.get_element_type(), input_port.get_shape(), input_data.data());
  14.  
        // -------- Step 4. Create an infer request --------
  15.  
        ov::InferRequest infer_request = compiled_model.create_infer_request();
  16.  
     
  17.  
        // -------- Step 5. Prepare input --------
  18.  
        ov::Tensor input_tensor1 = infer_request.get_input_tensor(0);
  19.  
        // NHWC => NCHW
  20.  
        auto data1 = input_tensor1.data<float>();
  21.  
        for (int h = 0; h < img_h; h++)
  22.  
        {
  23.  
            for (int w = 0; w < img_w; w++)
  24.  
            {
  25.  
                for (int c = 0; c < 3; c++)
  26.  
                {
  27.  
                    // int in_index = h * img_w * 3 + w * 3 + c;
  28.  
                    int out_index = c * img_h * img_w + h * img_w + w;
  29.  
                    data1[out_index] = float(img.at<cv::Vec3b>(h, w)[c]) / 255.0f;
  30.  
                }
  31.  
            }
  32.  
        }
  33.  
     
  34.  
        // -------- Step 6. Start inference --------
  35.  
        infer_request.infer();
  36.  
     
  37.  
        // -------- Step 7. Process output --------
  38.  
        auto output_tensor_p8 = infer_request.get_output_tensor(0);
  39.  
        const float *result_p8 = output_tensor_p8.data<const float>();
  40.  
        auto output_tensor_p16 = infer_request.get_output_tensor(1);
  41.  
        const float *result_p16 = output_tensor_p16.data<const float>();
  42.  
        auto output_tensor_p32 = infer_request.get_output_tensor(2);
  43.  
        const float *result_p32 = output_tensor_p32.data<const float>();
 

4.后处理任务

后处理部分需要调用我们之前定义的generate_proposals用于还原每一个feature map的结果数据,并进行堆叠,最后使用OpenCV DNN模块中自带的NMS方法,完成对结果bounding box的非极大抑制过滤,获取我们在原始input image中的目标位置与类别信息。

 
  1.  
    generate_proposals(8, result_p8, prob_threshold, objects8);
  2.  
        proposals.insert(proposals.end(), objects8.begin(), objects8.end());
  3.  
        generate_proposals(16, result_p16, prob_threshold, objects16);
  4.  
        proposals.insert(proposals.end(), objects16.begin(), objects16.end());
  5.  
        generate_proposals(32, result_p32, prob_threshold, objects32);
  6.  
        proposals.insert(proposals.end(), objects32.begin(), objects32.end());
  7.  
     
  8.  
        std::vector<int> classIds;
  9.  
        std::vector<float> confidences;
  10.  
        std::vector<cv::Rect> boxes;
  11.  
     
  12.  
        for (size_t i = 0; i < proposals.size(); i++)
  13.  
        {
  14.  
            classIds.push_back(proposals[i].label);
  15.  
            confidences.push_back(proposals[i].prob);
  16.  
            boxes.push_back(proposals[i].rect);
  17.  
        }
  18.  
     
  19.  
        std::vector<int> picked; 
  20.  
     
  21.  
        // do non maximum suppression for each bounding boxx
  22.  
        cv::dnn::NMSBoxes(boxes, confidences, prob_threshold, nms_threshold, picked);
 

此外,我们还需要进一步调整模型input data对应的结果数据,将其还原到原始尺寸的图片上进行展示。

 
  1.  
    int idx = picked[i];
  2.  
            cv::Rect box = boxes[idx];
  3.  
            cv::Rect scaled_box = scale_box(box, padd);
  4.  
            drawPred(classIds[idx], confidences[idx], scaled_box, padd[2], raw_h, raw_w, src_img, class_names);
 

参考示例使用方法

该示例分别提供了C++和Python的参考实现,具体使用方法如下:

 
  1.  
    # 下载示例仓库
  2.  
    $ git clone https://github.com/OpenVINO-dev-contest/YOLOv7_OpenVINO_cpp-python.git
 

1.依赖安装

C++ 环境依赖

由于本示例的C++版本只依赖OpenVINO™ 和OpenCV的运行库,所以需要开发者提前完成对这两个工具组件的安装:

OpenVINO™ C++ runtime:https://docs.openvino.ai/latest/openvino_docs_install_guides_installing_openvino_linux.html#install-openvino

OpenCV环境:https://docs.opencv.org/4.x/d7/d9f/tutorial_linux_install.html

注:由于该示例中提供的CMakeList使用OpenCV的默认路径,因此需要在完成OpenCV的编译后,执行make install命令。

Python环境依赖

Python环境的安装相对简单,只需通过pip命令行工具进行依赖安装

$ pip install -r python/requirements

2.预训练模型下载

可以从官方github仓库提供的链接中下载基于COCO数据集的YOLOv7预训练模型权重 https://github.com/WongKinYiu/yolov7

3.模型转换

目前 OpenVINO™ runtime可以直接支持onnx格式的模型部署,所以我们在得到.pt权重文件后,只需使用官方自带export.py脚本,就可将其导出为onnx格式模型,具体过程如下:

 
  1.  
    # 下载YOLOv7官方仓库:
  2.  
    $ git clone git@github.com:WongKinYiu/yolov7.git
  3.  
    $ cd yolov7/models
  4.  
    $ python export.py --weights yolov7.pt
 

4.测试运行

C++示例

编译C++示例源码,编译完成后会在build目录下生成yolov7可执行文件:

 
  1.  
    $ cd cpp
  2.  
      $ mkdir build && cd build
  3.  
      $ source '~/intel/openvino_2022.1.0.643/bin/setupvars.sh'
  4.  
      $ cmake ..
 

执行推理任务:

$ yolov7 yolov7.onnx data/horses.jpg 'CPU'

Python示例

执行推理任务:

$ python python/main.py -m yolov7.onnx -i data/horse.jpg

5.测试结果

运行推理示例后,会在本地目录下生成代bounding box以及label的图片,这里我们用到官方仓库中附带的马匹数据进行测试,具体结果如下:

图:推理运行结果

Benchmark App 介绍

OpenVINO™ 提供了性能测试工具 Benchmark App ,方便开发者快速测试 OpenVINO™ 模型在不同的硬件平台上的性能。我们以下面的例子,简单介绍 benchmark app 的使用方法和相关参数,更多内容请参考 Benchmark App 官方文档。

$ benchmark_app -m yolov7.onnx  -hint throughput
  • m: 指定模型路径。由于目前OpenVINO™  runtime是支持直接读取onnx格式文件的,所以这里我们设置为导出以后onnx模型。

  • hint: 指定性能测试的优先策略,以自动选择底层性能优化相关参数。这里我们选择 throughput 模式来提升系统整体吞吐量。如果应用对延迟比较敏感,推荐使用 latency 模式来减少推理延迟。

结论

YOLOv7由于其出色的精度和性能表现,在推出第一时就受到了极大的关注,目前github上的star已超过5K。本示例通过OpenVINO™ 2022.1新版本的C++ API接口,实现对YOLOv7官方预训练模型的部署。最后使用OpenVINO™ 自带的benchmark_app工具进一步对模型的性能进行验证测试。

参考文献

  • YOLOv7官方仓库:https://github.com/WongKinYiu/yolov7

  • OpenVINO™ 开发文档:https://docs.openvino.ai/latest/openvino_docs_OV_UG_Integrate_OV_with_your_application.html

posted @ 2024-02-21 09:23  阿风小子  阅读(223)  评论(0编辑  收藏  举报