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/examples/meter_reader/datasets/meter_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_libraries(meter_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
bool Segmenter::init(string model_path)
{
_model_path = model_path;
ov::Core core;
shared_ptr model = core.read_model(_model_path);
map 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 &inframes, vector &masks)
{
static map 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();
// nhwc -> nchw
for (int batch = 0; batch < batch_size; batch++)
{
resize(inframes[batch], inframes[batch], 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(inframes[batch].at(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();
// 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::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(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 &seg_results,
vector> *seg_label_maps);
bool CircleToRectangle(
const vector &seg_label_map,
vector *rectangle_meter);
bool RectangleToLine(const vector &rectangle_meter,
vector *line_scale,
vector *line_pointer);
bool MeanBinarization(const vector &data,
vector *binaried_data);
bool LocateScale(const vector &scale,
vector *scale_location);
bool LocatePointer(const vector &pointer,
float *pointer_location);
bool GetRelativeLocation(
const vector &scale_location,
const float &pointer_location,
MeterResult *result);
bool CalculateReading(const MeterResult &result,
float *reading);
bool PrintMeterReading(const vector &readings);
bool Visualize(Mat &img,
vector &detected_objects,
const vector &readings);
bool GetMeterReading(
const vector> &seg_label_maps,
vector *readings);
6 测试结果
在终端上运行 meter_reader 可执行文件,其中第一个参数代表检测模型的路径,第二个参数代表分割模型的路径,第三个参数代表测试图片的路径。
执行结束后会在本地保存本次推理的结果图片,具体示例如下: