使用 OpenVINO™ 实现 Paddle的PGNet 推理程序

OpenVINO™ 简介

OpenVINO™ 工具包 2022.1 版于 2022 年 3 月 22 日正式发布,根据官宣《OpenVINO™ 迎来迄今为止最重大更新,2022.1 新特性抢先看》,OpenVINO™ 2022.1 将是迄今为止最大变化的版本,并可以直接支持读取飞桨模型。

PGNet 简介

任意形状的文本检测与识别是很有挑战性的,目前大部分解决方案(如图Fig.2)都有如下劣势:
(1)两阶段网络通常都博爱阔NMS和RoI这两种非常耗时的操作,比如RoISlide 和 BezierAlign;
(2)训练时需要字符级标注,这是非常费时费力,比如 Mask TextSpotter;
(3)使用预定义规则对非常规文本方向进行识别,导致识别失败,比如 TextDragon 和 Mask TextSpotter 假设文本方向为从左到右或者从上到下,这些都阻碍了正确识别有挑战性的文本。

论文提出实时解析(检测与识别)文本的新框架,即PGNet, 它应用了一种成为点聚集的操作(point gathering)。PGNet是一种基于多任务学习的单阶段文本解读器, 架构如图所示。

在模型推理阶段:论文中使用FCN(Fully convolutional network)模型来学习文本区域包含的各种信息,包括文本中心线(text center line, TCL, 1 通道特征图), 文本边界偏移(text border offset, TBO, 4通道特征图-TCL每一像素距离文本区域上、下边界点的偏移量),文本方向偏移(text direction offset, TDO, 2通道特征图-TCL每一像素到下一个文本阅读位置的偏移量)以及文本字符分类特征图(text character classification, TCC, n通道特征图-n为字符类别数)。基于像素的字符分类特征图由PG-CTC(Point Gathering CTC)损失函数训练得到,避免了字符标注操作。

在后处理阶段:在每一个文本实例中可以根据TCL和TBO特征图提取出带有文本方向的中心点序列;可以从TBO特征图获取文本检测的结果;应用PG-CTC解码器,论文将高级别的二维TCC特征图转换为字符分类概率向量序列,用这个概率向量序列可以解码出最终的文本识别结果。

受到 SRN 和 GTC 的启发,论文提出使用图强化模块(GRM)来进一步提升端到端识别性能。文本序列中的点可视为图(graph)中的一个节点(node),每一个节点的表现能力可通过相邻的语义上下文(semantic context)和视觉上下文信息(visual context)来提升,字符分类结果理应更加准确。

任意形状文本的识别(例如,弯曲文本)受到越来越多的研究关注,而应用也愈发广泛(例如,广告牌识别、印章识别等),如下图所示。

AAAI 2021 会议上发表的百度自研的端到端场景文本识别
PGNet[1] 算法,有效的满足了上述需求,其典型特点有:

• PGNet 是一种新颖的、端到端的、的任意形状的文本检测识
别器框架

• 不需要字符级别的标注,NMS 操作以及 ROI 操作[2]

• 提出预测文本行内的阅读顺序模块和基于图的修正模块来提
升文本识别效果

• 识别精度和运行速度堪称 SOTA

另外,飞桨版PGNet的工程化和文档化做的极好,没有任何基础的人,都可以在不到半天的时间内完成 PGNet 的开发环境搭建、模型训练、ONNX 模型导出。

关于文档,可以参考PGNet 的文档链接:https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.5/doc/doc_ch/algorithm_e2e_pgnet.md

下面开始进入正题。

准备 PGNet 的 OpenVINO 推理程序开发环境

要完成 PGNet 的 OpenVINO 推理程序开发,需要安装:
• PaddleOCR 运行环境,参考:

  1. 安装 PaddleOCR 运行环境
  2. 克隆 PaddleOCR 到本地,并安装第三方库
    • OpenVINO™ 开发工具
pip install openvino-dev[onnx]

下载 PGNet 预训练模型

PaddleOCR 已提供 PGNet 预训练模型,请自行下载并解压,如下图所示。

PGNet 预训练模型下载链接:https://paddleocr.bj.bcebos.com/dygraph_v2.0/pgnet/e2e_server_pgnetA_infer.tar

!wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/pgnet/e2e_server_pgnetA_infer.tar
--2022-12-15 11:41:05--  https://paddleocr.bj.bcebos.com/dygraph_v2.0/pgnet/e2e_server_pgnetA_infer.tar
正在解析主机 paddleocr.bj.bcebos.com (paddleocr.bj.bcebos.com)... 182.61.200.195, 182.61.200.229, 2409:8c04:1001:1002:0:ff:b001:368a
正在连接 paddleocr.bj.bcebos.com (paddleocr.bj.bcebos.com)|182.61.200.195|:443... 已连接。
已发出 HTTP 请求,正在等待回应... 200 OK
长度: 195768320 (187M) [application/x-tar]
正在保存至: “e2e_server_pgnetA_infer.tar.3”

e2e_server_pgnetA_i 100%[===================>] 186.70M  59.5MB/s    in 3.3s    

2022-12-15 11:41:08 (56.9 MB/s) - 已保存 “e2e_server_pgnetA_infer.tar.3” [195768320/195768320])
!tar xvf e2e_server_pgnetA_infer.tar
!ls
e2e_server_pgnetA_infer/
e2e_server_pgnetA_infer/inference.pdiparams
e2e_server_pgnetA_infer/inference.pdiparams.info
e2e_server_pgnetA_infer/inference.pdmodel
data			     ic15_dict.txt  PaddleOCR-release-2.6.zip
e2e_server_pgnetA_infer      main.ipynb     work
e2e_server_pgnetA_infer.tar  PaddleOCR
!git clone https://gitee.com/paddlepaddle/PaddleOCR.git
fatal: 目标路径 'PaddleOCR' 已经存在,并且不是一个空目录。

下载完毕后,运行 PaddleOCR 自带的预测程序 tools/infer/predict_e2e.py

%cd PaddleOCR
/home/aistudio/PaddleOCR
!ls
applications  inference_results  ppocr	       requirements.txt  train.sh
benchmark     __init__.py	 PPOCRLabel    setup.py
configs       LICENSE		 ppstructure   StyleText
deploy	      MANIFEST.in	 README_ch.md  test_tipc
doc	      paddleocr.py	 README.md     tools
!pip install -r requirements.txt 
!pip install scikit-image imgaug
# 如果想使用 CPU 进行预测,需设置 use_gpu 参数为False
!python3 tools/infer/predict_e2e.py --e2e_algorithm="PGNet" --image_dir="./doc/imgs_en/img623.jpg" --e2e_model_dir="../e2e_server_pgnetA_infer" --e2e_pgnet_valid_set="totaltext" --use_gpu=False
python3: can't open file 'tools/infer/predict_e2e.py': [Errno 2] No such file or directory

运行结果,如下图所示,说明 PaddleOCR 和 PGNet 预训练模型都准备好了。

从图中可见,运行的平均时间是 5.09s。

%matplotlib inline
def imshow_dst():
    import matplotlib.pyplot as plt
    import matplotlib.image as mpimg

    img_path = './inference_results/e2e_res_img623.jpg'

    img1 = mpimg.imread(img_path)
    plt.figure(figsize=(10,10))
    plt.imshow(img1)
    plt.axis('off')
    plt.show()
imshow_dst()

用 OpenVINO™ 读取 PGNet 预训练模型

如前所述,OpenVINO™ 支持直接读入飞桨模型。使用下面的
代码,可以快速测试 OpenVINO™ 读入 PGNet 模型的效果。

# 安装openvino-dev
!pip install openvino==2022.3.0.dev20221125 -i https://pypi.tuna.tsinghua.edu.cn/simple
Looking in indexes: https://pypi.tuna.tsinghua.edu.cn/simple
Collecting openvino==2022.3.0.dev20221125
  Downloading https://pypi.tuna.tsinghua.edu.cn/packages/54/fd/06a9503058d22f2c743800e85857e92d6dd0a30ec3e927b3d09623069e90/openvino-2022.3.0.dev20221125-8831-cp37-cp37m-manylinux_2_17_x86_64.whl (37.5 MB)
