飞桨OCR打标、训练、预测、部署全流程

注:本文档全部在Windows10环境下操作

注:本文档使用的飞桨OCR全景项目代码版本为 release/2.4

查询文档列表:

飞桨OCR官方中文文档:https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.4/README_ch.md

飞桨OCR for pdserving 部署官方文档:https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.4/deploy/pdserving/README_CN.md

飞桨OCR for hubserving 部署官方文档:https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.4/deploy/hubserving/readme.md

飞桨OCR官方标注工具的使用文档:https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.4/PPOCRLabel/README_ch.md

飞桨OCR表格识别的官方文档:https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.4/ppstructure/table/README_ch.md

飞桨OCR官方给的一些训练和预测数据集:https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.4/doc/doc_ch/datasets.md

飞桨OCR官方训练文档:https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.4/doc/doc_ch/training.md

1、准备工作

1.1、本地拉取飞桨OCR的全量代码,调试和开发都能用到

【推荐】git clone https://github.com/PaddlePaddle/PaddleOCR

如果因为网络问题无法 pull 成功,也可选择使用码云上的托管:

git clone https://gitee.com/paddlepaddle/PaddleOCR

1.2、创建虚拟环境

根据自己的电脑开发环境,为项目创建一个虚拟环境,并且在使用项目时激活这个虚拟环境使用

对新手推荐的方法:

  • 安装 Python3 开发环境,推荐安装 Python3.8 版本,直接下载 Python 官网的 Windows 安装包即可,不会的百度即可
  • 安装 virtualenv 三方包,打开一个终端:pip install virtualenv
  • 进入项目目录:cd .\PaddleOCR\
  • 创建虚拟环境:virtualenv venv
  • 激活虚拟环境(在powershell下):.\venv\Scripts\activate.ps1

1.3、在虚拟环境中安装项目依赖

由于飞桨内维护的 requirements.txt 依赖不足,需要创建一个新的依赖文件 newrequirements.txt ,将一下内容拷贝进去

aiofiles==0.8.0
astor==0.8.1
Babel==2.9.1
backports.entry-points-selectable==1.1.1
bce-python-sdk==0.8.64
cachetools==5.0.0
certifi==2021.10.8
cffi==1.15.0
cfgv==3.3.1
chardet==4.0.0
charset-normalizer==2.0.9
click==7.1.2
colorama==0.4.4
colorlog==6.6.0
cryptography==36.0.1
cssselect==1.1.0
cssutils==2.3.0
cycler==0.11.0
Cython==0.29.26
decorator==5.1.0
dill==0.3.4
distlib==0.3.4
easydict==1.9
et-xmlfile==1.1.0
fasttext==0.9.1
filelock==3.4.2
flake8==4.0.1
Flask==1.1.4
Flask-Babel==2.0.0
fonttools==4.28.5
func-timeout==4.3.5
future==0.18.2
grpcio==1.33.2
grpcio-tools==1.33.2
h5py==3.6.0
httptools==0.3.0
identify==2.4.0
idna==3.3
imageio==2.13.5
imgaug==0.4.0
iopath==0.1.9
itsdangerous==1.1.0
jieba==0.42.1
Jinja2==2.11.3
joblib==1.1.0
kiwisolver==1.3.2
layoutparser==0.3.2
lmdb==1.2.1
lxml==4.7.1
MarkupSafe==1.1.1
matplotlib==3.5.1
mccabe==0.6.1
multidict==5.2.0
multiprocess==0.70.12.2
networkx==2.6.3
nodeenv==1.6.0
numpy==1.19.3
onnx==1.9.0
opencv-contrib-python==4.4.0.46
opencv-python==4.2.0.32
openpyxl==3.0.9
packaging==21.3
paddle-serving-server==0.5.0
paddle-serving-server-gpu @ file:///D:/aeas/PaddleOCR/paddle_serving_server_gpu-0.7.0.post102-py3-none-any.whl
paddle2onnx==0.9.0
paddlehub==2.2.0
paddlenlp==2.2.2
paddleocr==2.3.0.2
paddlepaddle==2.2.1
pandas==1.3.5
pdf2image==1.16.0
pdfminer.six==20211012
pdfplumber==0.6.0
Pillow==8.4.0
platformdirs==2.4.1
portalocker==2.3.2
pre-commit==2.16.0
premailer==3.10.0
protobuf==3.19.1
pybind11==2.8.1
pyclipper==1.3.0.post2
pycodestyle==2.8.0
pycparser==2.21
pycryptodome==3.12.0
pyflakes==2.4.0
pyparsing==3.0.6
PyQt5==5.15.6
PyQt5-Qt5==5.15.2
PyQt5-sip==12.9.0
python-dateutil==2.8.2
python-Levenshtein==0.12.2
pytz==2021.3
PyWavelets==1.2.0
pywin32==303
PyYAML==6.0
pyzmq==22.3.0
rarfile==4.0
requests==2.26.0
sanic==21.12.0
sanic-routing==0.7.2
scikit-image==0.19.1
scikit-learn==1.0.2
scipy==1.7.3
sentencepiece==0.1.92
seqeval==1.2.2
Shapely==1.8.0
shellcheck-py==0.8.0.3
six==1.16.0
threadpoolctl==3.0.0
tifffile==2021.11.2
toml==0.10.2
tqdm==4.62.3
typing_extensions==4.0.1
urllib3==1.26.7
virtualenv==20.10.0
visualdl==2.2.2
Wand==0.6.7
websockets==10.1
Werkzeug==1.0.1

为防止网络原因安装失败,指定 pypi 为阿里源

pip install -r newrequirements.txt -i https://mirrors.aliyun.com/pypi/simple/

1.3、下载官方模型,用于二次训练以及预测和部署

官方推荐、也是本文档使用的模型:

文本检测模型

文本识别模型

方向分类器模型

在 PaddleOCR 目录中创建一个文件夹,命名为 inference 并进入该目录,将下载的模型压缩包都拷贝进来

解压模型:

tar xf ch_PP-OCRv2_det_infer.tar
tar xf ch_PP-OCRv2_rec_infer.tar
tar xf ch_ppocr_mobile_v2.0_cls_infer.tar

2、如何进行数据打标

2.1、原始数据准备

这里以身份证正反面图片数据为例,提前下载好所有需要打标的数据,单个文件夹内数据较多时,推荐每500张图像为一个分组,注意名字中不能存在中文字符。目录名示例 train0-499。

2.2、工具准备

打开第一大项中克隆好的项目,并确保依赖都已安装完毕,进入 PPOCRLabel 目录

cd .\PPOCRLabel\

修改 PPOCRLabel 下的 PPOCRLabel.py 文件,修改内容如下:

1、将159行的 self.autoSaveNum = 5 改为 self.autoSaveNum = 1,目的是设置打标工具每完成一个打标时便自动保存标注
2、将1893行的 if self.noLabelText == shape.label or result[1][0] == shape.label: 改为以下代码,目的是防止闪退
if len(result) < 2:
    print('没有识别到数据')
if self.noLabelText == shape.label or (len(result) > 2 and result[1][0] == shape.label):

启动打标工具,–lang=ch 指定语言为中文,默认为英文

在启动时打标工具会自行下载官方的推理模型到系统文件夹中,默认的地址一般为:C:\Users\Administrator\.paddleocr\2.3.0.2\ocr

找不到也没关系,在打标工具启动的时候会打印这个地址,这里地址需要记录,会在后期检验二次训练模型识别率时用到

python .\PPOCRLabel.py --lang=ch

此时会自动打开一个打标窗口,此时点击 文件 --> 打开目录 --> 选择自己需要打标的目录确认即可

打开后可以看到文件加载完毕后,点击打标工具左下角的 自动标注 按钮,即可开启自动打标流程,此时会运行官方的是OCR模型完成检测、识别、标注功能。

2.3、确认标注内容

在打标工具自动表中完成后,返回到第一个文件,此时如果弹出一个确认,需要点击取消,否则会认为最后一个文件确认。

检查流程:

  • 从第一个文件开始,逐个检查每个图像的标注框是否正常,识别内容是否准确
  • 如果发现标注框异常的,可以自行调整至正常,或者删除掉,重新手动标注,手动标注后,点击右上角的 重新识别 按钮
  • 再重新识别后检测识别结果是否准备,如果错误,需要手动更正。
  • 确保标注框和识别结果准确无误后,点击右下角的 确认 按钮,即可完成一个图像的标注
  • 快捷键提示:W 打开矩形标注,Q 打开四点标注

打标完成后,会自动生成好训练模型可以使用的标注文件 Label.txt,可以在打标目录中查看

打标 标注框 的原则:

  • 换行的文本,每行打一个标注框
  • 文件间隔太大的要断开打标
  • 保持文本连续性打标,例如 身份证中不应该将姓名字段和真实姓名分开打标

标注的打标参考:

身份证正面:
在这里插入图片描述

身份证背面:
在这里插入图片描述

3、如何进行二次训练(实例基于身份证正面文本检测的二次训练)

3.1、数据准备

拿到步骤二中的标注数据,假设你的数据集目录名为:idcard_front,需要分成训练图像和测试图像,这个占比大约是训练80%,测试20%。

处理训练和测试数据:

  • 在 idcard_front 内创建一个 text_localization 文件夹,然后在 text_localization 内创建 idcard_front_train_imgs 和idcard_front_test_imgs 文件夹,再创建两个文件分别为 train_label.txt 和 test_label.txt

  • 将 idcard_front 中前面80%的图像移动到 idcard_front_train_imgs 目录中

  • 将 idcard_front 中剩余20%的图像移动到 idcard_front_test_imgs 目录中

  • 将 idcard_front 中的 Label.txt 的训练部分的标注移动到 train_label.txt 中,并将文件中的图像名改为正常

    提供的标注文件格式如下,中间用"\t"分隔:

    " 图像文件名                          json.dumps编码的图像标注信息"
    idcard_front_train_imgs/img_1.jpg	[{
          
          "transcription": "MASA", "points": [[310, 104], [416, 141], [418, 216], [312, 179]]}, {
          
          ...}]
    
  • 测试部分的标签处理同上

  • 最后删除掉 Label.txt 文件,然后将 idcard_front 整个移动到 PaddleOCR/train_data/ 目录中

处理后 PaddleOCR/train_data/ 有两个文件夹和两个文件,应该按照如下方式组织 idcard_front 数据集:

/PaddleOCR/train_data/idcard_front/text_localization/
  └─ idcard_front_train_imgs/    idcard_front数据集的训练数据
  └─ idcard_front_test_imgs/     idcard_front数据集的测试数据
  └─ train_label.txt             idcard_front数据集的训练标注
  └─ test_label.txt              idcard_front数据集的测试标注

3.2、训练准备

下载预训练模型:

文本检测预训练模型下载地址

准备预训练模型:

在 PaddleOCR 中创建一个 文件夹命名为 pretrain_models,然后将下载的预训练模型拷贝到 pretrain_models 目录中

修改训练配置文件:配置文件位置 PaddleOCR/configs/det/det_mv3_db.yml

Global:
  use_gpu: false # 1、如果是使用CPU训练,要关闭这个选项
  ....
  ....
Train:
  dataset:
    name: SimpleDataSet
    data_dir: ./train_data/idcard_front/text_localization/ # 修改为你的训练目录
    label_file_list:
      - ./train_data/idcard_front/text_localization/train_label.txt # 修改为你的训练标签地址
    ....
	....
Eval:
  dataset:
    name: SimpleDataSet
    data_dir: ./train_data/idcard_front/text_localization/ # 修改为你的测试目录
    label_file_list:
      - ./train_data/idcard_front/text_localization/test_label.txt # 修改为你的测试标签文件
    ....
    ....

3.3、开始训练

# 单机单卡训练 mv3_db 模型
python tools/train.py -c configs/det/det_mv3_db.yml -o Global.pretrained_model=./pretrain_models/MobileNetV3_large_x0_5_pretrained

# 单机多卡训练,通过 --gpus 参数设置使用的GPU ID
python -m paddle.distributed.launch --gpus '0,1,2,3' tools/train.py -c configs/det/det_mv3_db.yml \
     -o Global.pretrained_model=./pretrain_models/MobileNetV3_large_x0_5_pretrained

# 多机多卡训练,通过 --ips 参数设置使用的机器IP地址,通过 --gpus 参数设置使用的GPU ID
python -m paddle.distributed.launch --ips="xx.xx.xx.xx,xx.xx.xx.xx" --gpus '0,1,2,3' tools/train.py -c configs/det/det_mv3_db.yml \
     -o Global.pretrained_model=./pretrain_models/MobileNetV3_large_x0_5_pretrained

当训练完成时会将训练好的模型输出到 ./output/db_mv3/latest 目录下

3.4、导出训练模型为预测模型

python tools\export_model.py -c configs/det/det_mv3_db.yml -o Global.pretrained_model=./output/db_mv3/latest Global.save_inference_dir=./inference

运行完成后,会将预测模型输出到 ./inference 目录下

3.5、如何评估二次训练的模型识别率

  • 先准备一批未进行训练、测试、打标的原始图像,放到两个文件夹中。
  • 将第一文件夹使用打标工具打开,进行自动标注后,关闭标注工具。
  • 用二次训练的模型替换掉打标工具使用的文件检测模型,前面记录的地址 C:\Users\Administrator\.paddleocr\2.3.0.2\ocr
  • 重新启动打标工具,打开第二个文件夹,进行自动标注。
  • 再次打开一个标注工具窗口,然后打开第一个文件夹,比较两个文件中的标注情况,通过实际效果来评估新模型的识别率。

