要点:

  • 奇奇怪怪

1 预测部署简介与总览

本章主要介绍PP-OCRv2系统的高性能推理方法、服务化部署方法以及端侧部署方法。通过本章的学习,您可以学习到:

  • 根据不同的场景挑选合适的预测部署方法
  • PP-OCRv2系列模型在不同场景下的推理方法
  • Paddle Inference、Paddle Serving、Paddle Lite的推理部署开发方法

1.1 简介

在前面几个章节中,我们通过模型训练的方法,得到了训练好的模型,在使用它去预测的时候,我们首先需要定义好模型,然后加载训练好的模型,再将预处理之后的数据送进网络中进行预测、后处理,得到最终的结果。使用这种方法去进行预测,调试方便,但是预测效率比较低下。

针对上面的问题,对于训练得到的模型,一般有下面2种离线预测的方式。

  1. 基于训练引擎推理,也就是和训练使用同一套引擎,它调试方便,便于我们迅速定位问题,验证正确性。多为Python语言。
  2. 基于预测引擎推理,将训练得到的模型进行转换,去掉与推理无关的部分,使用这种方法可以加速预测。多为Python语言或C++。

具体地,PaddleOCR 针对不同应用场景,提供了三种预测部署方案

  • Inference的离线预测,这种方式主要应用在对预测响应的及时性要求不高,特别是需要大量图片预测的场景。像文档电子化、广告信息提取等。虽然不能及时响应预测请求,但是没有网络延时,计算效率比较高,数据安全性很高。
  • Serving服务化部署,这种方式主要应用在对预测响应的及时性要求很高的场景,像商业化OCR的API接口、实时拍照翻译、拍题等场景。虽然这种方式能及时对预测需求及时响应,但是网络耗时开销比较大,GPU利用率往往不高,而且存在数据安全风险。
  • Lite端侧部署,这种方式主要希望模型部署到手机、机器人等端侧设备上,主要考虑部署方便和数据安全性,像手机APP里面身份证识别、银行卡识别,工业应用场景的仪表监控识别等。这种方法,对OCR模型的大小比较敏感。虽然没有网络延时,数据安全性很高,但是由于算力限制,预测效率不高。

本章基于PP-OCRv2,介绍文本检测、识别以及系统串联预测推理与部署过程。

1.2 环境准备

体验本章节内容需要首先下载PaddleOCR代码,安装相关依赖,具体命令如下

import os

os.chdir("/home/aistudio")
# 下载代码
!git clone https://gitee.com/paddlepaddle/PaddleOCR.git
os.chdir("/home/aistudio/PaddleOCR")
# 安装运行所需要的whl包
!pip install -U pip
!pip install -r requirements.txt
# VQA任务中需要用到该库
!pip install paddlenlp==2.2.1

# 导入一些库
import cv2
import matplotlib.pyplot as plt
%matplotlib inline
import numpy as np
import os

2 基于Paddle Inference的python推理

2.1 简介

在项目中,模型的推理性能直接影响项目成本,因此我们期望一个训练好的模型的模型可以拥有更快的推理速度。直接基于训练引擎进行预测,模型中包含与训练相关的算子,因此效率一般较低;而且需要定义模型,难以与训练代码解耦。Paddle Inference应运而生。它是飞桨的原生推理库,作用于服务器端和云端,提供高性能的推理能力。由于能力直接基于飞桨的训练算子,因此Paddle Inference 可以通用支持飞桨训练出的所有模型。

考虑到大家的使用场景差异很大,Paddle Inference针对不同平台不同的应用场景进行了深度的适配优化,做到高吞吐、低时延,保证了飞桨模型在服务器端即训即用,快速部署。

本章主要介绍基于Paddle Inference的PP-OCRv2预测推理过程,更多关于Paddle Inference的介绍可以参考:Paddle Inference 介绍

在基于Paddle Inference进行模型推理时,一般有以下几个步骤。

PP-OCRv2系统包含文字检测、方向分类器和文字识别3个模型,下面分别介绍这3个模型基于Paddle Inference的推理过程。

2.2 PP-OCRv2文字检测模型推理

PaddleOCR中,在基于文字检测模型进行推理时,需要通过参数image_dir指定单张图像或者图像集合的路径、参数det_model_dir, 指定检测的 inference 模型路径。

下面进行最新的超轻量文本检测模型推理实战,更多的模型和使用方法请参考文本检测预测教程

更多等算法超参数的介绍可以参考PaddleOCR Inference推理相关参数介绍

2.2.1 准备数据和环境

在最开始已经安装好了Paddle以及相应的依赖,这里环境已经准备好了。

检测示例数据在doc/imgs文件夹下面,部分数据如下所示。

# 切换目录
os.chdir("/home/aistudio/PaddleOCR")

# 查看数据
!ls doc/imgs/

# 选择2张图像可视化
img1 = cv2.imread("doc/imgs/00006737.jpg")
img2 = cv2.imread("doc/imgs/00056221.jpg")
plt.figure(figsize=(15, 6))
plt.subplot(1, 2, 1)
plt.imshow(img1[:,:,::-1])
plt.subplot(1, 2, 2)
plt.imshow(img2[:,:,::-1])
plt.show()

2.2.2 准备推理模型

下载推理模型并解压,放在inference目录下面。

# 下载模型
!mkdir inference
!cd inference && wget https://paddleocr.bj.bcebos.com/PP-OCRv2/chinese/ch_PP-OCRv2_det_infer.tar -O ch_PP-OCRv2_det_infer.tar && tar -xf ch_PP-OCRv2_det_infer.tar
!tree -h inference/ch_PP-OCRv2_det_infer
  • 如果您希望导出自己训练得到的模型,使用Paddle Inference部署,那么可以使用下面的命令将预训练模型使用动转静的方法,转化为推理模型。
# 参考代码
# https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.4/tools/export_model.py
# 下载预训练模型
!wget https://paddleocr.bj.bcebos.com/PP-OCRv2/chinese/ch_PP-OCRv2_det_distill_train.tar && tar -xf ch_PP-OCRv2_det_distill_train.tar && rm ch_PP-OCRv2_det_distill_train.tar
# 导出推理模型
!python tools/export_model.py -c configs/det/ch_PP-OCRv2/ch_PP-OCRv2_det_cml.yml \
    -o Global.pretrained_model="ch_PP-OCRv2_det_distill_train/best_accuracy" \
    Global.save_inference_dir="./my_model"
# PP-OCRv2检测模型包含3个子网络:教师、学生、学生2,因此导出时,包含3个子文件,实际推理时,使用其中1个学生网络进行推理即可
!tree -h my_model

2.2.3 文本检测功能初探

我们先来看看加载加载推理模型预测得到的结果。

