基于OpenVINO C++ 接口部署飞桨表计识别模型

openlab_4276841a 更新于 2年前

原创 杨亦诚 文章转自:OpenVINO中文社区


1 项目说明

在该项目中,主要向大家介绍如何基于基于  OpenVINO C++  接口来实现对指针型表计读数。

在电力能源厂区需要定期监测表计读数,以保证设备正常运行及厂区安全。但厂区分布分散,人工巡检耗时长,无法实时监测表计,且部分工作环境危险导致人工巡检无法触达。针对上述问题,希望通过摄像头拍照->智能读数的方式高效地完成此任务。




为实现智能读数,我们采取目标检测->语义分割->读数后处理的方案:

· 第一步,使用目标检测模型定位出图像中的表计;

· 第二步,使用语义分割模型将各表计的指针和刻度分割出来;

· 第三步,根据指针的相对位置和预知的量程计算出各表计的读数。

整个方案的流程如下所示:




2 环境准备 (Ubuntu)

由于本次任务将用到 OpenCV 和 OpenVINO 的相关组件,所以需要在进行代码开发之前,完成相关 runtime 依赖的安装。这边以 Ubuntu 系统作为示例,具体方法可以参考:

1.OpenVINO: 

https://docs.openvino.ai/latest/openvino_docs_install_guides_installing_openvino_linux.html#install-openvino

2.OpenCV: 

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

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


3 数据准备

3.1 测试数据下载

本案例开放了表计检测数据集,使用该数据集可以测试本次 OpenVINO 部署的模型精度和识别性能。

· 表计测试图片:

https://bj.bcebos.com/paddlex/example***eter_reader/dataset***eter_test.tar.gz

解压后的表计测试图片的文件夹内容如下:

一共有58张测试图片。

meter_test/|-- 20190822_105.jpg|-- 20190822_142.jpg|-- ... ...

由于本次任务主要是完成推理阶段的部署,所以我们只需要从这58张测试图片中随机选取测试用例即可。

3.2 预训练模型下载

该实例代码将演示如何在通过 OpenVINO 完成 Paddle 模型在 Intel 平台上部署。我们可以使用训练好的 PPYOLO 和 DeepLabV3P 模型对测试用的圆形表计图片进行识别,实现表面缺陷的识别。预训练模型下载地址:

表计检测预训练模型:

https://bj.bcebos.com/paddlex/examples2/meter_reader/meter_det_model.tar.gz

刻度和指针分割预训练模型:

https://bj.bcebos.com/paddlex/examples2/meter_reader/meter_seg_model.tar.gz

3.3 模型转换

目前 OpenVINO 2022.1的 runtime 可以直接支持对 Paddle 静态模型的读取和加载,但为了追求更好的性能,这里我们还是展示了如果通过 OpenVINO 的 Model Optimizer 工具对下载后的 Paddle 模型进行转换。

$ mo --input_model meter_det_model/model.pdmodel$ mo --input_model meter_seg_model/model.pdmodel

转换成功以后会在当前目录下分别生成以下三个模型文件:

meter_det_model/|-- model.xml|-- model.bin|-- model.mapping

其中.xml文件用来描述模型的拓扑结构,.bin存储模型的权重信息,.mapping则是用来记录转换前后的2个模型的算子映射关系。实际推理过程中只需要用到.xml及.bin两个文件即可。


4 代码编译

4.1 代码下载

下载仓库中该任务的源码包:

meter_reader_openvino_cpp-main.zip 或者也可以通过

$ git clone https://github.com/OpenVINO-dev-contest/meter_reader_openvino_cpp.git

下载到本地电脑,本进行解压。

4.2 修改CMakeLists.txt

将 CMakeLists.txt 其中的 OpenVINO 相关环境的路径换成你本地路径。

cmake_minimum_required(VERSION 3.10)set(CMAKE_CXX_STANDARD 11)find_package(OpenCV REQUIRED)#find_package(OpenVINO REQUIRED)
set(openvino_LIBRARIES "/home/ethan/intel/openvino_2022.1.0.643/runtime/lib/intel64/libopenvino.so")
include_directories( ./ /home/ethan/intel/openvino_2022.1.0.643/runtime/include /home/ethan/intel/openvino_2022.1.0.643/runtime/include/ie /home/ethan/intel/openvino_2022.1.0.643/runtime/include/ngraph /home/ethan/intel/openvino_2022.1.0.643/runtime/include/openvino ${OpenCV_INCLUDE_DIR})link_directories("/home/ethan/intel/openvino_2022.1.0.643/runtime/lib")aux_source_directory(src SRC)add_executable(meter_reader main.cpp ${SRC})target_link_librarie***eter_reader PRIVATE ${openvino_LIBRARIES} ${OpenCV_LIBS})

4.3 编译

运行下列指令,完成后将在build目录下生成meter_reader可执行文件。

$ cd ~/meter_reader_openvino_cpp $ mkdir build && cd build $ cmake .. $ make


5 代码模块说明

本示例的推理部分模块大致可以分成三个部分:

检测任务模块

分割任务模块

后处理模块

关于 OpenVINO C++ 接口的部署流程大家可以参考这个文档:Integrate OpenVINO™ with Your Application:

https://docs.openvino.ai/latest/openvino_docs_OV_UG_Integrate_OV_with_your_application.html

相对应的API模块可以参考以下流程:

1.初始化 OpenVINO Runtime Core;

include <openvino/openvino.hpp>ov::Core core;

2.读取模型并进行编译;

ov::CompiledModel compied_model = core.compile_model("model.xml", "AUTO");

3.创建推理请求;

ov::InferRequest infer_request = compiled_model.create_infer_request();

4.为模型配置输入数据;

// Get input port for model with one inputauto input_port = compiled_model.input();// Create tensor from external memoryov::Tensor input_tensor(input_port.get_element_type(), input_port.get_shape(), memory_ptr);// Set input tensor for model with one inputinfer_request.set_input_tensor(input_tensor);

5.开始推理;

infer_request.start_async();infer_request.wait();

6.获取结果数据

// Get output tensor by tensor nameauto output = infer_request.get_tensor("tensor_name");const float \*output_buffer = output.data<const float>();
接下来就让我们一起来逐一来看表计读数这个实例中每个模块的基本逻辑:

5.1 检测任务模块

这部分主要由 Detector 类的初始化与执行推理函数两部分组成。由于 PPYOLO 有三组输入数据,因此需要分别获取三组数据的所对应的 input_tensor 指针地址,并将输入数据处理后,按模型的layout排布要求按顺序存入该指针地址,同时在处理每一个通道值的时候,还要通过减均值除方差操作来对输出数据进行归一化。相对应的我们也需要在处理结果数据时获取相应的 output_tensor 指针,并按顺序提取其中的物体置信度与位置信息。

此外为了加快推理性能,我们需要将检测模型的batch size维度加以固定。这里是通过model->reshape(name_to_shape)方法来实现的。

bool Detector::init(string model_path, double threshold){    _model_path = model_path;    _threshold = threshold;    ov::Core core;    shared_ptr<ov::Model> model = core.read_model(_model_path);    map<string, ov::PartialShape> name_to_shape;    name_to_shape["image"] = ov::PartialShape{1, 3, 608, 608};    name_to_shape["im_shape"] = ov::PartialShape{1, 2};    name_to_shape["scale_factor"] = ov::PartialShape{1, 2};    model->reshape(name_to_shape);    ov::CompiledModel detect_model = core.compile_model(model, "CPU");    detect_infer_request = detect_model.create_infer_request();    return true;}
bool Detector::process_frame(Mat &src_img, vector<Rect> &detected_objects){ int total_num = 22743; float mean[3] = {0.485, 0.456, 0.406}; float std[3] = {0.229, 0.224, 0.225}; float height = src_img.rows; float width = src_img.cols; float scale_x = width / 608 * 2; float scale_y = height / 608;
Mat img; resize(src_img, img, Size(608, 608)); ov::Tensor input_tensor0 = detect_infer_request.get_tensor("im_shape"); ov::Tensor input_tensor1 = detect_infer_request.get_tensor("image"); ov::Tensor input_tensor2 = detect_infer_request.get_tensor("scale_factor"); // nhwc -> nchw auto data1 = input_tensor1.data<float>(); for (int h = 0; h < 608; h++) { for (int w = 0; w < 608; w++) { for (int c = 0; c < 3; c++) { int out_index = c * 608 * 608 + h * 608 + w; data1[out_index] = float(((float(img.at<Vec3b>(h, w)[c]) / 255.0f) - mean[c]) / std[c]); } } }
auto data0 = input_tensor0.data<float>(); data0[0] = 608; data0[1] = 608;
auto data2 = input_tensor2.data<float>(); data2[0] = 1; data2[1] = 2;
//start inference detect_infer_request.infer();
//extract the output data auto output = detect_infer_request.get_output_tensor(0); const float *result = output.data<const float>(); for (int num = 0; num < total_num; num++) { auto box_prob = result[num * 6 + 1]; if (box_prob > _threshold && box_prob <= 1) { float x0 = result[num * 6 + 2] * scale_x; float y0 = result[num * 6 + 3] * scale_y; float x1 = result[num * 6 + 4] * scale_x; float y1 = result[num * 6 + 5] * scale_y; Rect rect = Rect(round(x0), round(y0), round(x1 - x0), round(y1 - y0)); detected_objects.push_back(rect); } }
return true;}

5.2 分割任务模块

分割任务和检测任务的模板类似,也需要分别实现 Segmenter 类初始化和推理两个功能函数。这里值得特别注意的是在提取结果数据时,由于原始的输出排布为NCHW,其中C为3中类别各自的对应每一个像素点的置信度,但鉴于C++只有没有类似argmax这样的函数帮助我们对指定维度进行排序,因此为了方便对齐数据维度,需要手动先将layout转置为NHWC再进行最大值排序。

大家可以发现相较检测模型的推理任务,在处理分割模型时,我们并没有对它的batch size维度进行固定,原因是本次示例中的用到的测试图片中可能存在1个或者多个表头,为了提升模型的通用性,OpenVINO目前也是支持将分割模型以Dynamic Input Shape的形式进行部署。

bool Segmenter::init(string model_path){    _model_path = model_path;    ov::Core core;    shared_ptr<ov::Model> model = core.read_model(_model_path);    map<string, ov::PartialShape> name_to_shape;    model->reshape({{-1, 3, 512, 512}});    ov::CompiledModel segment_model = core.compile_model(model, "CPU");    segment_infer_request = segment_model.create_infer_request();    return true;}
bool Segmenter::process_frame(vector<Mat> &inframes, vector<Mat> &masks){ static map<int32_t, Vec3b> color_table = { {0, Vec3b(0, 0, 0)}, {1, Vec3b(20, 59, 255)}, {2, Vec3b(120, 59, 200)}, }; float mean[3] = {0.5, 0.5, 0.5}; float std[3] = {0.5, 0.5, 0.5};
int batch_size = inframes.size(); ov::Tensor input_tensor0 = segment_infer_request.get_input_tensor(0); input_tensor0.set_shape({batch_size, 3, 512, 512}); auto data0 = input_tensor0.data<float>(); // nhwc -> nchw for (int batch = 0; batch < batch_size; batch++) { resize(inframe***atch], inframe***atch], Size(512, 512)); for (int h = 0; h < 512; h++) { for (int w = 0; w < 512; w++) { for (int c = 0; c < 3; c++) { int out_index = batch * 3 * 512 * 512 + c * 512 * 512 + h * 512 + w; data0[out_index] = float(((float(inframe***atch].at<Vec3b>(h, w)[c]) / 255.0f) - mean[c]) / std[c]); } } } }
//start inference segment_infer_request.infer();
//extract the output data auto output = segment_infer_request.get_output_tensor(0); const float *result = output.data<const float>(); // nchw -> nhwc for (int batch = 0; batch < batch_size; batch++) { Mat mask = Mat::zeros(512, 512, CV_8UC1); for (int h = 0; h < 512; h++) { for (int w = 0; w < 512; w++) { int argmax_id; float max_conf = numeric_limits<float>::min(); for (int c = 0; c < 3; c++) { int out_index = batch * 3 * 512 * 512 + c * 512 * 512 + h * 512 + w; float out_value = result[out_index]; if (out_value > max_conf) { argmax_id = c; max_conf = out_value; } } mask.at<uchar>(h, w) = argmax_id; } } masks.push_back(mask); }
return true;}

5.3 后处理模块

这里的后处理模块其实是复用了PaddleX中提供的参考示例,整体逻辑大家可以参考开篇的那张图片,关于具体的功能模块我们可以直接看其中的头文件。这里我们额外定义了一个Visualize函数,用来将检测模型与表计读数的结果以bounding box和读数的形式标注在原始输入图片上,并保存在本地。

  1. Erode 腐蚀分割结果,分离一些“粘连”的的临近刻度;

  2. CircleToRectangle 将分割模型的输出的表计原型mask转化为长方形;

  3. RectangleToLine 将方形的表计mask中关于指针和刻度的像素点数据以一维vector进行表示;

  4. MeanBinarization 二值化操作,刻度中心点置1,非中心点置0;

  5. LocateScale 及 LocatePointer 定位每个刻度和指针的具**置;

  6. GetRelativeLocation 找到刻度和指针的相对位置;

  7. GetMeterReading 根据表计的量程以及单位刻度的数值,计算实际指针所指向的刻度值。

bool Erode(const int32_t &kernel_size,           const vector<Mat> &seg_results,           vector<vector<uint8_t>> *seg_label_maps);
bool CircleToRectangle( const vector<uint8_t> &seg_label_map, vector<uint8_t> *rectangle_meter);
bool RectangleToLine(const vector<uint8_t> &rectangle_meter, vector<int> *line_scale, vector<int> *line_pointer);
bool MeanBinarization(const vector<int> &data, vector<int> *binaried_data);
bool LocateScale(const vector<int> &scale, vector<float> *scale_location);
bool LocatePointer(const vector<int> &pointer, float *pointer_location);
bool GetRelativeLocation( const vector<float> &scale_location, const float &pointer_location, MeterResult *result);
bool CalculateReading(const MeterResult &result, float *reading);
bool PrintMeterReading(const vector<float> &readings);
bool Visualize(Mat &img, vector<Rect> &detected_objects, const vector<float> &readings);
bool GetMeterReading( const vector<vector<uint8_t>> &seg_label_maps,    vector<float> *readings);


6 测试结果

在终端上运行 meter_reader 可执行文件,其中第一个参数代表检测模型的路径,第二个参数代表分割模型的路径,第三个参数代表测试图片的路径。

执行结束后会在本地保存本次推理的结果图片,具体示例如下:




更多参考示例可以访问:

https://github.com/OpenVINO-dev-contest/meter_reader_openvino_cpp

参考文献:

https://github.com/PaddlePaddle/PaddleX/tree/develop/example***eter_reader

0个评论