注:本文档全部在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
【推荐】git clone https://github.com/PaddlePaddle/PaddleOCR
如果因为网络问题无法 pull 成功,也可选择使用码云上的托管:
git clone https://gitee.com/paddlepaddle/PaddleOCR
根据自己的电脑开发环境,为项目创建一个虚拟环境,并且在使用项目时激活这个虚拟环境使用
对新手推荐的方法:
pip install virtualenv
cd .\PaddleOCR\
virtualenv venv
.\venv\Scripts\activate.ps1
由于飞桨内维护的 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/
官方推荐、也是本文档使用的模型:
文本检测模型
文本识别模型
方向分类器模型
在 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
这里以身份证正反面图片数据为例,提前下载好所有需要打标的数据,单个文件夹内数据较多时,推荐每500张图像为一个分组,注意名字中不能存在中文字符。目录名示例 train0-499。
打开第一大项中克隆好的项目,并确保依赖都已安装完毕,进入 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模型完成检测、识别、标注功能。
在打标工具自动表中完成后,返回到第一个文件,此时如果弹出一个确认,需要点击取消,否则会认为最后一个文件确认。
检查流程:
打标完成后,会自动生成好训练模型可以使用的标注文件 Label.txt,可以在打标目录中查看
打标 标注框 的原则:
标注的打标参考:
拿到步骤二中的标注数据,假设你的数据集目录名为: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数据集的测试标注
下载预训练模型:
文本检测预训练模型下载地址
准备预训练模型:
在 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 # 修改为你的测试标签文件
....
....
# 单机单卡训练 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 目录下
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 目录下
C:\Users\Administrator\.paddleocr\2.3.0.2\ocr
本文档实测可用的部署代码仓库:https://gitee.com/aeasringnar/pdserving.git
将预测模型导出为部署模型
# 进入项目
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_serving
和ppocrv2_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
识别模型同理。
配置文件位置:./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
启动服务可运行如下命令:
# 启动服务,运行日志保存在log.txt
python web_service.py &>log.txt &
基于 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请求,在请求体中携带图片便可完成识别
示例: