@

PaddleOCR简介

PaddleOCR分为文本检测、文本识别和方向分类器三部分,其中文本检测有三个模型,分别是MobileNetV3、ResNet18_vd和ResNet50,其中最常使用的是MobileNetV3模型,整体比较小,适合应用于手机端。文本识别只有一个MobileNetV3预训练模型。方向分类器使用默认的模型。

环境配置

PaddleOCR2.0的配置环境

PaddleOCR2.0的配置环境为:

  • PaddlePaddle 2.0.0
  • Python 3.7
  • glibc 2.23
  • cuDNN 7.6+(GPU)

其中,PaddlePaddle可以使用2.0.*

Docker

如果使用docker运行,可以直接使用 paddlepaddle/paddle:2.0.1-gpu-cuda11.0-cudnn8镜像,根据个人使用习惯,可以更换,但是paddlepaddle的版本和cudnn版本不能低于PaddleOCR2.0的配置环境。

数据集

其中,文本检测和文本识别使用的都是icdar2015数据集,方向分类器的数据集需自备

icdar2015数据集可以从官网下载,首次下载需注册。

文本检测

文本检测的icdar2015数据集的标注文件下载:

wget -P ./train_data/  https://paddleocr.bj.bcebos.com/dataset/train_icdar2015_label.txt
wget -P ./train_data/  https://paddleocr.bj.bcebos.com/dataset/test_icdar2015_label.txt

也可以使用PaddleOCR提供的数据格式转换脚本,将官网的label转换为支持的数据格式。转换数据工具为ppocr/utils/gen_label.py

# 将官网下载的标签文件转换为 train_icdar2015_label.txt
python gen_label.py --mode="det" --root_path="icdar_c4_train_imgs/"  \
                    --input_path="ch4_training_localization_transcription_gt" \
                    --output_label="train_icdar2015_label.txt"

解压数据集和下载标注文件后,放在PaddleOCR/train_data文件中,文件目录为:

/PaddleOCR/train_data/icdar2015/text_localization/
  └─ icdar_c4_train_imgs/         icdar数据集的训练数据
  └─ ch4_test_images/             icdar数据集的测试数据
  └─ train_icdar2015_label.txt    icdar数据集的训练标注
  └─ test_icdar2015_label.txt     icdar数据集的测试标注

如果不按照这个格式,需要在后面的配置文件中配置

使用自己的数据集

标注文件格式如下,中间用”\t”分隔:

" 图像文件名                    json.dumps编码的图像标注信息"
ch4_test_images/img_61.jpg    [{"transcription": "MASA", "points": [[310, 104], [416, 141], [418, 216], [312, 179]]}, {...}]

json.dumps编码前的图像标注信息是包含多个字典的list,字典中的 points 表示文本框的四个点的坐标(x, y),从左上角的点开始顺时针排列。 transcription 表示当前文本框的文字,当其内容为“###”时,表示该文本框无效,在训练时会跳过。

如果您想在其他数据集上训练,可以按照上述形式构建标注文件

文本识别

文本识别的数据集也可以参考DTRB,下载benchmark所需的lmdb格式数据集。如果希望复现SRN的论文标准,需要下载离线增广数据,提取码: y3ry。增广数据是由MJSynth和SynthText做旋转和扰动得到的。数据下载完成后请解压到 {your_path}/PaddleOCR/train_data/data_lmdb_release/training/ 路径下。

PaddleOCR提供了一份用于训练icdar2015数据集的标签文件:

# 训练集标签
wget -P ./train_data/ic15_data  https://paddleocr.bj.bcebos.com/dataset/rec_gt_train.txt
# 测试集标签
wget -P ./train_data/ic15_data  https://paddleocr.bj.bcebos.com/dataset/rec_gt_test.txt

PaddleOCR也提供了数据格式转换文本,可以将官网label转换支持的数据格式。数据转换工具在ppocr/utils/gen_label.py

# 将官网下载的标签文件转换为 rec_gt_label.txt
python gen_label.py --mode="rec" --input_path="{path/of/origin/label}" --output_label="rec_gt_label.txt"
使用自己的数据集

默认请将图片路径和图片标签用 \t 分割,如用其他方式分割将造成训练报错。

" 图像文件名                 图像标注信息 "

train_data/train_0001.jpg   简单可依赖
train_data/train_0002.jpg   用科技让复杂的世界更简单

最终训练集的文件结构为:

|-train_data
    |-ic15_data
        |- rec_gt_train.txt
        |- train
            |- word_001.png
            |- word_002.jpg
            |- word_003.jpg
            | ...

测试集的文件结构为:

|-train_data
    |-ic15_data
        |- rec_gt_test.txt
        |- test
            |- word_001.jpg
            |- word_002.jpg
            |- word_003.jpg
            | ...
字典

文本检测与文本识别相比还需要一个字典文件({word_dict_name}.txt),使用模型训练时,可以将所有出现的字符映射为字典的索引。

因此字典需要包含所有希望被正确是被的字符,{word_dict_name}.txt需要写成如下格式,并以utf-8编码格式保存:

l
d
a
d
r
n

word_dict.txt每行有一个单子,将字符与数字索引映在一起,“and”将被索引称[2 5 1]

ppocr/utils/ppocr_keys_v1.txt 是一个包含6623个字符的中文字典

ppocr/utils/ic15_dict.txt 是一个包含36个字符的英文字典

ppocr/utils/dict/french_dict.txt 是一个包含118个字符的法文字典

ppocr/utils/dict/japan_dict.txt 是一个包含4399个字符的日文字典

ppocr/utils/dict/korean_dict.txt 是一个包含3636个字符的韩文字典

ppocr/utils/dict/german_dict.txt 是一个包含131个字符的德文字典

ppocr/utils/dict/en_dict.txt 是一个包含63个字符的英文字典

您可以按需使用。

目前的多语言模型仍处在demo阶段,会持续优化模型并补充语种。

自定义字典

如需自定义dic文件,请在 configs/rec/rec_icdar15_train.yml 中添加 character_dict_path 字段, 指向您的字典路径。 并将 character_type 设置为 ch

添加空格类别

如果希望支持识别”空格”类别, 请将yml文件中的 use_space_char 字段设置为 True

文本角度分类

请按如下步骤设置数据集:

训练数据的默认存储路径是 PaddleOCR/train_data/cls,如果您的磁盘上已有数据集,只需创建软链接至数据集目录:

ln -sf <path/to/dataset> <path/to/paddle_ocr>/train_data/cls/dataset

请参考下文组织您的数据。

  • 训练集

首先请将训练图片放入同一个文件夹(train_images),并用一个txt文件(cls_gt_train.txt)记录图片路径和标签。

注意: 默认请将图片路径和图片标签用 \t 分割,如用其他方式分割将造成训练报错

0和180分别表示图片的角度为0度和180度

" 图像文件名                 图像标注信息 "
train/word_001.jpg   0
train/word_002.jpg   180

最终训练集应有如下文件结构:

|-train_data
    |-cls
        |- cls_gt_train.txt
        |- train
            |- word_001.png
            |- word_002.jpg
            |- word_003.jpg
            | ...
  • 测试集

同训练集类似,测试集也需要提供一个包含所有图片的文件夹(test)和一个cls_gt_test.txt,测试集的结构如下所示:

|-train_data
    |-cls
        |- cls_gt_test.txt
        |- test
            |- word_001.jpg
            |- word_002.jpg
            |- word_003.jpg
            | ...

文本检测

训练

模型准备

首先下载模型backbone的pretrain model,PaddleOCR的检测模型目前支持两种backbone,分别是MobileNetV3、ResNet_vd系列, 您可以根据需求使用PaddleClas中的模型更换backbone。

cd PaddleOCR/
# 下载MobileNetV3的预训练模型
wget -P ./pretrain_models/ https://paddle-imagenet-models-name.bj.bcebos.com/MobileNetV3_large_x0_5_pretrained.tar
# 或,下载ResNet18_vd的预训练模型
wget -P ./pretrain_models/ https://paddle-imagenet-models-name.bj.bcebos.com/ResNet18_vd_pretrained.tar
# 或,下载ResNet50_vd的预训练模型
wget -P ./pretrain_models/ https://paddle-imagenet-models-name.bj.bcebos.com/ResNet50_vd_ssld_pretrained.tar

# 解压预训练模型文件,以MobileNetV3为例
tar -xf ./pretrain_models/MobileNetV3_large_x0_5_pretrained.tar ./pretrain_models/

# 注:正确解压backbone预训练权重文件后,文件夹下包含众多以网络层命名的权重文件,格式如下:
./pretrain_models/MobileNetV3_large_x0_5_pretrained/
  └─ conv_last_bn_mean
  └─ conv_last_bn_offset
  └─ conv_last_bn_scale
  └─ conv_last_bn_variance
  └─ ......

启动训练

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

如果安装的是cpu版本,请将配置文件中的use_gpu字段修改为false

训练参数

  • -m 配置paddle

    • –gpus 使用的GPU
  • -c 选择配置文件

  • -o 配置配置文件中的内容

    • pretrain_weights: 使用的模型目录
    • Optimizer.base_lr: 训练时的学习率

断点训练

如果训练程序中断,如果希望加载训练中断的模型从而恢复训练,可以通过指定Global.checkpoints指定要加载的模型路径(如./output/det_r50_vd/latest):

python3 tools/train.py -c configs/det/det_mv3_db.yml -o Global.checkpoints=./output/det_mv3_vd/latest

注意Global.checkpoints的优先级高于Global.pretrain_weights的优先级,即同时指定两个参数时,优先加载Global.checkpoints指定的模型,如果Global.checkpoints指定的模型路径有误,会加载Global.pretrain_weights指定的模型。

指标评估

PaddleOCR计算三个OCR检测相关的指标,分别是:Precision、Recall、Hmean。

运行如下代码,根据配置文件det_db_mv3.ymlsave_res_path指定的测试集检测结果文件,计算评估指标。

评估时设置后处理参数box_thresh=0.5unclip_ratio=1.5,使用不同数据集、不同模型训练,可调整这两个参数进行优化 训练中模型参数默认保存在Global.save_model_dir目录下。在评估指标时,需要设置Global.pretrained_model指向保存的参数文件。

python3 tools/eval.py -c configs/det/det_mv3_db.yml  -o Global.pretrained_model="./output/det_mv3_vd/best_accuracy" PostProcess.box_thresh=0.5 PostProcess.unclip_ratio=1.5
  • 注:box_threshunclip_ratio是DB后处理所需要的参数,在评估EAST模型时不需要设置

测试检测效果

测试单张图像的检测效果

python3 tools/infer_det.py -c configs/det/det_mv3_db.yml -o Global.infer_img="./doc/imgs_en/img_10.jpg" Global.pretrained_model="./output/det_db/best_accuracy" Global.load_static_weights=false

测试DB模型时,调整后处理阈值,

python3 tools/infer_det.py -c configs/det/det_mv3_db.yml -o Global.infer_img="./doc/imgs_en/img_10.jpg" Global.pretrained_model="./output/det_db/best_accuracy" Global.load_static_weights=false PostProcess.box_thresh=0.6 PostProcess.unclip_ratio=1.5

测试文件夹下所有图像的检测效果

python3 tools/infer_det.py -c configs/det/det_mv3_db.yml -o Global.infer_img="./doc/imgs_en/" Global.pretrained_model="./output/det_db/best_accuracy" Global.load_static_weights=false

文本识别

启动训练

PaddleOCR提供了训练脚本、评估脚本和预测脚本,本节将以 CRNN 识别模型为例:

首先下载pretrain model,您可以下载训练好的模型在 icdar2015 数据上进行finetune

cd PaddleOCR/
# 下载MobileNetV3的预训练模型
wget -P ./pretrain_models/ https://paddleocr.bj.bcebos.com/dygraph_v2.0/en/rec_mv3_none_bilstm_ctc_v2.0_train.tar
# 解压模型参数
cd pretrain_models
tar -xf rec_mv3_none_bilstm_ctc_v2.0_train.tar && rm -rf rec_mv3_none_bilstm_ctc_v2.0_train.tar

开始训练:

如果您安装的是cpu版本,请将配置文件中的 use_gpu 字段修改为false

# GPU训练 支持单卡,多卡训练,通过--gpus参数指定卡号
# 训练icdar15英文数据 训练日志会自动保存为 "{save_model_dir}" 下的train.log
python3 -m paddle.distributed.launch --gpus \'0,1,2,3\'  tools/train.py -c configs/rec/rec_icdar15_train.yml
  • 数据增强

PaddleOCR提供了多种数据增强方式,如果您希望在训练时加入扰动,请在配置文件中设置 distort: true

默认的扰动方式有:颜色空间转换(cvtColor)、模糊(blur)、抖动(jitter)、噪声(Gasuss noise)、随机切割(random crop)、透视(perspective)、颜色反转(reverse)。

训练过程中每种扰动方式以50%的概率被选择,具体代码实现请参考:img_tools.py

由于OpenCV的兼容性问题,扰动操作暂时只支持Linux

  • 训练

PaddleOCR支持训练和评估交替进行, 可以在 configs/rec/rec_icdar15_train.yml 中修改 eval_batch_step 设置评估频率,默认每500个iter评估一次。评估过程中默认将最佳acc模型,保存为 output/rec_CRNN/best_accuracy

如果验证集很大,测试将会比较耗时,建议减少评估次数,或训练完再进行评估。

其他模型配置请参考PaddleOCR github

评估

评估数据集可以通过 configs/rec/rec_icdar15_train.yml 修改Eval中的 label_file_path 设置。

# GPU 评估, Global.pretrained_model 为待测权重
python3 -m paddle.distributed.launch --gpus \'0\' tools/eval.py -c configs/rec/rec_icdar15_train.yml -o Global.pretrained_model={path/to/weights}/best_accuracy

预测

  • 训练引擎的预测

使用 PaddleOCR 训练好的模型,可以通过以下脚本进行快速预测。

默认预测图片存储在 infer_img 里,通过 -o Global.pretrained_model 指定权重:

# 预测英文结果
python3 tools/infer_rec.py -c configs/rec/rec_icdar15_train.yml -o Global.pretrained_model={path/to/weights}/best_accuracy Global.load_static_weights=false Global.infer_img=doc/imgs_words/en/word_1.png

预测使用的配置文件必须与训练一致,如您通过 python3 tools/train.py -c configs/rec/ch_ppocr_v2.0/rec_chinese_lite_train_v2.0.yml 完成了中文模型的训练, 您可以使用如下命令进行中文模型预测。

# 预测中文结果
python3 tools/infer_rec.py -c configs/rec/ch_ppocr_v2.0/rec_chinese_lite_train_v2.0.yml -o Global.pretrained_model={path/to/weights}/best_accuracy Global.load_static_weights=false Global.infer_img=doc/imgs_words/ch/word_1.jpg

文本角度分类

训练

启动训练

将准备好的txt文件和图片文件夹路径分别写入配置文件的 Train/Eval.dataset.label_file_listTrain/Eval.dataset.data_dir 字段下,Train/Eval.dataset.data_dir字段下的路径和文件里记载的图片名构成了图片的绝对路径。

PaddleOCR提供了训练脚本、评估脚本和预测脚本。

开始训练:

如果您安装的是cpu版本,请将配置文件中的 use_gpu 字段修改为false

# GPU训练 支持单卡,多卡训练,通过 \'--gpus\' 指定卡号。
# 启动训练,下面的命令已经写入train.sh文件中,只需修改文件里的配置文件路径即可
python3 -m paddle.distributed.launch --gpus \'0,1,2,3,4,5,6,7\'  tools/train.py -c configs/cls/cls_mv3.yml

数据增强

PaddleOCR提供了多种数据增强方式,如果您希望在训练时加入扰动,请在配置文件中取消Train.dataset.transforms下的RecAugRandAugment字段的注释。

默认的扰动方式有:颜色空间转换(cvtColor)、模糊(blur)、抖动(jitter)、噪声(Gasuss noise)、随机切割(random crop)、透视(perspective)、颜色反转(reverse),随机数据增强(RandAugment)。

训练过程中除随机数据增强外每种扰动方式以50%的概率被选择,具体代码实现请参考: rec_img_aug.py randaugment.py

由于OpenCV的兼容性问题,扰动操作暂时只支持linux

训练

PaddleOCR支持训练和评估交替进行, 可以在 configs/cls/cls_mv3.yml 中修改 eval_batch_step 设置评估频率,默认每1000个iter评估一次。训练过程中将会保存如下内容:

├── best_accuracy.pdopt # 最佳模型的优化器参数
├── best_accuracy.pdparams # 最佳模型的参数
├── best_accuracy.states # 最佳模型的指标和epoch等信息
├── config.yml # 本次实验的配置文件
├── latest.pdopt # 最新模型的优化器参数
├── latest.pdparams # 最新模型的参数
├── latest.states # 最新模型的指标和epoch等信息
└── train.log # 训练日志

如果验证集很大,测试将会比较耗时,建议减少评估次数,或训练完再进行评估。

注意,预测/评估时的配置文件请务必与训练一致

评估

评估数据集可以通过修改configs/cls/cls_mv3.yml文件里的Eval.dataset.label_file_list 字段设置。

export CUDA_VISIBLE_DEVICES=0
# GPU 评估, Global.checkpoints 为待测权重
python3 tools/eval.py -c configs/cls/cls_mv3.yml -o Global.checkpoints={path/to/weights}/best_accuracy

预测

  • 训练引擎的预测

使用 PaddleOCR 训练好的模型,可以通过以下脚本进行快速预测。

通过 Global.infer_img 指定预测图片或文件夹路径,通过 Global.checkpoints 指定权重:

# 预测分类结果
python3 tools/infer_cls.py -c configs/cls/cls_mv3.yml -o Global.pretrained_model={path/to/weights}/best_accuracy Global.load_static_weights=false Global.infer_img=doc/imgs_words/ch/word_1.jpg

配置文件说明

Global

字段 用途 默认值 备注
use_gpu 设置代码是否在gpu运行 true \
epoch_num 最大训练epoch数 500 \
log_smooth_window log队列长度,每次打印输出队列里的中间值 20 \
print_batch_step 设置打印log间隔 10 \
save_model_dir 设置模型保存路径 output/{算法名称} \
save_epoch_step 设置模型保存间隔 3 \
eval_batch_step 设置模型评估间隔 2000 或 [1000, 2000] 2000 表示每2000次迭代评估一次,[1000, 2000]表示从1000次迭代开始,每2000次评估一次
cal_metric_during_train 设置是否在训练过程中评估指标,此时评估的是模型在当前batch下的指标 true \
load_static_weights 设置预训练模型是否是静态图模式保存(目前仅检测算法需要) true \
pretrained_model 设置加载预训练模型路径 ./pretrain_models/CRNN/best_accuracy \
checkpoints 加载模型参数路径 None 用于中断后加载参数继续训练
use_visualdl 设置是否启用visualdl进行可视化log展示 False 教程地址
infer_img 设置预测图像路径或文件夹路径 ./infer_img |
character_dict_path 设置字典路径 ./ppocr/utils/ppocr_keys_v1.txt \
max_text_length 设置文本最大长度 25 \
character_type 设置字符类型 ch en/ch, en时将使用默认dict,ch时使用自定义dict
use_space_char 设置是否识别空格 True 仅在 character_type=ch 时支持空格
label_list 设置方向分类器支持的角度 [\’0\’,\’180\’] 仅在方向分类器中生效
save_res_path 设置检测模型的结果保存地址 ./output/det_db/predicts_db.txt 仅在检测模型中生效

Optimizer (ppocr/optimizer)

字段 用途 默认值 备注
name 优化器类名 Adam 目前支持Momentum,Adam,RMSProp, 见ppocr/optimizer/optimizer.py
beta1 设置一阶矩估计的指数衰减率 0.9 \
beta2 设置二阶矩估计的指数衰减率 0.999 \
clip_norm 所允许的二范数最大值 \
lr 设置学习率decay方式 \
name 学习率decay类名 Cosine 目前支持Linear,Cosine,Step,Piecewise, 见ppocr/optimizer/learning_rate.py
learning_rate 基础学习率 0.001 \
regularizer 设置网络正则化方式 \
name 正则化类名 L2 目前支持L1,L2, 见ppocr/optimizer/regularizer.py
factor 学习率衰减系数 0.00004 \

Architecture (ppocr/modeling)

在ppocr中,网络被划分为Transform,Backbone,Neck和Head四个阶段