[2K     [90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[0m [32m37.5/37.5 MB[0m [31m6.1 MB/s[0m eta [36m0:00:00[0m00:01[0m00:01[0m
[?25hRequirement already satisfied: numpy<=1.23.1,>=1.16.6 in /opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages (from openvino==2022.3.0.dev20221125) (1.19.5)
Installing collected packages: openvino
Successfully installed openvino-2022.3.0.dev20221125

[1m[[0m[34;49mnotice[0m[1;39;49m][0m[39;49m A new release of pip available: [0m[31;49m22.1.2[0m[39;49m -> [0m[32;49m22.3.1[0m
[1m[[0m[34;49mnotice[0m[1;39;49m][0m[39;49m To update, run: [0m[32;49mpip install --upgrade pip[0m

32;49mpip install --upgrade pip[0m

!pip install openvino-dev
from openvino.runtime import Core
# 指定 PGNet 模型路径
pgnet_path = "../e2e_server_pgnetA_infer/inference.pdmodel"
# 创建 Core 对象
core = Core()
# 载入并编译 PGNet 模型
sess = core.compile_model(model=pgnet_path, 
device_name="CPU")
# 输出 PGNet 模型输入&输出信息
print(sess.input)

运行效果如下图所示,说明 OpenVINO™ 直接读取飞桨版PGNet 模型成功。


使用 OpenVINO™ 开发 PGNet 的推理程序

基于上述代码,结合PGNet的数据前处理和后处理代码,即可开发出完整的PGNet推理程序。

升级 PaddleOCR 自带预测程序,支持OpenVINO 推理

由于 PaddleOCR 自带的预测程序 tools/infer/predict_e2e.py,已经实现了 PGNet 的数据前处理和后处理代码,所以,只需要
稍微修改:

• tools/infer/predict_e2e.py

• tools/infer/utility.py

添加 OpenVINO 的推理代码,即可升级 PaddleOCR 对
OpenVINO 的支持。

上述代码,可以从 https://gitee.com/ppov-nuc/pgnetopenvino-inference 下载。下载后,请并替换同名文件。

# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import sys

__dir__ = os.path.dirname(os.path.abspath(__file__))
sys.path.append(__dir__)
sys.path.insert(0, os.path.abspath(os.path.join(__dir__, '../..')))

os.environ["FLAGS_allocator_strategy"] = 'auto_growth'

import cv2
import numpy as np
import time
import sys

import tools.infer.utility as utility
from ppocr.utils.logging import get_logger
from ppocr.utils.utility import get_image_file_list, check_and_read_gif
from ppocr.data import create_operators, transform
from ppocr.postprocess import build_post_process

logger = get_logger()

class TextE2E(object):
    def __init__(self, args):
        self.args = args
        self.e2e_algorithm = args.e2e_algorithm
        self.use_onnx = args.use_onnx
        # OpenVINO Support here
        self.use_openvino = args.use_openvino

        pre_process_list = [{
            'E2EResizeForTest': {}
        }, {
            'NormalizeImage': {
                'std': [0.229, 0.224, 0.225],
                'mean': [0.485, 0.456, 0.406],
                'scale': '1./255.',
                'order': 'hwc'
            }
        }, {
            'ToCHWImage': None
        }, {
            'KeepKeys': {
                'keep_keys': ['image', 'shape']
            }
        }]
        postprocess_params = {}
        if self.e2e_algorithm == "PGNet":
            pre_process_list[0] = {
                'E2EResizeForTest': {
                    'max_side_len': args.e2e_limit_side_len,
                    'valid_set': 'totaltext'
                }
            }
            postprocess_params['name'] = 'PGPostProcess'
            postprocess_params["score_thresh"] = args.e2e_pgnet_score_thresh
            postprocess_params["character_dict_path"] = args.e2e_char_dict_path
            postprocess_params["valid_set"] = args.e2e_pgnet_valid_set
            postprocess_params["mode"] = args.e2e_pgnet_mode
        else:
            logger.info("unknown e2e_algorithm:{}".format(self.e2e_algorithm))
            sys.exit(0)
        self.preprocess_op = create_operators(pre_process_list)
        self.postprocess_op = build_post_process(postprocess_params)
        self.predictor, self.input_tensor, self.output_tensors, _ = utility.create_predictor(
            args, 'e2e', logger)  # paddle.jit.load(args.det_model_dir)
        # self.predictor.eval()

    def clip_det_res(self, points, img_height, img_width):
        for pno in range(points.shape[0]):
            points[pno, 0] = int(min(max(points[pno, 0], 0), img_width - 1))
            points[pno, 1] = int(min(max(points[pno, 1], 0), img_height - 1))
        return points

    def filter_tag_det_res_only_clip(self, dt_boxes, image_shape):
        img_height, img_width = image_shape[0:2]
        dt_boxes_new = []
        for box in dt_boxes:
            box = self.clip_det_res(box, img_height, img_width)
            dt_boxes_new.append(box)
        dt_boxes = np.array(dt_boxes_new)
        return dt_boxes

    def __call__(self, img):
        ori_im = img.copy()
        data = {'image': img}
        data = transform(data, self.preprocess_op)
        img, shape_list = data
        if img is None:
            return None, 0
        img = np.expand_dims(img, axis=0)
        shape_list = np.expand_dims(shape_list, axis=0)
        img = img.copy()
        starttime = time.time()

        if self.use_onnx:
            input_dict = {}
            input_dict[self.input_tensor.name] = img
            outputs = self.predictor.run(self.output_tensors, input_dict)
            preds = {}
            preds['f_border'] = outputs[0]
            preds['f_char'] = outputs[1]
            preds['f_direction'] = outputs[2]
            preds['f_score'] = outputs[3]
        # OpenVINO Support Here
        elif self.use_openvino:
            outputs = self.predictor([img])
            out_layers = self.predictor.output
            preds = {}
            preds['f_border'] = outputs[out_layers(0)]
            preds['f_char'] = outputs[out_layers(1)]
            preds['f_direction'] = outputs[out_layers(2)]
            preds['f_score'] = outputs[out_layers(3)]
        else:
            self.input_tensor.copy_from_cpu(img)
            self.predictor.run()
            outputs = []
            for output_tensor in self.output_tensors:
                output = output_tensor.copy_to_cpu()
                outputs.append(output)

            preds = {}
            if self.e2e_algorithm == 'PGNet':
                preds['f_border'] = outputs[0]
                preds['f_char'] = outputs[1]
                preds['f_direction'] = outputs[2]
                preds['f_score'] = outputs[3]
            else:
                raise NotImplementedError
        post_result = self.postprocess_op(preds, shape_list)
        points, strs = post_result['points'], post_result['texts']
        dt_boxes = self.filter_tag_det_res_only_clip(points, ori_im.shape)
        elapse = time.time() - starttime
        return dt_boxes, strs, elapse


if __name__ == "__main__":
    args = utility.parse_args()
    image_file_list = get_image_file_list(args.image_dir)
    text_detector = TextE2E(args)
    count = 0
    total_time = 0
    draw_img_save = "./inference_results"
    if not os.path.exists(draw_img_save):
        os.makedirs(draw_img_save)
    for image_file in image_file_list:
        img, flag = check_and_read_gif(image_file)
        if not flag:
            img = cv2.imread(image_file)
        if img is None:
            logger.info("error in loading image:{}".format(image_file))
            continue
        points, strs, elapse = text_detector(img)
        if count > 0:
            total_time += elapse
        count += 1
        logger.info("Predict time of {}: {}".format(image_file, elapse))
        src_im = utility.draw_e2e_res(points, strs, image_file)
        img_name_pure = os.path.split(image_file)[-1]
        img_path = os.path.join(draw_img_save,
                                "e2e_res_{}".format(img_name_pure))
        cv2.imwrite(img_path, src_im)
        logger.info("The visualized image saved in {}".format(img_path))
    if count > 1:
        logger.info("Avg Time: {}".format(total_time / (count - 1)))

借鉴 PaddleOCR 的前处理和后处理代码,实现OpenVINO 推理程序

直接使用 ppocr 中自带的前处理和后处理代码,可以轻松实现PGNet的OpenVINO 推理程序:

def clip_det_res(points, img_height, img_width):
    for pno in range(points.shape[0]):
        points[pno, 0] = int(min(max(points[pno, 0], 0), img_width - 1))
        points[pno, 1] = int(min(max(points[pno, 1], 0), img_height - 1))
    return points

# 定义 filter_tag_det_res_only_clip 函数
def filter_tag_det_res_only_clip(dt_boxes, image_shape):
    img_height, img_width = image_shape[0:2]
    dt_boxes_new = []
    for box in dt_boxes:
        box = clip_det_res(box, img_height, img_width)
        dt_boxes_new.append(box)
        dt_boxes = np.array(dt_boxes_new)
    return dt_boxes
from openvino.runtime import Core
from ppocr.data import create_operators, transform
from ppocr.postprocess import build_post_process
import cv2
import numpy as np
import time

# 指定 PGNet 模型路径
pgnet_path = "../e2e_server_pgnetA_infer/inference.pdmodel"

# 创建 Core 对象
core = Core()

# 载入并编译 PGNet 模型
pgnet = core.compile_model(model=pgnet_path, device_name="CPU")

# 创建 preprocess_op
pre_process_list = [{
'E2EResizeForTest': {
'max_side_len': 768,
'valid_set': 'totaltext'}
}, {
'NormalizeImage': {
'std': [0.229, 0.224, 0.225],
'mean': [0.485, 0.456, 0.406],
'scale': '1./255.',
'order': 'hwc'
}
}, {
'ToCHWImage': None
}, {
'KeepKeys': {
'keep_keys': ['image', 'shape']
}
}]
preprocess_op = create_operators(pre_process_list)

# 创建 postprocess_op
postprocess_params = {}
postprocess_params['name'] = 'PGPostProcess'
postprocess_params["score_thresh"] = 0.5
postprocess_params["character_dict_path"] = "../ic15_dict.txt"
postprocess_params["valid_set"] = 'totaltext'
postprocess_params["mode"] = 'fast'
postprocess_op = build_post_process(postprocess_params)


# 载入图像数据并实现预处理
image_path = './doc/imgs_en/img623.jpg'
image = cv2.imread(image_path)
data = {'image': image}
data = transform(data, preprocess_op)
img, shape_list = data
img = np.expand_dims(img, axis=0)
shape_list = np.expand_dims(shape_list, axis=0)
starttime = time.time()
# Do the inference by OpenVINO
outputs = pgnet([img])
out_layers = pgnet.output
preds = {}
preds['f_border'] = outputs[out_layers(0)]
preds['f_char'] = outputs[out_layers(1)]
preds['f_direction'] = outputs[out_layers(2)]
preds['f_score'] = outputs[out_layers(3)]
post_result = postprocess_op(preds, shape_list)
points, strs = post_result['points'], post_result['texts']
dt_boxes = filter_tag_det_res_only_clip(points, image.shape)
elapse = time.time() - starttime
#print(f"points:{points}s")
print(f"strs:{strs}s")
print(f"Predict time: {elapse}s")
imshow_dst()

运行结果,如下图所示:

总结

飞桨版的 PGNet 是一个易学易用、文档化和工程化做的非常好的[3]、端到端的、可以检测弯曲文字的模型。由于从 OpenVINO™ 2022.1 版本开始,OpenVINO™ Runtime 已经支持了直接读取飞桨模型,且 OpenVINO™ 的API 简单易用,所以,可以很容易的升级 PaddleOCR 自带的预测程序 tools/infer/predict_e2e.py 支持 OpenVINO™ 推理计算,或者从零开发 OpenVINO™ 推理程序。
经过 OpenVINO™ 优化后,PGNet 的 CPU 推理计算效率大大提升,平均运行时间从 5.09s,提升到 1.33s,效率提升非常明显。

注意:运行时间,仅供参考,在不同 CPU 上,运行的时间会不一样。

个人介绍

  • 林旭 某小厂算法架构师
  • AICA六期班学员
  • 飞桨PFCC成员

参考资料

[1] PGNet: Real-time Arbitrarily-Shaped Text Spotting
with Point Gathering Network, https://www.aaai.org/AAAI21Papers/AAAI-2885.WangP.pdf

[2] PaddleOCR FAQ: https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.5/doc/doc_ch/FAQ.md#13

[3] PGNet repo: https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.5/doc/doc_ch/algorithm_e2e_pgnet.md

此文章为搬运
原项目链接

Logo

学大模型,用大模型上飞桨星河社区!每天8点V100G算力免费领!免费领取ERNIE 4.0 100w Token >>>

更多推荐