基于OpenVINO C++ 接口部署飞桨表计识别模型
原创 杨亦诚 文章转自: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 input
auto input_port = compiled_model.input();
// Create tensor from external memory
ov::Tensor input_tensor(input_port.get_element_type(), input_port.get_shape(), memory_ptr);
// Set input tensor for model with one input
infer_request.set_input_tensor(input_tensor);
5.开始推理;
infer_request.start_async();
infer_request.wait();
6.获取结果数据
// Get output tensor by tensor name
auto 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和读数的形式标注在原始输入图片上,并保存在本地。
Erode 腐蚀分割结果,分离一些“粘连”的的临近刻度;
CircleToRectangle 将分割模型的输出的表计原型mask转化为长方形;
RectangleToLine 将方形的表计mask中关于指针和刻度的像素点数据以一维vector进行表示;
MeanBinarization 二值化操作,刻度中心点置1,非中心点置0;
LocateScale 及 LocatePointer 定位每个刻度和指针的具**置;
GetRelativeLocation 找到刻度和指针的相对位置;
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