# 参考代码
# https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.4/tools/infer/predict_det.py
# 预测
!python tools/infer/predict_det.py --image_dir="./doc/imgs/00018069.jpg" --det_model_dir="./inference/ch_PP-OCRv2_det_infer" --use_gpu=False

# 读取图像并显示出来,显示结果
plt.figure(figsize=(20, 8))
img_ori = cv2.imread("./doc/imgs/00018069.jpg")
img_out = cv2.imread("./inference_results/det_res_00018069.jpg")
plt.subplot(1, 2, 1)
plt.imshow(img_ori[:,:,::-1])
plt.subplot(1, 2, 2)
plt.imshow(img_out[:,:,::-1])
plt.show()

首先需要定义参数设置如下所示。更多参数的介绍可以参考:PaddleOCR推理过程参数介绍

# 参考代码
# https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.4/tools/infer/utility.py
import argparse
import os
import sys
import cv2
import numpy as np
import paddle
from PIL import Image, ImageDraw, ImageFont
import math
from paddle import inference
import time
from ppocr.utils.logging import get_logger


def str2bool(v):
    return v.lower() in ("true", "t", "1")


def init_args():
    parser = argparse.ArgumentParser()
    # params for prediction engine
    parser.add_argument("--use_gpu", type=str2bool, default=True)
    parser.add_argument("--ir_optim", type=str2bool, default=True)
    parser.add_argument("--use_tensorrt", type=str2bool, default=False)
    parser.add_argument("--min_subgraph_size", type=int, default=15)
    parser.add_argument("--precision", type=str, default="fp32")
    parser.add_argument("--gpu_mem", type=int, default=500)

    # params for text detector
    parser.add_argument("--image_dir", type=str)
    parser.add_argument("--det_algorithm", type=str, default='DB')
    parser.add_argument("--det_model_dir", type=str)
    parser.add_argument("--det_limit_side_len", type=float, default=960)
    parser.add_argument("--det_limit_type", type=str, default='max')

    # DB parmas
    parser.add_argument("--det_db_thresh", type=float, default=0.3)
    parser.add_argument("--det_db_box_thresh", type=float, default=0.6)
    parser.add_argument("--det_db_unclip_ratio", type=float, default=1.5)
    parser.add_argument("--max_batch_size", type=int, default=10)
    parser.add_argument("--use_dilation", type=str2bool, default=False)
    parser.add_argument("--det_db_score_mode", type=str, default="fast")
    # EAST parmas
    parser.add_argument("--det_east_score_thresh", type=float, default=0.8)
    parser.add_argument("--det_east_cover_thresh", type=float, default=0.1)
    parser.add_argument("--det_east_nms_thresh", type=float, default=0.2)

    # SAST parmas
    parser.add_argument("--det_sast_score_thresh", type=float, default=0.5)
    parser.add_argument("--det_sast_nms_thresh", type=float, default=0.2)
    parser.add_argument("--det_sast_polygon", type=str2bool, default=False)

    # PSE parmas
    parser.add_argument("--det_pse_thresh", type=float, default=0)
    parser.add_argument("--det_pse_box_thresh", type=float, default=0.85)
    parser.add_argument("--det_pse_min_area", type=float, default=16)
    parser.add_argument("--det_pse_box_type", type=str, default='box')
    parser.add_argument("--det_pse_scale", type=int, default=1)

    # params for text recognizer
    parser.add_argument("--rec_algorithm", type=str, default='CRNN')
    parser.add_argument("--rec_model_dir", type=str)
    parser.add_argument("--rec_image_shape", type=str, default="3, 32, 320")
    parser.add_argument("--rec_batch_num", type=int, default=6)
    parser.add_argument("--max_text_length", type=int, default=25)
    parser.add_argument(
        "--rec_char_dict_path",
        type=str,
        default="./ppocr/utils/ppocr_keys_v1.txt")
    parser.add_argument("--use_space_char", type=str2bool, default=True)
    parser.add_argument(
        "--vis_font_path", type=str, default="./doc/fonts/simfang.ttf")
    parser.add_argument("--drop_score", type=float, default=0.5)

    # params for e2e
    parser.add_argument("--e2e_algorithm", type=str, default='PGNet')
    parser.add_argument("--e2e_model_dir", type=str)
    parser.add_argument("--e2e_limit_side_len", type=float, default=768)
    parser.add_argument("--e2e_limit_type", type=str, default='max')

    # PGNet parmas
    parser.add_argument("--e2e_pgnet_score_thresh", type=float, default=0.5)
    parser.add_argument(
        "--e2e_char_dict_path", type=str, default="./ppocr/utils/ic15_dict.txt")
    parser.add_argument("--e2e_pgnet_valid_set", type=str, default='totaltext')
    parser.add_argument("--e2e_pgnet_mode", type=str, default='fast')

    # params for text classifier
    parser.add_argument("--use_angle_cls", type=str2bool, default=False)
    parser.add_argument("--cls_model_dir", type=str)
    parser.add_argument("--cls_image_shape", type=str, default="3, 48, 192")
    parser.add_argument("--label_list", type=list, default=['0', '180'])
    parser.add_argument("--cls_batch_num", type=int, default=6)
    parser.add_argument("--cls_thresh", type=float, default=0.9)

    parser.add_argument("--enable_mkldnn", type=str2bool, default=False)
    parser.add_argument("--cpu_threads", type=int, default=10)
    parser.add_argument("--use_pdserving", type=str2bool, default=False)
    parser.add_argument("--warmup", type=str2bool, default=False)

    #
    parser.add_argument(
        "--draw_img_save_dir", type=str, default="./inference_results")
    parser.add_argument("--save_crop_res", type=str2bool, default=False)
    parser.add_argument("--crop_res_save_dir", type=str, default="./output")

    # multi-process
    parser.add_argument("--use_mp", type=str2bool, default=False)
    parser.add_argument("--total_process_num", type=int, default=1)
    parser.add_argument("--process_id", type=int, default=0)

    parser.add_argument("--benchmark", type=str2bool, default=False)
    parser.add_argument("--save_log_path", type=str, default="./log_output/")

    parser.add_argument("--show_log", type=str2bool, default=True)
    parser.add_argument("--use_onnx", type=str2bool, default=False)
    # 这里需要注意,添加这个是因为直接在notebook中解析的话,sys.argv会在后面添加下面的内容,导致解析失败
    # '-f', '/home/aistudio/.local/share/jupyter/runtime/kernel-e1221262-c656-4129-896f-1b197b6b782c.json'
    parser.add_argument("-f", type=str, default=None)
    return parser


def parse_args():
    parser = init_args()
    return parser.parse_args()

下面和大家具体看下文字检测的具体代码。

# 参考代码
# https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.4/tools/infer/predict_det.py
import os
import sys
import cv2
import numpy as np
import time

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
import json
logger = get_logger()

