基于PP-OCRv3的电表检测识别

本案例将使用OCR技术自动识别电表读数与电表编号,攻克表计图片“识别不到”与“识别不准”的难题,通过本章您可以掌握:

  • PaddleOCR快速使用
  • 文本检测优化方法
  • 文本识别优化方法
  • 数据合成方法
  • 基于现有数据微调

1 项目背景

我国电力行业发展迅速,电表作为测电设备经历了普通电表、预付费电表和智能电表三个阶段的发展,虽然智能电表具有通信功能,但一方面环境和设备使得智能电表具有不稳定性,另一方面非智能电表仍然无法实现自动采集,人工抄表有时往往不可取代。采集到的大量电表图片如果能够借助人工智能技术批量检测和识别,将会大幅提升效率和精度。

在本系列项目中,我们使用Paddle工具库实现一个OCR垂类场景。原始数据集是一系列电度表的照片,类型较多,需要完成电表的读数识别,对于有编号的电表,还要完成其编号的识别。关于电表检测任务的难点以及基线解决方案,请参考项目PPOCR:多类别电表读数识别的详细说明。

2022年5月,PaddleOCR更新到了release/2.5,并发布了PP-OCRv3,速度可比情况下,中文场景效果相比于PP-OCRv2再提升5%,英文场景提升11%,80语种多语言模型平均识别准确率提升5%以上。

如此重要的升级,电表检测识别模型怎能错过?

因此,我们就在基于PP-OCRv2开发的项目PPOCR:多类别电表读数识别基础上,来测试下升级到PP-OCRv3之后的电表检测模型效果吧!

参考资料——电表检测识别全系列:

(主线篇)

(众人拾柴——应用篇)

  • AI达人特训营】PPOCR:电表检测模型的Windows端部署实现
  • AI达人特训营】电表检测WEB部署方案
  • AI达人特训营】多类别电表读数识别的Windows客户端Web端部署

(番外篇)

  • PPOCR+PPDET电表读数和编号检测

2 安装说明

项目开发环境基于PaddleOCR release/2.5。

# 拉取模型库
!git clone https://gitee.com/paddlepaddle/PaddleOCR.git
# 安装依赖
!pip install -r PaddleOCR/requirements.txt
# text_renderer在项目中已经准备好
# !git clone https://gitee.com/wowowoll/text_renderer.git
!pip install -r text_renderer/requirements.txt

3 数据集简介

本项目示例数据集采用通过PPOCRLabel标注得到263张脱敏电表OCR图片,标注有电表读数和编号。在实际工业场景中,可以通过摄像头采集的方法收集大量真实数据,但是对这些数据逐一进行标注耗时甚长。因此,本例项目中,重点介绍如何利用合成数据,快速实现数据集扩充的方法。

该方法基于TextRender,由于需要与最初训练效果指标进行对比,本文只对电表编号数据进行了扩增。电表读数的扩展方法大同小异,读者可准备好相关字体文件,参考项目电表读数识别:数据集补充解决方案对比(TextRender和StyleText)自行进行电表读数补充数据的生成。

# 解压电表数据集
!unzip data/data117381/M2021.zip
# 准备电表编号补充目录
!mkdir M2021_crop
import os 
import json
import cv2
import numpy as np
import math
from PIL import Image, ImageDraw

class Rotate(object):

    def __init__(self, image: Image.Image, coordinate):
        self.image = image.convert('RGB')
        self.coordinate = coordinate
        self.xy = [tuple(self.coordinate[k]) for k in ['left_top', 'right_top', 'right_bottom', 'left_bottom']]
        self._mask = None
        self.image.putalpha(self.mask)

    @property
    def mask(self):
        if not self._mask:
            mask = Image.new('L', self.image.size, 0)
            draw = ImageDraw.Draw(mask, 'L')
            draw.polygon(self.xy, fill=255)
            self._mask = mask
        return self._mask

    def run(self):
        image = self.rotation_angle()
        box = image.getbbox()
        return image.crop(box)

    def rotation_angle(self):
        x1, y1 = self.xy[0]
        x2, y2 = self.xy[1]
        angle = self.angle([x1, y1, x2, y2], [0, 0, 10, 0]) * -1
        return self.image.rotate(angle, expand=True)

    def angle(self, v1, v2):
        dx1 = v1[2] - v1[0]
        dy1 = v1[3] - v1[1]
        dx2 = v2[2] - v2[0]
        dy2 = v2[3] - v2[1]
        angle1 = math.atan2(dy1, dx1)
        angle1 = int(angle1 * 180 / math.pi)
        angle2 = math.atan2(dy2, dx2)
        angle2 = int(angle2 * 180 / math.pi)
        if angle1 * angle2 >= 0:
            included_angle = abs(angle1 - angle2)
        else:
            included_angle = abs(angle1) + abs(angle2)
            if included_angle > 180:
                included_angle = 360 - included_angle
        return included_angle

with open('./M2021/Label.txt','r',encoding='utf8')as fp:
    s = [i[:-1].split('\t') for i in fp.readlines()]
    f1 = open('M2021_crop/rec_gt_train.txt', 'w', encoding='utf-8')
    f2 = open('M2021_crop/rec_gt_eval.txt', 'w', encoding='utf-8')
    for i in enumerate(s):
        path = i[1][0]
        anno = json.loads(i[1][1])
        filename = i[1][0][6:-4]
        image = Image.open(path)
        for j in range(len(anno)): 
            label = anno[j]['transcription']
            roi = anno[j]['points']
            coordinate = {'left_top': anno[j]['points'][0], 'right_top': anno[j]['points'][1], 'right_bottom': anno[j]['points'][2], 'left_bottom': anno[j]['points'][3]}
            print(roi, label)
            rotate = Rotate(image, coordinate)
            # 把图片放到目录下
            crop_path = 'M2021_crop' + path[5:-4:] + '_' + str(j) + '.jpg'
            rotate.run().convert('RGB').save(crop_path)
            # label文件不写入图片目录
            crop_path = path[6:-4:] + '_' + str(j) + '.jpg'
            if i[0] % 5 != 0:
                f1.writelines(crop_path + '\t' + label + '\n')
            else:
                f2.writelines(crop_path + '\t' + label + '\n')
    f1.close()
    f2.close()
with open('./M2021/Label.txt','r',encoding='utf8')as fp:
    s = [i[:-1].split('\t') for i in fp.readlines()]
    f3 = open('text_renderer/data/list_corpus/books.txt', 'w', encoding='utf-8')
    for i in enumerate(s):
        anno = json.loads(i[1][1])
        for j in range(len(anno)): 
            label = anno[j-1]['transcription']
            if len(label) > 8:
                print(label)
                f3.writelines(label + '\n')
    f3.close()
%cd text_renderer/
/home/aistudio/text_renderer
# 作为示例,这里只生成5000张图片
!python main.py --num_img=5000 --chars_file='./data/chars/eng.txt' \
--fonts_list='./data/fonts_list/eng.txt' --corpus_dir "data/list_corpus" \
--corpus_mode "list"
Total fonts num: 5
Background num: 1
Loading corpus from: data/list_corpus
Load corpus: data/list_corpus/books.txt
Total lines: 229
Generate text images in ./output/default
5000/5000 100%
Finish generate data: 59.740 s
# 生成PaddleOCR文字识别数据集格式文件
with open('./output/default/tmp_labels.txt','r') as f:
    s = [i[:-1].split(' ') for i in f.readlines()]
    f4 = open('./render_data.txt', 'w', encoding='utf-8')
    for i in enumerate(s):
        path = i[1][0] + '.jpg'
        f4.writelines(path + '\t' + i[1][1] + '\n')
    f4.close()
# 将生成的标签文件和图片与裁剪的电表编号数据集合并
!mv ./render_data.txt ../M2021_crop/
!mv output/default/*.jpg ../M2021_crop/

4 模型介绍

PP-OCRv3在PP-OCRv2的基础上进一步升级。整体的框架图保持了与PP-OCRv2相同的pipeline,针对检测模型和识别模型进行了优化。其中,检测模块仍基于DB算法优化,而识别模块不再采用CRNN,换成了IJCAI 2022最新收录的文本识别算法SVTR,并对其进行产业适配。PP-OCRv3系统框图如下所示(粉色框中为PP-OCRv3新增策略):

在这里插入图片描述

从算法改进思路上看,分别针对检测和识别模型,进行了共9个方面的改进:

  • 检测模块:

    • LK-PAN:大感受野的PAN结构;
    • DML:教师模型互学习策略;
    • RSE-FPN:残差注意力机制的FPN结构;
  • 识别模块:

    • SVTR_LCNet:轻量级文本识别网络;
    • GTC:Attention指导CTC训练策略;
    • TextConAug:挖掘文字上下文信息的数据增广策略;
    • TextRotNet:自监督的预训练模型;
    • UDML:联合互学习策略;
    • UIM:无标注数据挖掘方案。
%cd ../PaddleOCR/
/home/aistudio/PaddleOCR

4.1 检测模型优化

PP-OCRv3检测模型是对PP-OCRv2中的CML(Collaborative Mutual Learning) 协同互学习文本检测蒸馏策略进行了升级。CML的核心思想结合了①传统的Teacher指导Student的标准蒸馏与 ②Students网络之间的DML互学习,可以让Students网络互学习的同时,Teacher网络予以指导。

在这里插入图片描述

PP-OCRv3分别针对教师模型和学生模型进行进一步效果优化。其中,在对教师模型优化时,提出了大感受野的PAN结构LK-PAN和引入了DML(Deep Mutual Learning)蒸馏策略;在对学生模型优化时,提出了残差注意力机制的FPN结构RSE-FPN。

(1)LK-PAN:大感受野的PAN结构

LK-PAN (Large Kernel PAN) 是一个具有更大感受野的轻量级PAN结构,核心是将PAN结构的path augmentation中卷积核从3*3改为9*9。通过增大卷积核,提升特征图每个位置覆盖的感受野,更容易检测大字体的文字以及极端长宽比的文字。使用LK-PAN结构,可以将教师模型的hmean从83.2%提升到85.0%。

在这里插入图片描述

(2)DML:教师模型互学习策略

DML (Deep Mutual Learning)互学习蒸馏方法,如下图所示,通过两个结构相同的模型互相学习,可以有效提升文本检测模型的精度。教师模型采用DML策略,hmean从85%提升到86%。将PP-OCRv2中CML的教师模型更新为上述更高精度的教师模型,学生模型的hmean可以进一步从83.2%提升到84.3%。

在这里插入图片描述

(3)RSE-FPN:残差注意力机制的FPN结构

RSE-FPN(Residual Squeeze-and-Excitation FPN)如下图所示,引入残差结构和通道注意力结构,将FPN中的卷积层更换为通道注意力结构的RSEConv层,进一步提升特征图的表征能力。考虑到PP-OCRv2的检测模型中FPN通道数非常小,仅为96,如果直接用SEblock代替FPN中卷积会导致某些通道的特征被抑制,精度会下降。RSEConv引入残差结构会缓解上述问题,提升文本检测效果。进一步将PP-OCRv2中CML的学生模型的FPN结构更新为RSE-FPN,学生模型的hmean可以进一步从84.3%提升到85.4%。

在这里插入图片描述

4.2 识别模型优化

PP-OCRv3的识别模块是基于文本识别算法SVTR优化。SVTR不再采用RNN结构,通过引入Transformers结构更加有效地挖掘文本行图像的上下文信息,从而提升文本识别能力。直接将PP-OCRv2的识别模型,替换成SVTR_Tiny,识别准确率从74.8%提升到80.1%(+5.3%),但是预测速度慢了将近11倍,CPU上预测一条文本行,将近100ms。因此,如下图所示,PP-OCRv3采用如下6个优化策略进行识别模型加速。

在这里插入图片描述

(1)SVTR_LCNet:轻量级文本识别网络

SVTR_LCNet是针对文本识别任务,将基于Transformer的SVTR网络和轻量级CNN网络PP-LCNet 融合的一种轻量级文本识别网络。使用该网络,预测速度优于PP-OCRv2的识别模型20%,但是由于没有采用蒸馏策略,该识别模型效果略差。此外,进一步将输入图片规范化高度从32提升到48,预测速度稍微变慢,但是模型效果大幅提升,识别准确率达到73.98%(+2.08%),接近PP-OCRv2采用蒸馏策略的识别模型效果。

在这里插入图片描述

(2)GTC:Attention指导CTC训练策略

GTC(Guided Training of CTC),利用Attention模块CTC训练,融合多种文本特征的表达,是一种有效的提升文本识别的策略。使用该策略,预测时完全去除 Attention 模块,在推理阶段不增加任何耗时,识别模型的准确率进一步提升到75.8%(+1.82%)。训练流程如下所示:

在这里插入图片描述

(3)TextConAug:挖掘文字上下文信息的数据增广策略

TextConAug是一种挖掘文字上下文信息的数据增广策略,主要思想来源于论文ConCLR,作者提出ConAug数据增广,在一个batch内对2张不同的图像进行联结,组成新的图像并进行自监督对比学习。PP-OCRv3将此方法应用到有监督的学习任务中,设计了TextConAug数据增强方法,可以丰富训练数据上下文信息,提升训练数据多样性。使用该策略,识别模型的准确率进一步提升到76.3%(+0.5%)。TextConAug示意图如下所示:

在这里插入图片描述

(4)TextRotNet:自监督的预训练模型

TextRotNet是使用大量无标注的文本行数据,通过自监督方式训练的预训练模型,参考于论文STR-Fewer-Labels。该模型可以初始化SVTR_LCNet的初始权重,从而帮助文本识别模型收敛到更佳位置。使用该策略,识别模型的准确率进一步提升到76.9%(+0.6%)。TextRotNet训练流程如下图所示:

在这里插入图片描述

(5)UDML:联合互学习策略

UDML(Unified-Deep Mutual Learning)联合互学习是PP-OCRv2中就采用的对于文本识别非常有效的提升模型效果的策略。在PP-OCRv3中,针对两个不同的SVTR_LCNet和Attention结构,对他们之间的PP-LCNet的特征图、SVTR模块的输出和Attention模块的输出同时进行监督训练。使用该策略,识别模型的准确率进一步提升到78.4%(+1.5%)。

在这里插入图片描述

(6)UIM:无标注数据挖掘方案

UIM(Unlabeled Images Mining)是一种非常简单的无标注数据挖掘方案。核心思想是利用高精度的文本识别大模型对无标注数据进行预测,获取伪标签,并且选择预测置信度高的样本作为训练数据,用于训练小模型。使用该策略,识别模型的准确率进一步提升到79.4%(+1%)。

在这里插入图片描述

5 模型训练

5.1 检测模型训练

由于电表读数、电表编号所在区域,相比于整个电表,面积占比并不高。因此,在检测模型训练时,一个核心的优化点是要调大输入检测模型的图片size。

检测模块训练配置文件关键设置如下:

Train:
  dataset:
    name: SimpleDataSet
    data_dir: ../
    label_file_list:
      - ../train_list.txt
    ……
    - EastRandomCropData:
        size:
        - 1600
        - 1600
        max_tries: 50
        keep_ratio: true
    ……
  loader:
    shuffle: true
    drop_last: false
    batch_size_per_card: 6
    use_shared_memory: False
    num_workers: 8
Eval:
  dataset:
    name: SimpleDataSet
    data_dir: ../
    label_file_list:
      - ../val_list.txt
    ……
  loader:
    shuffle: false
    drop_last: false
    batch_size_per_card: 1
    use_shared_memory: False
    num_workers: 2

注意:在AIStudio训练,一定要注意几个重点!

  • 用至尊版!因为原图分辨率太大,目标框相对其实很小,所以输入模型的size太小训练效果不好,而size设大自然需要更多显存
  • use_shared_memory设置为False
  • batch_size_per_card不能设置太大,因为输入size比较大
# 下载预训练模型
!wget https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_det_distill_train.tar
!tar -xvf ch_PP-OCRv3_det_distill_train.tar
# 训练检测模型
!python tools/train.py -c ../dianbiao/ch_PP-OCRv3_det_student.yml

最终,在PP-OCRv3下,电表编号和读数检测模型可以达到的参考效果如下:

2022/05/08 08:50:36] ppocr INFO: epoch: [400/400], global_step: 3600, lr: 0.000000, loss: 1.110980, loss_shrink_maps: 0.546560, loss_threshold_maps: 0.420974, loss_binary_maps: 0.109129, avg_reader_cost: 2.31141 s, avg_batch_cost: 2.73862 s, avg_samples: 5.3, ips: 1.93528 samples/s, eta: 0:00:00
[2022/05/08 08:51:09] ppocr INFO: cur metric, precision: 0.9157894736842105, recall: 0.925531914893617, hmean: 0.9206349206349206, fps: 4.671872943533558
[2022/05/08 08:51:09] ppocr INFO: save best model is to ./output/det_ppocrv3/best_accuracy
[2022/05/08 08:51:09] ppocr INFO: best metric, hmean: 0.9206349206349206, precision: 0.9157894736842105, recall: 0.925531914893617, fps: 4.671872943533558, best_epoch: 400
[2022/05/08 08:51:10] ppocr INFO: save model in ./output/det_ppocrv3/latest
[2022/05/08 08:51:10] ppocr INFO: save model in ./output/det_ppocrv3/iter_epoch_400
[2022/05/08 08:51:10] ppocr INFO: best metric, hmean: 0.9206349206349206, precision: 0.9157894736842105, recall: 0.925531914893617, fps: 4.671872943533558, best_epoch: 400

5.2 识别模型训练

识别模块训练配置文件关键设置如下:

Train:
  dataset:
    name: SimpleDataSet
    data_dir: ../M2021_crop/
    ext_op_transform_idx: 1
    label_file_list:
    - ../M2021_crop/rec_gt_train.txt
    - ../M2021_crop/render_data.txt
    ratio_list: [1.0, 1.0]
    ……
  loader:
    shuffle: true
    batch_size_per_card: 128
    drop_last: true
    use_shared_memory: False
    num_workers: 8
Eval:
  dataset:
    name: SimpleDataSet
    data_dir: ../M2021_crop/
    label_file_list:
    - ../M2021_crop/rec_gt_eval.txt
    ……
  loader:
    shuffle: false
    drop_last: false
    batch_size_per_card: 128
    use_shared_memory: False
    num_workers: 8
!wget https://paddleocr.bj.bcebos.com/PP-OCRv3/english/en_PP-OCRv3_rec_train.tar
!tar -xvf en_PP-OCRv3_rec_train.tar
!python tools/train.py -c ../dianbiao/rec_en_ppocrv3.yml

识别模型参考训练效果如下:

[2022/05/08 12:37:19] ppocr INFO: epoch: [199/200], global_step: 4160, lr: 0.000001, CTCLoss: 0.046551, SARLoss: 0.233911, loss: 0.279258, avg_reader_cost: 0.42351 s, avg_batch_cost: 0.46051 s, avg_samples: 25.6, ips: 55.59113 samples/s, eta: 0:00:28
[2022/05/08 12:37:22] ppocr INFO: epoch: [199/200], global_step: 4170, lr: 0.000001, CTCLoss: 0.051888, SARLoss: 0.240020, loss: 0.290269, avg_reader_cost: 0.00250 s, avg_batch_cost: 0.18425 s, avg_samples: 128.0, ips: 694.69022 samples/s, eta: 0:00:21
[2022/05/08 12:37:24] ppocr INFO: epoch: [199/200], global_step: 4179, lr: 0.000001, CTCLoss: 0.042004, SARLoss: 0.233527, loss: 0.281940, avg_reader_cost: 0.00015 s, avg_batch_cost: 0.16175 s, avg_samples: 115.2, ips: 712.19180 samples/s, eta: 0:00:15
[2022/05/08 12:37:25] ppocr INFO: save model in ./output/rec_en_ppocrv3/latest
[2022/05/08 12:37:29] ppocr INFO: epoch: [200/200], global_step: 4180, lr: 0.000001, CTCLoss: 0.047450, SARLoss: 0.232198, loss: 0.283179, avg_reader_cost: 0.41280 s, avg_batch_cost: 0.43282 s, avg_samples: 12.8, ips: 29.57376 samples/s, eta: 0:00:14
[2022/05/08 12:37:32] ppocr INFO: epoch: [200/200], global_step: 4190, lr: 0.000000, CTCLoss: 0.054212, SARLoss: 0.227913, loss: 0.292011, avg_reader_cost: 0.06983 s, avg_batch_cost: 0.25463 s, avg_samples: 128.0, ips: 502.69504 samples/s, eta: 0:00:07
[2022/05/08 12:37:36] ppocr INFO: epoch: [200/200], global_step: 4200, lr: 0.000000, CTCLoss: 0.054718, SARLoss: 0.230516, loss: 0.291420, avg_reader_cost: 0.04034 s, avg_batch_cost: 0.22075 s, avg_samples: 128.0, ips: 579.85354 samples/s, eta: 0:00:00
[2022/05/08 12:37:36] ppocr INFO: save model in ./output/rec_en_ppocrv3/latest
[2022/05/08 12:37:36] ppocr INFO: best metric, acc: 0.8645832432725788, norm_edit_dis: 0.9656151899000623, fps: 1557.634472327207, best_epoch: 72

6 模型评估

通过上述代码训练好模型后,运行 tools/eval.py, 指定配置文件和模型参数即可评估效果。

实验结果如下表所示:

检测模型实验指标
PP-OCRv2仅调整lrH-Means=0.3
PP-OCRv2调整lr+增大输入分辨率H-Means=0.65
PP-OCRv2调整lr+增大输入分辨率+CopyPasteH-Means=0.85
PP-OCRv3调整lr+增大输入分辨率+CopyPasteH-Means=0.97
识别模型实验指标
PP-OCRv2电表编号TextRender扩增+5000张Acc=0.81
PP-OCRv3电表编号TextRender扩增+5000张Acc=0.86
!python tools/eval.py -c ../dianbiao/ch_PP-OCRv3_det_student.yml
[2022/07/04 01:58:59] ppocr INFO: train with paddle 2.1.2 and device CUDAPlace(0)
[2022/07/04 01:58:59] ppocr INFO: Initialize indexs of datasets:['../val_list.txt']
W0704 01:58:59.245405 27927 device_context.cc:404] Please NOTE: device: 0, GPU Compute Capability: 7.0, Driver API Version: 11.2, Runtime API Version: 10.1
W0704 01:58:59.249910 27927 device_context.cc:422] device: 0, cuDNN Version: 7.6.
[2022/07/04 01:59:02] ppocr INFO: load pretrain successful from ../dianbiao/train_log_model/det_ppocrv3_1600/best_accuracy
eval model:: 100%|██████████████████████████████| 50/50 [00:14<00:00,  3.38it/s]
[2022/07/04 01:59:17] ppocr INFO: metric eval ***************
[2022/07/04 01:59:17] ppocr INFO: precision:0.9805825242718447
[2022/07/04 01:59:17] ppocr INFO: recall:0.9711538461538461
[2022/07/04 01:59:17] ppocr INFO: hmean:0.9758454106280192
[2022/07/04 01:59:17] ppocr INFO: fps:7.078776890862657
!python tools/eval.py -c ../dianbiao/rec_en_ppocrv3.yml
[2022/07/04 01:55:47] ppocr INFO: train with paddle 2.1.2 and device CUDAPlace(0)
[2022/07/04 01:55:47] ppocr INFO: Initialize indexs of datasets:['../M2021_crop/rec_gt_eval.txt']
W0704 01:55:47.620630 27339 device_context.cc:404] Please NOTE: device: 0, GPU Compute Capability: 7.0, Driver API Version: 11.2, Runtime API Version: 10.1
W0704 01:55:47.625003 27339 device_context.cc:422] device: 0, cuDNN Version: 7.6.
[2022/07/04 01:55:50] ppocr INFO: load pretrain successful from ../dianbiao/rec_en_ppocrv3/best_accuracy
eval model:: 100%|████████████████████████████████| 1/1 [00:01<00:00,  1.34s/it]
[2022/07/04 01:55:52] ppocr INFO: metric eval ***************
[2022/07/04 01:55:52] ppocr INFO: acc:0.8645832432725788

如需获取已训练模型,请扫码填写问卷,加入PaddleOCR官方交流群获取全部OCR垂类模型下载链接、《动手学OCR》电子书等全套OCR学习资料

将下载或训练完成的模型放置在对应目录下即可完成模型推理

关于后续的模型导出和串接,系列项目前面已经介绍过了,本文就不再赘述。

7 总结

在电表检测识别任务上,PP-OCRv3效果实现了大幅提升,可以预见的是,在增加电表原始图片+生成补充数据集后,基于PP-OCRv3的电表检测识别模型,将基本具备直接落地的能力。

开源链接:https://aistudio.baidu.com/aistudio/projectdetail/511591

Logo

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

更多推荐