字段 用途 默认值 备注
model_type 网络类型 rec 目前支持rec,det,cls
algorithm 模型名称 CRNN 支持列表见algorithm_overview
Transform 设置变换方式 目前仅rec类型的算法支持, 具体见ppocr/modeling/transform
name 变换方式类名 TPS 目前支持TPS
num_fiducial TPS控制点数 20 上下边各十个
loc_lr 定位网络学习率 0.1 \
model_name 定位网络大小 small 目前支持small,large
Backbone 设置网络backbone类名 具体见ppocr/modeling/backbones
name backbone类名 ResNet 目前支持MobileNetV3,ResNet
layers resnet层数 34 支持18,34,50,101,152,200
model_name MobileNetV3 网络大小 small 支持small,large
Neck 设置网络neck 具体见ppocr/modeling/necks
name neck类名 SequenceEncoder 目前支持SequenceEncoder,DBFPN
encoder_type SequenceEncoder编码器类型 rnn 支持reshape,fc,rnn
hidden_size rnn内部单元数 48 \
out_channels DBFPN输出通道数 256 \
Head 设置网络Head 具体见ppocr/modeling/heads
name head类名 CTCHead 目前支持CTCHead,DBHead,ClsHead
fc_decay CTCHead正则化系数 0.0004 \
k DBHead二值化系数 50 \
class_dim ClsHead输出分类数 2 \

Loss (ppocr/losses)

字段 用途 默认值 备注
name 网络loss类名 CTCLoss 目前支持CTCLoss,DBLoss,ClsLoss
balance_loss DBLossloss中是否对正负样本数量进行均衡(使用OHEM) True \
ohem_ratio DBLossloss中的OHEM的负正样本比例 3 \
main_loss_type DBLossloss中shrink_map所采用的的loss DiceLoss 支持DiceLoss,BCELoss
alpha DBLossloss中shrink_map_loss的系数 5 \
beta DBLossloss中threshold_map_loss的系数 10 \

PostProcess (ppocr/postprocess)

字段 用途 默认值 备注
name 后处理类名 CTCLabelDecode 目前支持CTCLoss,AttnLabelDecode,DBPostProcess,ClsPostProcess
thresh DBPostProcess中分割图进行二值化的阈值 0.3 \
box_thresh DBPostProcess中对输出框进行过滤的阈值,低于此阈值的框不会输出 0.7 \
max_candidates DBPostProcess中输出的最大文本框数量 1000
unclip_ratio DBPostProcess中对文本框进行放大的比例 2.0 \

Metric (ppocr/metrics)

字段 用途 默认值 备注
name 指标评估方法名称 CTCLabelDecode 目前支持DetMetric,RecMetric,ClsMetric
main_indicator 主要指标,用于选取最优模型 acc 对于检测方法为hmean,识别和分类方法为acc

Dataset (ppocr/data)

字段 用途 默认值 备注
dataset 每次迭代返回一个样本
name dataset类名 SimpleDataSet 目前支持SimpleDataSetLMDBDateSet
data_dir 数据集图片存放路径 ./train_data \
label_file_list 数据标签路径 [“./train_data/train_list.txt”] dataset为LMDBDateSet时不需要此参数
ratio_list 数据集的比例 [1.0] 若label_file_list中有两个train_list,且ratio_list为[0.4,0.6],则从train_list1中采样40%,从train_list2中采样60%组合整个dataset
transforms 对图片和标签进行变换的方法列表 [DecodeImage,CTCLabelEncode,RecResizeImg,KeepKeys] ppocr/data/imaug
loader dataloader相关
shuffle 每个epoch是否将数据集顺序打乱 True \
batch_size_per_card 训练时单卡batch size 256 \
drop_last 是否丢弃因数据集样本数不能被 batch_size 整除而产生的最后一个不完整的mini-batch True \
num_workers 用于加载数据的子进程个数,若为0即为不开启子进程,在主进程中进行数据加载 8

错误解决方案

当文本识别使用ResNet50时,会出现DataLoader reader thread raised an exception!

将配置文件的train loader batch_size_per_card改为8即可

本人博客 https://xiaoxiablogs.top

版权声明:本文为xiaoxia722原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/xiaoxia722/p/14627482.html