# 文字检测类
class TextDetector(object):
    def __init__(self, args):
        self.args = args
        self.det_algorithm = args.det_algorithm
        pre_process_list = [{
            'DetResizeForTest': {
                'limit_side_len': args.det_limit_side_len,
                'limit_type': args.det_limit_type,
            }
        }, {
            '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.det_algorithm == "DB":
            postprocess_params['name'] = 'DBPostProcess'
            postprocess_params["thresh"] = args.det_db_thresh
            postprocess_params["box_thresh"] = args.det_db_box_thresh
            postprocess_params["max_candidates"] = 1000
            postprocess_params["unclip_ratio"] = args.det_db_unclip_ratio
            postprocess_params["use_dilation"] = args.use_dilation
            postprocess_params["score_mode"] = args.det_db_score_mode
        else:
            logger.info("unknown det_algorithm:{}".format(self.det_algorithm))
            sys.exit(0)
        # 初始化预测引擎
        self.predictor, self.input_tensor, self.output_tensors, self.config = utility.create_predictor(
            args, 'det', logger)
        # 构建预处理算子
        self.preprocess_op = create_operators(pre_process_list)
        # 构建后处理算子
        self.postprocess_op = build_post_process(postprocess_params)
        

    def order_points_clockwise(self, pts):
        """
        参考: https://github.com/jrosebr1/imutils/blob/master/imutils/perspective.py
        对检测出来的点进行按照顺时针排序
        """
        xSorted = pts[np.argsort(pts[:, 0]), :]

        leftMost = xSorted[:2, :]
        rightMost = xSorted[2:, :]

        leftMost = leftMost[np.argsort(leftMost[:, 1]), :]
        (tl, bl) = leftMost

        rightMost = rightMost[np.argsort(rightMost[:, 1]), :]
        (tr, br) = rightMost

        rect = np.array([tl, tr, br, bl], dtype="float32")
        return rect

    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(self, dt_boxes, image_shape):
        # 去除小于特定尺寸的检测结果
        img_height, img_width = image_shape[0:2]
        dt_boxes_new = []
        for box in dt_boxes:
            box = self.order_points_clockwise(box)
            box = self.clip_det_res(box, img_height, img_width)
            rect_width = int(np.linalg.norm(box[0] - box[1]))
            rect_height = int(np.linalg.norm(box[0] - box[3]))
            if rect_width <= 3 or rect_height <= 3:
                continue
            dt_boxes_new.append(box)
        dt_boxes = np.array(dt_boxes_new)
        return dt_boxes

    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}

        st = time.time()
        
        # 数据预处理
        data = transform(data, self.preprocess_op)
        img, shape_list = data
        if img is None:
            return None, 0
        # 扩展bs维度:CHW -> NCHW
        img = np.expand_dims(img, axis=0)
        shape_list = np.expand_dims(shape_list, axis=0)
        img = img.copy()
        # 将数据拷贝到预测引擎中
        self.input_tensor.copy_from_cpu(img)
        # 自动推理
        self.predictor.run()
        outputs = []
        # 将返回结果从预测引擎中拷贝回CPU
        for output_tensor in self.output_tensors:
            output = output_tensor.copy_to_cpu()
            outputs.append(output)

        preds = {}
        if self.det_algorithm in ['DB', 'PSE']:
            preds['maps'] = outputs[0]
        else:
            raise NotImplementedError

        # 后处理
        post_result = self.postprocess_op(preds, shape_list)
        dt_boxes = post_result[0]['points']
        dt_boxes = self.filter_tag_det_res(dt_boxes, ori_im.shape)
        
        et = time.time()
        return dt_boxes, et - st

# 设置参数
args = parse_args()
args.det_model_dir = "./inference/ch_PP-OCRv2_det_infer"
args.image_dir = "./doc/imgs/00018069.jpg"

# 获取图片列表
image_file_list = get_image_file_list(args.image_dir)
# 创建文本检测器对象
text_detector = TextDetector(args)

count = 0
total_time = 0
draw_img_save = "./inference_results"

if not os.path.exists(draw_img_save):
    os.makedirs(draw_img_save)
save_results = []
for image_file in image_file_list:
    img = cv2.imread(image_file)
    if img is None:
        logger.info("error in loading image:{}".format(image_file))
        continue
    st = time.time()
    dt_boxes, _ = text_detector(img)
    elapse = time.time() - st
    if count > 0:
        total_time += elapse
    count += 1
    save_pred = os.path.basename(image_file) + "\t" + str(
        json.dumps(np.array(dt_boxes).astype(np.int32).tolist())) + "\n"
    save_results.append(save_pred)
    logger.info(save_pred)
    logger.info("The predict time of {}: {}".format(image_file, elapse))
    src_im = utility.draw_text_det_res(dt_boxes, image_file)
    img_name_pure = os.path.split(image_file)[-1]
    img_path = os.path.join(draw_img_save,
                            "det_res_{}".format(img_name_pure))
    cv2.imwrite(img_path, src_im)
    logger.info("The visualized image saved in {}".format(img_path))
    
    break

with open(os.path.join(draw_img_save, "det_results.txt"), 'w') as f:
    f.writelines(save_results)
    f.close()

plt.figure(figsize=(10, 10))
plt.imshow(src_im[:, :, ::-1])
plt.show()

上面就完成了完整的文本检测流程。

2.3  2.3 PP-OCRv2方向分类器模型推理

同样地,对于方向分类器模型,我们也可以使用下面的命令快速体验其功能。

# 下载模型
!cd inference && wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_infer.tar -O ch_ppocr_mobile_v2.0_cls_infer.tar && tar -xf ch_ppocr_mobile_v2.0_cls_infer.tar
# 预测
!python tools/infer/predict_cls.py \
    --image_dir="./doc/imgs_words/ch/word_1.jpg" \
    --cls_model_dir="./inference/ch_ppocr_mobile_v2.0_cls_infer" \
    --use_gpu=False
# 画图
img = cv2.imread("./doc/imgs_words/ch/word_1.jpg")
plt.imshow(img[:,:,::-1])
plt.show()

图片的方向是正向水平文本,预测结果正确。

具体地,方向分类器的具体实现代码如下所示。

# 参考代码
# https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.4/tools/infer/predict_cls.py
import copy

# 方向分类器实现的类
class TextClassifier(object):
    def __init__(self, args):
        self.cls_image_shape = [int(v) for v in args.cls_image_shape.split(",")]
        self.cls_batch_num = args.cls_batch_num
        self.cls_thresh = args.cls_thresh
        postprocess_params = {
            'name': 'ClsPostProcess',
            "label_list": args.label_list,
        }
        # 后处理算子
        self.postprocess_op = build_post_process(postprocess_params)
        # 初始化预测引擎
        self.predictor, self.input_tensor, self.output_tensors, _ = \
            utility.create_predictor(args, 'cls', logger)

    # 对图像进行resize并且normalize
    def resize_norm_img(self, img):
        imgC, imgH, imgW = self.cls_image_shape
        h = img.shape[0]
        w = img.shape[1]
        ratio = w / float(h)
        if math.ceil(imgH * ratio) > imgW:
            resized_w = imgW
        else:
            resized_w = int(math.ceil(imgH * ratio))
        resized_image = cv2.resize(img, (resized_w, imgH))
        resized_image = resized_image.astype('float32')
        if self.cls_image_shape[0] == 1:
            resized_image = resized_image / 255
            resized_image = resized_image[np.newaxis, :]
        else:
            resized_image = resized_image.transpose((2, 0, 1)) / 255
        resized_image -= 0.5
        resized_image /= 0.5
        padding_im = np.zeros((imgC, imgH, imgW), dtype=np.float32)
        padding_im[:, :, 0:resized_w] = resized_image
        return padding_im

    def __call__(self, img_list):
        img_list = copy.deepcopy(img_list)
        img_num = len(img_list)
        # 记录宽高比
        width_list = []
        for img in img_list:
            width_list.append(img.shape[1] / float(img.shape[0]))
        # 排序,加速后续的预处理过程
        indices = np.argsort(np.array(width_list))

        cls_res = [['', 0.0]] * img_num
        batch_num = self.cls_batch_num
        elapse = 0
        for beg_img_no in range(0, img_num, batch_num):
            end_img_no = min(img_num, beg_img_no + batch_num)
            norm_img_batch = []
            max_wh_ratio = 0
            starttime = time.time()
            # 预处理数据,组batch
            for ino in range(beg_img_no, end_img_no):
                h, w = img_list[indices[ino]].shape[0:2]
                wh_ratio = w * 1.0 / h
                max_wh_ratio = max(max_wh_ratio, wh_ratio)
            for ino in range(beg_img_no, end_img_no):
                norm_img = self.resize_norm_img(img_list[indices[ino]])
                norm_img = norm_img[np.newaxis, :]
                norm_img_batch.append(norm_img)
            norm_img_batch = np.concatenate(norm_img_batch)
            norm_img_batch = norm_img_batch.copy()
            # 将数据拷贝到预测引擎
            self.input_tensor.copy_from_cpu(norm_img_batch)
            # 自动推理过程
            self.predictor.run()
            # 将数据拷贝回CPU
            prob_out = self.output_tensors[0].copy_to_cpu()
            # 后处理
            cls_result = self.postprocess_op(prob_out)
            elapse += time.time() - starttime
            for rno in range(len(cls_result)):
                label, score = cls_result[rno]
                cls_res[indices[beg_img_no + rno]] = [label, score]
                if '180' in label and score > self.cls_thresh:
                    img_list[indices[beg_img_no + rno]] = cv2.rotate(
                        img_list[indices[beg_img_no + rno]], 1)
        return img_list, cls_res, elapse

args = parse_args()
args.cls_model_dir = "./inference/ch_ppocr_mobile_v2.0_cls_infer"
args.image_dir = "./doc/imgs_words/ch/word_4.jpg"

image_file_list = get_image_file_list(args.image_dir)
text_classifier = TextClassifier(args)
valid_image_file_list = []
img_list = []
for image_file in image_file_list:
    img = cv2.imread(image_file)
    # 预测之前对图像旋转180度
    # img = cv2.rotate(img, cv2.ROTATE_180)
    if img is None:
        logger.info("error in loading image:{}".format(image_file))
        continue
    valid_image_file_list.append(image_file)
    img_list.append(img)
img_list, cls_res, predict_time = text_classifier(img_list)
for ino in range(len(img_list)):
    logger.info("Predicts of {}:{}".format(valid_image_file_list[ino],
                                            cls_res[ino]))

plt.imshow(img[:,:,::-1])
plt.show()

这里我们也可以将图像旋转180度之后,看下方向分类器的分类效果。

上面就完成了完整的方向分类器的推理过程。

2.4 PP-OCRv2文字识别模型推理

对于文字识别模型,我们也可以使用下面的命令快速体验其功能。

# 下载模型
!cd inference && wget https://paddleocr.bj.bcebos.com/PP-OCRv2/chinese/ch_PP-OCRv2_rec_infer.tar -O ch_PP-OCRv2_rec_infer.tar &&  tar -xf ch_PP-OCRv2_rec_infer.tar
# 预测
!python tools/infer/predict_rec.py \
    --image_dir="./doc/imgs_words/ch/word_4.jpg" \
    --rec_model_dir="./inference/ch_PP-OCRv2_rec_infer" \
    --use_gpu=False

# 读取图像并显示
img = cv2.imread("./doc/imgs_words/ch/word_4.jpg")
plt.imshow(img[:,:,::-1])
plt.show()

文字识别的具体代码如下所示。

# 参考代码
# https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.4/tools/infer/predict_rec.py
class TextRecognizer(object):
    def __init__(self, args):
        self.rec_image_shape = [int(v) for v in args.rec_image_shape.split(",")]
        self.rec_batch_num = args.rec_batch_num
        self.rec_algorithm = args.rec_algorithm
        postprocess_params = {
            'name': 'CTCLabelDecode',
            "character_dict_path": args.rec_char_dict_path,
            "use_space_char": args.use_space_char
        }
        # 初始化预测引擎
        self.predictor, self.input_tensor, self.output_tensors, self.config = \
            utility.create_predictor(args, 'rec', logger)
        # 初始化后处理过程
        self.postprocess_op = build_post_process(postprocess_params)

    # 预处理核心逻辑
    def resize_norm_img(self, img, max_wh_ratio):
        imgC, imgH, imgW = self.rec_image_shape
        assert imgC == img.shape[2]
        imgW = int((32 * max_wh_ratio))
        h, w = img.shape[:2]
        ratio = w / float(h)
        if math.ceil(imgH * ratio) > imgW:
            resized_w = imgW
        else:
            resized_w = int(math.ceil(imgH * ratio))
        resized_image = cv2.resize(img, (resized_w, imgH))
        resized_image = resized_image.astype('float32')
        # [0, 255] -> [0, 1]
        resized_image = resized_image.transpose((2, 0, 1)) / 255
        # [0, 1] -> [-0.5, 0.5]
        resized_image -= 0.5
        # [-0.5, 0.5] -> [-1, 1]
        resized_image /= 0.5
        padding_im = np.zeros((imgC, imgH, imgW), dtype=np.float32)
        padding_im[:, :, 0:resized_w] = resized_image
        return padding_im

    # 对图像列表进行处理
    def __call__(self, img_list):
        img_num = len(img_list)
        # 记录宽高比
        width_list = []
        for img in img_list:
            width_list.append(img.shape[1] / float(img.shape[0]))
        # 排序,加速处理过程
        indices = np.argsort(np.array(width_list))
        rec_res = [['', 0.0]] * img_num
        batch_num = self.rec_batch_num
        st = time.time()
        for beg_img_no in range(0, img_num, batch_num):
            end_img_no = min(img_num, beg_img_no + batch_num)
            norm_img_batch = []
            max_wh_ratio = 0
            for ino in range(beg_img_no, end_img_no):
                h, w = img_list[indices[ino]].shape[0:2]
                wh_ratio = w * 1.0 / h
                max_wh_ratio = max(max_wh_ratio, wh_ratio)
            # 调用预处理方法并组batch
            for ino in range(beg_img_no, end_img_no):
                norm_img = self.resize_norm_img(img_list[indices[ino]],
                                                max_wh_ratio)
                norm_img = norm_img[np.newaxis, :]
                norm_img_batch.append(norm_img)
            norm_img_batch = np.concatenate(norm_img_batch)
            norm_img_batch = norm_img_batch.copy()
            
            # 将数据拷贝到预测引擎中
            self.input_tensor.copy_from_cpu(norm_img_batch)
            # 自动化推理过程
            self.predictor.run()
            outputs = []
            # 将数据拷贝到CPU
            for output_tensor in self.output_tensors:
                output = output_tensor.copy_to_cpu()
                outputs.append(output)
            if len(outputs) != 1:
                preds = outputs
            else:
                preds = outputs[0]
            # 后处理
            rec_result = self.postprocess_op(preds)
            for rno in range(len(rec_result)):
                rec_res[indices[beg_img_no + rno]] = rec_result[rno]
        return rec_res, time.time() - st


# 定义参数
args = parse_args()
args.rec_model_dir = "./inference/ch_PP-OCRv2_rec_infer"
args.image_dir = "./doc/imgs_words/ch/word_4.jpg"
img_list = []

image_file_list = get_image_file_list(args.image_dir)
text_recognizer = TextRecognizer(args)
valid_image_file_list = []
for image_file in image_file_list:
    img = cv2.imread(image_file)
    if img is None:
        logger.info("error in loading image:{}".format(image_file))
        continue
    valid_image_file_list.append(image_file)
    img_list.append(img)
rec_res, _ = text_recognizer(img_list)
for ino in range(len(img_list)):
    logger.info("Predicts of {}:{}".format(valid_image_file_list[ino],
                                            rec_res[ino]))

2.5 PP-OCRv2 系统串联预测推理

前面的内容给大家介绍了PP-OCRv2系统中,检测、方向分类器、识别模型的单独推理过程。为了方便大家端到端地使用,我们将这三个模块串联起来,组成了PP-OCRv2系统,并提供了相应的预测脚本。

在执行PP-OCRv2的系统推理时,需要通过参数image_dir指定单张图像或者图像集合的路径、参数det_model_dircls_model_dir 和 rec_model_dir 分别指定检测、方向分类和识别的 inference 模型路径。参数 use_angle_cls 用于控制是否启用方向分类模型。use_mp 表示是否使用多进程。total_process_num 表示在使用多进程时的进程数。

以图像文件./doc/imgs/00018069.jpg为例,预测的原始图像如下。

如果串联预测时使用方向分类器,则可以使用下面的命令进行预测。

# 使用方向分类器,运行PP-OCRv2系统
!python tools/infer/predict_system.py \
    --image_dir="./doc/imgs/00018069.jpg" \
    --det_model_dir="./inference/ch_PP-OCRv2_det_infer/" \
    --cls_model_dir="./inference/ch_ppocr_mobile_v2.0_cls_infer/" \
    --rec_model_dir="./inference/ch_PP-OCRv2_rec_infer/" \
    --use_angle_cls=True

# 可视化
img = cv2.imread("./inference_results/00018069.jpg")
plt.figure(figsize=(20, 8))
plt.imshow(img[..., ::-1])
plt.show()

可视化识别结果默认保存到 ./inference_results 文件夹里面。

在图象中可视化出了检测框和识别结果,在上面的notebook中也打印出了具体的识别文件以及文件读取路径信息。

如果希望保存裁剪后的识别结果,可以将save_crop_res参数设置为True,最终结果保存在output目录下,其中部分裁剪后图像如下所示。保存的结果可以用于后续的识别模型标注与训练。

# 裁剪文字检测的结果图像并保存
!python tools/infer/predict_system.py \
    --image_dir="./doc/imgs/00018069.jpg" \
    --det_model_dir="./inference/ch_PP-OCRv2_det_infer/" \
    --cls_model_dir="./inference/ch_ppocr_mobile_v2.0_cls_infer/" \
    --rec_model_dir="./inference/ch_PP-OCRv2_rec_infer/" \
    --use_angle_cls=True \
    --save_crop_res=True

!ls output

plt.figure(figsize=(8, 8))
plt.imshow(cv2.imread("./doc/imgs/00018069.jpg")[:, :, ::-1])
plt.show()
plt.figure(figsize=(14, 4))
plt.subplot(1,3,1)
plt.imshow(cv2.imread("output/mg_crop_0.jpg")[:, :, ::-1])
plt.subplot(1,3,2)
plt.imshow(cv2.imread("output/mg_crop_1.jpg")[:, :, ::-1])
plt.subplot(1,3,3)
plt.imshow(cv2.imread("output/mg_crop_2.jpg")[:, :, ::-1])
plt.show()

串联预测通过TextSystem类进行实现,其具体实现过程与函数定义如下。

# 参考代码:https://github.com/PaddlePaddle/PaddleOCR/blob/release%2F2.4/tools/infer/predict_system.py
from tools.infer.utility import draw_ocr_box_txt, get_rotate_crop_image
from ppocr.utils.utility import get_image_file_list

class TextSystem(object):
    # 初始化函数
    def __init__(self, args):
        self.args = args
  		# 如果不希望显示log,可以将show_log设置为False
        if not args.show_log:
            logger.setLevel(logging.INFO)
        # 定义文本检测模型预测引擎
        self.text_detector = TextDetector(args)
        # 定义文本识别模型预测引擎
        self.text_recognizer = TextRecognizer(args)
        # 是否使用方向分类器
        self.use_angle_cls = args.use_angle_cls
        # 得分阈值,根据该阈值判断检测与识别结果是否需要进行可视化或者返回
        self.drop_score = args.drop_score
        # 定义方向分类器预测引擎
        if self.use_angle_cls:
            self.text_classifier = TextClassifier(args)
	
  	# 保存文本检测结果图像
    def draw_crop_rec_res(self, output_dir, img_crop_list, rec_res):
        os.makedirs(output_dir, exist_ok=True)
        bbox_num = len(img_crop_list)
        for bno in range(bbox_num):
            cv2.imwrite(
                os.path.join(output_dir,
                             f"mg_crop_{bno+self.crop_image_res_index}.jpg"),
                img_crop_list[bno])
            logger.debug(f"{bno}, {rec_res[bno]}")
        self.crop_image_res_index += bbox_num
	
    # 核心预测函数
    def __call__(self, img, cls=True):
        ori_im = img.copy()
        # 获取检测文本检测结果
        dt_boxes, elapse = self.text_detector(img)
        logger.debug("dt_boxes num : {}, elapse : {}".format(
            len(dt_boxes), elapse))
        if dt_boxes is None:
            return None, None
        img_crop_list = []
        # 对检测框进行排序,顺序为:优先从上到下,其次从左到右
        dt_boxes = sorted_boxes(dt_boxes)
        # 对检测结果进行透视变换与校正
        for bno in range(len(dt_boxes)):
            tmp_box = copy.deepcopy(dt_boxes[bno])
            img_crop = get_rotate_crop_image(ori_im, tmp_box)
            img_crop_list.append(img_crop)
        # 使用方向分类器对检测结果进行转正
        if self.use_angle_cls and cls:
            img_crop_list, angle_list, elapse = self.text_classifier(
                img_crop_list)
            logger.debug("cls num  : {}, elapse : {}".format(
                len(img_crop_list), elapse))
        # 获取文本识别结果
        rec_res, elapse = self.text_recognizer(img_crop_list)
        logger.debug("rec_res num  : {}, elapse : {}".format(
            len(rec_res), elapse))
        # 保存经过校正之后的文本检测图像
        if self.args.save_crop_res:
            self.draw_crop_rec_res(self.args.crop_res_save_dir, img_crop_list,
                                   rec_res)
        filter_boxes, filter_rec_res = [], []
        # 根据识别得分的阈值对结果进行过滤,如果得分小于阈值,就过滤掉
        for box, rec_result in zip(dt_boxes, rec_res):
            text, score = rec_result
            if score >= self.drop_score:
                filter_boxes.append(box)
                filter_rec_res.append(rec_result)
        return filter_boxes, filter_rec_res

def sorted_boxes(dt_boxes):
    # 对检测框进行排序:优先从上到下,其次从左到右
    num_boxes = dt_boxes.shape[0]
    sorted_boxes = sorted(dt_boxes, key=lambda x: (x[0][1], x[0][0]))
    _boxes = list(sorted_boxes)

    for i in range(num_boxes - 1):
        if abs(_boxes[i + 1][0][1] - _boxes[i][0][1]) < 10 and \
                (_boxes[i + 1][0][0] < _boxes[i][0][0]):
            tmp = _boxes[i]
            _boxes[i] = _boxes[i + 1]
            _boxes[i + 1] = tmp
    return _boxes

args = parse_args()
args.cls_model_dir = "./inference/ch_ppocr_mobile_v2.0_cls_infer"
args.det_model_dir="./inference/ch_PP-OCRv2_det_infer/"
args.rec_model_dir="./inference/ch_PP-OCRv2_rec_infer/"
args.image_dir = "./doc/imgs/00018069.jpg"
args.use_angle_cls=True
args.use_gpu=True

image_file_list = get_image_file_list(args.image_dir)
image_file_list = image_file_list[args.process_id::args.total_process_num]
text_sys = TextSystem(args)
is_visualize = True
font_path = args.vis_font_path
drop_score = args.drop_score

total_time = 0
cpu_mem, gpu_mem, gpu_util = 0, 0, 0
_st = time.time()
count = 0
for idx, image_file in enumerate(image_file_list):
    img = cv2.imread(image_file)
    if img is None:
        logger.debug("error in loading image:{}".format(image_file))
        continue
    starttime = time.time()
    dt_boxes, rec_res = text_sys(img)
    elapse = time.time() - starttime
    total_time += elapse

    logger.debug(
        str(idx) + "  Predict time of %s: %.3fs" % (image_file, elapse))
    for text, score in rec_res:
        logger.debug("{}, {:.3f}".format(text, score))

    if is_visualize:
        image = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
        boxes = dt_boxes
        txts = [rec_res[i][0] for i in range(len(rec_res))]
        scores = [rec_res[i][1] for i in range(len(rec_res))]

        draw_img = draw_ocr_box_txt(
            image,
            boxes,
            txts,
            scores,
            drop_score=drop_score,
            font_path=font_path)
        draw_img_save_dir = args.draw_img_save_dir
        os.makedirs(draw_img_save_dir, exist_ok=True)
        cv2.imwrite(
            os.path.join(draw_img_save_dir, os.path.basename(image_file)),
            draw_img[:, :, ::-1])
        logger.debug("The visualized image saved in {}".format(
            os.path.join(draw_img_save_dir, os.path.basename(image_file))))

logger.info("The predict total time is {}".format(time.time() - _st))

plt.figure(figsize=(8, 8))
plt.imshow(image)
plt.show()
plt.figure(figsize=(16, 8))
plt.imshow(draw_img)
plt.show()

2.6 PP-OCRv2 使用whl包预测推理

为了更加方便快速体验OCR文本检测与识别模型,PaddleOCR提供了基于Paddle Inference预测引擎的whl包,方便您一键安装,体验PaddleOCR。

2.6.1 安装whl包

使用pip安装paddleocr的whl包,命令如下。

!pip install "paddleocr==2.3.0.2"

# 如果希望获取最新特性,可以基于源码编译安装
#     python3 setup.py bdist_wheel
#     pip3 install dist/paddleocr-x.x.x-py3-none-any.whl # x.x.x是paddleocr的版本号

2.6.2 使用whl包预测推理

paddleocr whl包会自动下载PP-OCRv2超轻量模型作为默认模型,也支持自定义模型路径、预测配置等参数,参数名称与基于Paddle Inference的python预测中参数相同。

  • 单独执行检测。

运行下面的代码,可快速体验文本检测模型的预测与效果。

from paddleocr import PaddleOCR, draw_ocr

ocr = PaddleOCR(use_gpu=False)  # need to run only once to download and load model into memory
img_path = '/home/aistudio/PaddleOCR/doc/imgs/11.jpg'
result = ocr.ocr(img_path, rec=False)
for line in result:
    print(line)

# 显示结果
from PIL import Image

image = Image.open(img_path).convert('RGB')
im_show = draw_ocr(image, result, txts=None, scores=None, font_path='/home/aistudio/PaddleOCR/doc/fonts/simfang.ttf')
plt.figure(figsize=(15, 8))
plt.imshow(im_show)
plt.show()
  • 单独执行识别

可以指定det=False,仅运行单独的识别模块。

from paddleocr import PaddleOCR

ocr = PaddleOCR(use_gpu=False)  # need to run only once to download and load model into memory
img_path = '/home/aistudio/PaddleOCR/doc/imgs_words/ch/word_1.jpg'
result = ocr.ocr(img_path, det=False)
for line in result:
    print(line)
  • 单独执行方向分类器

可以指定det=False, rec=False, cls=True,仅运行方向分类器。

from paddleocr import PaddleOCR

ocr = PaddleOCR(use_angle_cls=True, use_gpu=False)  # need to run only once to download and load model into memory
img_path = '/home/aistudio/PaddleOCR/doc/imgs_words/ch/word_1.jpg'
result = ocr.ocr(img_path, det=False, rec=False, cls=True)
for line in result:
    print(line)

img = cv2.imread(img_path)
plt.imshow(img[...,::-1])
plt.show()
  • 检测+方向分类器+识别全流程体验
from paddleocr import PaddleOCR, draw_ocr
import matplotlib.pyplot as plt
%matplotlib inline

# PaddleOCR目前支持中英文、英文、法语、德语、韩语、日语,可以通过修改lang参数进行切换
# 参数依次为`ch`, `en`, `french`, `german`, `korean`, `japan`。
ocr = PaddleOCR(use_angle_cls=True, lang="ch", use_gpu=False)  # need to run only once to download and load model into memory
img_path = '/home/aistudio/PaddleOCR/doc/imgs/11.jpg'
result = ocr.ocr(img_path, cls=True)
for line in result:
    print(line)

# 显示结果
from PIL import Image

image = Image.open(img_path).convert('RGB')
boxes = [line[0] for line in result]
txts = [line[1][0] for line in result]
scores = [line[1][1] for line in result]
im_show = draw_ocr(image, boxes, txts, scores, font_path='/home/aistudio/PaddleOCR/doc/fonts/simfang.ttf')
plt.figure(figsize=(15, 8))
plt.imshow(im_show)
plt.show()

结果是一个list,每个item包含了文本框,文字和识别置信度。

3 基于Paddle Inference的C++推理

在推理部署过程中,相比于Python,C++的性能一般会更好一些,因此很多实际推理场景中会考虑使用C++作为开发语言进行推理。

上一小节给大家介绍的Paddle Inference也支持C++的推理过程,本节主要介绍C++的PP-OCRv2推理过程。

在基于Paddle Inference,对PP-OCRv2系统使用C++推理时,有以下几个步骤。

(1)准备模型

(2)编译opencv库

(3)获取Paddle Inference预测库

(4)编译PaddleOCR C++推理代码

(5)运行PP-OCRv2系统

由于AiStudio上版本限制,这里不做具体演示过程,仅给大家介绍具体的流程。建议您在本地体验PP-OCRv2的C++推理过程。

关于本小节更加详细的内容可以参考:PP-OCRv2 C++推理教程

3.1 准备模型

使用下面的命令,准备PP-OCRv2的推理模型。

cd deploy/cpp_infer
wget https://paddleocr.bj.bcebos.com/PP-OCRv2/chinese/ch_PP-OCRv2_det_infer.tar -O ch_PP-OCRv2_det_infer.tar && tar -xf ch_PP-OCRv2_det_infer.tar
wget https://paddleocr.bj.bcebos.com/PP-OCRv2/chinese/ch_PP-OCRv2_rec_infer.tar -O ch_PP-OCRv2_rec_infer.tar &&  tar -xf ch_PP-OCRv2_rec_infer.tar
wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_infer.tar -O ch_ppocr_mobile_v2.0_cls_infer.tar && tar -xf ch_ppocr_mobile_v2.0_cls_infer.tar

3.2 编译OpenCV库

  • 首先需要从opencv官网上下载在Linux环境下源码编译的包,以opencv3.4.7为例,下载命令如下。
wget https://paddleocr.bj.bcebos.com/libs/opencv/opencv-3.4.7.tar.gz
tar -xf opencv-3.4.7.tar.gz

最终可以在当前目录下看到opencv-3.4.7/的文件夹。

  • 编译OpenCV,设置OpenCV源码路径(root_path)以及安装路径(install_path)。进入opencv源码路径下,按照下面的方式进行编译。
root_path="your_opencv_root_path"
install_path=${root_path}/opencv3
build_dir=${root_path}/build

rm -rf ${build_dir}
mkdir ${build_dir}
cd ${build_dir}

cmake .. \
    -DCMAKE_INSTALL_PREFIX=${install_path} \
    -DCMAKE_BUILD_TYPE=Release \
    -DBUILD_SHARED_LIBS=OFF \
    -DWITH_IPP=OFF \
    -DBUILD_IPP_IW=OFF \
    -DWITH_LAPACK=OFF \
    -DWITH_EIGEN=OFF \
    -DCMAKE_INSTALL_LIBDIR=lib64 \
    -DWITH_ZLIB=ON \
    -DBUILD_ZLIB=ON \
    -DWITH_JPEG=ON \
    -DBUILD_JPEG=ON \
    -DWITH_PNG=ON \
    -DBUILD_PNG=ON \
    -DWITH_TIFF=ON \
    -DBUILD_TIFF=ON

make -j
make install

也可以直接修改tools/build_opencv.sh的内容,然后直接运行下面的命令进行编译。

sh tools/build_opencv.sh

其中root_path为下载的opencv源码路径,install_path为opencv的安装路径,make install完成之后,会在该文件夹下生成opencv头文件和库文件,用于后面的OCR代码编译。

最终在安装路径下的文件结构如下所示。

opencv3/
|-- bin
|-- include
|-- lib
|-- lib64
|-- share

3.3 获取Paddle Inference预测库

  • Paddle预测库官网 上提供了不同cuda版本的Linux预测库,可以在官网根据自己的环境选择合适的预测库版本。

  • 下载之后使用下面的方法解压。

wget https://paddle-inference-lib.bj.bcebos.com/2.2.1/cxx_c/Linux/GPU/x86-64_gcc8.2_avx_mkl_cuda10.2_cudnn8.1.1_trt7.2.3.4/paddle_inference.tgz -O paddle_inference.tgz
tar -xf paddle_inference.tgz

最终会在当前的文件夹中生成paddle_inference/的子文件夹。

3.4 编译PaddleOCR的推理代码

编译命令如下,其中Paddle C++预测库、opencv等其他依赖库的地址需要换成自己机器上的实际地址。

sh tools/build.sh
  • 具体的,需要修改tools/build.sh中环境路径,相关内容如下:
OPENCV_DIR=your_opencv_dir
LIB_DIR=your_paddle_inference_dir
CUDA_LIB_DIR=your_cuda_lib_dir
CUDNN_LIB_DIR=/your_cudnn_lib_dir

其中,OPENCV_DIR为opencv编译安装的地址;LIB_DIR为下载(paddle_inference文件夹)或者编译生成的Paddle预测库地址(build/paddle_inference_install_dir文件夹);CUDA_LIB_DIR为cuda库文件地址,在docker中为/usr/local/cuda/lib64CUDNN_LIB_DIR为cudnn库文件地址,在docker中为/usr/lib/x86_64-linux-gnu/注意:以上路径都写绝对路径,不要写相对路径。

  • 编译完成之后,会在build文件夹下生成一个名为ppocr的可执行文件。

3.5 运行PP-OCRv2系统

运行方式:

./build/ppocr <mode> [--param1] [--param2] [...]

其中,mode为必选参数,表示选择的功能,取值范围['det', 'rec', 'system'],分别表示调用检测、识别、检测识别串联(包括方向分类器)。具体命令如下:

  • 只运行文字检测模型
./build/ppocr det \
    --det_model_dir=./ch_PP-OCRv2_det_infer/ \
    --image_dir=../../doc/imgs/12.jpg
  • 只运行文字识别模型
./build/ppocr rec \
    --rec_model_dir=./ch_PP-OCRv2_rec_infer/ \
    --image_dir=../../doc/imgs_words/ch/
  • 运行PP-OCRv2系统
# 不使用方向分类器
./build/ppocr system \
    --det_model_dir=./ch_PP-OCRv2_det_infer/ \
    --rec_model_dir=./ch_PP-OCRv2_rec_infer/ \
    --image_dir=../../doc/imgs/12.jpg

# 使用方向分类器
./build/ppocr system \
    --det_model_dir=./ch_PP-OCRv2_det_infer/ \
    --rec_model_dir=./ch_PP-OCRv2_rec_infer/ \
    --use_angle_cls=true \
    --cls_model_dir=./ch_ppocr_mobile_v2.0_cls_infer \
    --image_dir=../../doc/imgs/12.jpg

4 Paddle Serving服务化部署实战

在第2和第3节内容中,我们详细介绍了基于Paddle Inference的PP-OCRv2系统推理,它属于离线推理,即在特定机器上部署的代码只能在这台机器上使用,无法通过其他机器进行访问。因此模型服务化部署的需求也就衍生出来。

服务化部署指的是,将模型以服务的形式进行部署,其他的设备可以通过发送请求的形式去访问服务,从而获取模型服务的推理结果。服务化部署示意图如下所示。

在模型部署成功后,不同用户都可以通过客户端,以发送网络请求的方式获得推理服务。

Paddle Serving是飞桨为方便开发者进行服务化部署而打造的工具,本节主要介绍基于Paddle Serving的PP-OCRv2系统服务化部署过程。

4.1 Paddle Serving简介

Paddle Serving作为飞桨(PaddlePaddle)开源的服务化部署框架,长期目标就是围绕着人工智能落地的最后一公里提供越来越专业、可靠、易用的服务。Paddle Serving目前提供了两套框架C++ Serving和Python Pipeline。Python Pipeline框架倾向于二次开发的便捷性,C++ Serving框架更倾向于追求极致性能。

基于Paddle Serving进行PP-OCRv2模型的服务化部署时,流程如下所示。

4.2 准备预测数据和部署环境

数据与模型推理所用数据一致。

运行Paddle Serving,需要安装Paddle Serving三个安装包:paddle-serving-server、paddle-serving-client 和 paddle-serving-app,命令如下。

!wget https://paddle-serving.bj.bcebos.com/test-dev/whl/paddle_serving_server_gpu-0.7.0.post102-py3-none-any.whl
!pip install paddle_serving_server_gpu-0.7.0.post102-py3-none-any.whl

!wget https://paddle-serving.bj.bcebos.com/test-dev/whl/paddle_serving_client-0.7.0-cp37-none-any.whl
!pip install paddle_serving_client-0.7.0-cp37-none-any.whl

!wget https://paddle-serving.bj.bcebos.com/test-dev/whl/paddle_serving_app-0.7.0-py3-none-any.whl
!pip install paddle_serving_app-0.7.0-py3-none-any.whl

!rm ./*.whl

4.3 准备部署模型

在进行模型服务化部署时,首先需要将推理模型转为用户服务化部署的模型。

首先运行下面的命令下载推理模型。

os.chdir("/home/aistudio/PaddleOCR/deploy/pdserving/")

# 下载并解压 OCR 文本检测模型
!wget https://paddleocr.bj.bcebos.com/PP-OCRv2/chinese/ch_PP-OCRv2_det_infer.tar -O ch_PP-OCRv2_det_infer.tar && tar -xf ch_PP-OCRv2_det_infer.tar && rm ch_PP-OCRv2_det_infer.tar
# 下载并解压 OCR 文本识别模型
!wget https://paddleocr.bj.bcebos.com/PP-OCRv2/chinese/ch_PP-OCRv2_rec_infer.tar -O ch_PP-OCRv2_rec_infer.tar &&  tar -xf ch_PP-OCRv2_rec_infer.tar && rm ch_PP-OCRv2_rec_infer.tar

运行下面的命令进行模型转换。

# 转换检测模型
!python -m paddle_serving_client.convert --dirname ./ch_PP-OCRv2_det_infer/ \
                                         --model_filename inference.pdmodel          \
                                         --params_filename inference.pdiparams       \
                                         --serving_server ./ppocrv2_det_serving/ \
                                         --serving_client ./ppocrv2_det_client/

# 转换识别模型
!python -m paddle_serving_client.convert --dirname ./ch_PP-OCRv2_rec_infer/ \
                                         --model_filename inference.pdmodel          \
                                         --params_filename inference.pdiparams       \
                                         --serving_server ./ppocrv2_rec_serving/  \
                                         --serving_client ./ppocrv2_rec_client/

# 查看文件夹
!tree -h *_client *_serving

检测模型转换完成后,会在当前文件夹多出ppocrv2_det_mobile_serving 和ppocrv2_det_mobile_client的文件夹,具备如下格式:

|- ppocrv2_det_mobile_serving/
  |- __model__  
  |- __params__
  |- serving_server_conf.prototxt  
  |- serving_server_conf.stream.prototxt

|- ppocrv2_det_mobile_client
  |- serving_client_conf.prototxt  
  |- serving_client_conf.stream.prototxt

识别模型同理。

4.4 Paddle Serving pipeline部署

注意: 将PaddleOCR/deploy/pdserving/config.yml文件中的两个model_config字段分别修改为ppocrv2_det_mobile_serving、ppocrv2_rec_mobile_serving,对应模型转换的文件夹。

pdserving目录包含启动pipeline服务和发送预测请求的代码,包括:
```
__init__.py
config.yml            # 启动服务的配置文件
ocr_reader.py         # OCR模型预处理和后处理的代码实现
pipeline_http_client.py   # 发送预测请求的脚本
web_service.py        # 启动服务端的脚本
```

4.4.1 启动服务

运行如下命令启动服务:

开启新的终端运行下列启动服务的命令

```
# 启动服务,运行日志保存在web_serving_log.txt
cd PaddleOCR/deploy/pdserving/
nohup python web_service.py &>web_serving_log.txt &
```

成功启动服务后,web_serving_log.txt中会打印类似如下日志

4.4.2 发送服务请求:

!python pipeline_http_client.py
04-13 04:04