4、Linux下,基于Pdserving部署识别服务

本文档实测可用的部署代码仓库:https://gitee.com/aeasringnar/pdserving.git

4.1、模型转换

将预测模型导出为部署模型

# 进入项目
cd ./deploy/pdserving/
# 导出检测模型
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_serving/
# 导出识别模型
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/

导出成功后会输出到 ppocrv2_det_serving 目录中

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

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

|- ppocrv2_det_client
  |- serving_client_conf.prototxt  
  |- serving_client_conf.stream.prototxt

识别模型同理。

4.2、配置文件

配置文件位置:./config.yml

调整 config.yml 中的并发个数获得最大的QPS, 一般检测和识别的并发数为2:1

#rpc端口, rpc_port和http_port不允许同时为空。当rpc_port为空且http_port不为空时,会自动将rpc_port设置为http_port+1
rpc_port: 18091
#http端口, rpc_port和http_port不允许同时为空。当rpc_port可用且http_port为空时,不自动生成http_port
http_port: 9998
#worker_num, 最大并发数。当build_dag_each_worker=True时, 框架会创建worker_num个进程,每个进程内构建grpcSever和DAG
##当build_dag_each_worker=False时,框架会设置主线程grpc线程池的max_workers=worker_num
worker_num: 2
#build_dag_each_worker, False,框架在进程内创建一条DAG;True,框架会每个进程内创建多个独立的DAG
build_dag_each_worker: False
dag:
    #op资源类型, True, 为线程模型;False,为进程模型
    is_thread_op: True
    #重试次数
    retry: 10
    #使用性能分析, True,生成Timeline性能数据,对性能有一定影响;False为不使用
    use_profile: False
    tracer:
        interval_s: 10
op:
    det:
        #并发数,is_thread_op=True时,为线程并发;否则为进程并发
        concurrency: 2
        #当op配置没有server_endpoints时,从local_service_conf读取本地服务配置
        local_service_conf:
            #client类型,包括brpc, grpc和local_predictor.local_predictor不启动Serving服务,进程内预测
            client_type: local_predictor
            #det模型路径
            model_config: ./ppocrv2_det_serving
            #Fetch结果列表,以client_config中fetch_var的alias_name为准
            fetch_list: ["save_infer_model/scale_0.tmp_1"]
            #计算硬件ID,当devices为""或不写时为CPU预测;当devices为"0", "0,1,2"时为GPU预测,表示使用的GPU卡
            devices: ""
            ir_optim: True
    rec:
        #并发数,is_thread_op=True时,为线程并发;否则为进程并发
        concurrency: 1
        #超时时间, 单位ms
        timeout: -1
        #Serving交互重试次数,默认不重试
        retry: 1
        #当op配置没有server_endpoints时,从local_service_conf读取本地服务配置
        local_service_conf:
            #client类型,包括brpc, grpc和local_predictor。local_predictor不启动Serving服务,进程内预测
            client_type: local_predictor
            #rec模型路径
            model_config: ./ppocrv2_rec_serving
            #Fetch结果列表,以client_config中fetch_var的alias_name为准
            fetch_list: ["save_infer_model/scale_0.tmp_1"]  
            #计算硬件ID,当devices为""或不写时为CPU预测;当devices为"0", "0,1,2"时为GPU预测,表示使用的GPU卡
            devices: ""
            ir_optim: True

4.3、启动服务

启动服务可运行如下命令:

# 启动服务,运行日志保存在log.txt
python web_service.py &>log.txt &

4.4、解析识别服务

基于 Sanic 来构建的解析服务,基于身份证正反面识别解析服务流程如下

# api_server.py
from sanic import Sanic
from sanic.response import text
from sanic.response import json
import requests
import cv2
import base64
import time
import json as Json
import re


app = Sanic("ocr-server")


def hub_predict(img):
    data = {
    
    'images':[img]}
    headers = {
    
    "Content-type": "application/json"}
    url = "http://127.0.0.1:8866/predict/ocr_system"
    start_time = time.time()
    r = requests.post(url=url, headers=headers, data=Json.dumps(data))
    print(f'识别耗时:{
      
      time.time() - start_time}')
    # 打印预测结果
    return r.json().get('results')

def pd_predict(img):
    data = {
    
    "key": ["image"], "value": [img]}
    headers = {
    
    "Content-type": "application/json"}
    url = "http://127.0.0.1:9998/ocr/prediction"
    r = requests.post(url=url, headers=headers, data=Json.dumps(data))
    values = r.json().get('value')
    return eval(values[0])


@app.post("/hub/idcard/predict")
async def hello_world(request):
    res = {
    
    
        'msg': 'ok',
        'code': 0,
        'data': {
    
    }
    }
    if len(request.files.keys()) > 1:
        res['msg'] = '单次只能传入一个文件'
        res['code'] = 1
        return json(res)
    f = request.files.get(list(request.files.keys())[0])
    img = base64.b64encode(f.body).decode('utf8')
    predict_res = hub_predict(img) 
    return_dict = res['data']
    is_back = False
    for item in ['中华人民共和国', '居民身份证', '签发机关', '有效期限']:
        if item in [obj['text'] for obj in predict_res[0]]:
            is_back = True
            break
    return_dict['is_back'] = is_back
    if is_back:
        for obj in predict_res[0]:
            item = obj['text']
            print(item)
            if '-' in item:
                return_dict['date'] = item.replace('有效期限', '')
            elif item.replace(' ', '') in ['中华人民共和国', '居民身份证', '签发机关', '有效期限']:
                continue
            else:
                return_dict['sign'] = item.replace('签发机关', '')
        return json(res)
    for obj in predict_res[0]:
        item = obj['text']
        print(item)
        if '姓名' in item:
            return_dict['name'] = item.replace('姓名', '')
        elif len(item) == 18 and (item.isnumeric() or item[:-1].isnumeric()):
            return_dict['idNo'] = item
        elif '公民身份号码' in item:
            return_dict['idNo'] = item.replace('公民身份号码', '').replace(' ', '')
        elif '性别' in item:
            return_dict['gender'] = item.replace('性别', '')
        elif '民族' in item:
            return_dict['nation'] = item.replace('民族', '')
        elif '出生' in item:
            return_dict['birthday'] = '-'.join(re.findall( r'\d+', item, re.M|re.I))
        elif '住址' in item:
            return_dict['address'] = item.replace('住址', '')
        elif item == '公民身份号码':
            continue
        elif item.replace(' ', '') in ['中华人民共和国', '居民身份证', '签发机关', '有效期限']:
            continue
        else:
            address = return_dict.get('address', '')
            address += item
            return_dict['address'] = address
    return json(res)

@app.post("/pd/idcard/predict")
async def hello_world(request):
    res = {
    
    
        'msg': 'ok',
        'code': 0,
        'data': {
    
    }
    }
    if len(request.files.keys()) > 1:
        res['msg'] = '单次只能传入一个文件'
        res['code'] = 1
        return json(res)
    f = request.files.get(list(request.files.keys())[0])
    img = base64.b64encode(f.body).decode('utf8')
    predict_res = pd_predict(img)
    return_dict = res['data']
    is_back = False
    for item in ['中华人民共和国', '居民身份证', '签发机关', '有效期限']:
        if item in [obj for obj in predict_res]:
            is_back = True
            break
    return_dict['is_back'] = is_back
    if is_back:
        for item in predict_res:
            print(item)
            if '-' in item:
                return_dict['date'] = item.replace('有效期限', '')
            elif item.replace(' ', '') in ['中华人民共和国', '居民身份证', '签发机关', '有效期限']:
                continue
            else:
                return_dict['sign'] = item.replace('签发机关', '')
        return json(res)
    for item in predict_res:
        print(item)
        if '姓名' in item:
            return_dict['name'] = item.replace('姓名', '')
        elif len(item) == 18 and (item.isnumeric() or item[:-1].isnumeric()):
            return_dict['idNo'] = item
        elif '公民身份号码' in item:
            return_dict['idNo'] = item.replace('公民身份号码', '').replace(' ', '')
        elif '性别' in item:
            return_dict['gender'] = item.replace('性别', '')
        elif '民族' in item:
            return_dict['nation'] = item.replace('民族', '')
        elif '出生' in item:
            return_dict['birthday'] = '-'.join(re.findall( r'\d+', item, re.M|re.I))
        elif '住址' in item:
            return_dict['address'] = item.replace('住址', '')
        elif item == '公民身份号码':
            continue
        elif item.replace(' ', '') in ['中华人民共和国', '居民身份证', '签发机关', '有效期限']:
            continue
        else:
            address = return_dict.get('address', '')
            address += item
            return_dict['address'] = address
    return json(res)


if __name__ == "__main__":
    app.run(host='0.0.0.0', port='8080', debug=True)

启动服务:

python api_server.py

如何测试:

使用 postman 识别解析服务的 /pd/idcard/predict 接口,发送Post请求,在请求体中携带图片便可完成识别

示例:

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/haeasringnar/article/details/122936537