前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >PP-Structure版面分析、表格识别使用指南

PP-Structure版面分析、表格识别使用指南

作者头像
算法之名
发布2022-10-31 10:34:49
5.5K0
发布2022-10-31 10:34:49
举报
文章被收录于专栏:算法之名

版面分析

版面分析指的是对图片形式的文档进行区域划分,定位其中的关键区域,如文字、标题、表格、图片等。

在上图中,最上面有图片区域,中间是标题和表格区域,下面是文字区域。

命令行使用

代码语言:javascript
复制
paddleocr --image_dir=ppstructure/docs/table/1.png --type=structure --table=false --ocr=false

Python 代码使用

代码语言:javascript
复制
import os
import cv2
from paddleocr import PPStructure,save_structure_res

if __name__ == '__main__':

    table_engine = PPStructure(table=False, ocr=False, show_log=True)

    save_folder = './output'
    img_path = 'ppstructure/docs/table/1.png'
    img = cv2.imread(img_path)
    result = table_engine(img)
    save_structure_res(result, save_folder, os.path.basename(img_path).split('.')[0])

    for line in result:
        img = line.pop('img')
        print(line)
        while True:
            cv2.imshow('img', img)
            key = cv2.waitKey()
            if key & 0xFF == ord('q'):
                break
        cv2.destroyAllWindows()

运行结果

代码语言:javascript
复制
{'type': 'text', 'bbox': [11, 729, 407, 847], 'res': '', 'img_idx': 0}
{'type': 'text', 'bbox': [442, 754, 837, 847], 'res': '', 'img_idx': 0}
{'type': 'title', 'bbox': [443, 705, 559, 719], 'res': '', 'img_idx': 0}
{'type': 'figure', 'bbox': [10, 1, 841, 294], 'res': '', 'img_idx': 0}
{'type': 'figure_caption', 'bbox': [70, 317, 707, 357], 'res': '', 'img_idx': 0}
{'type': 'figure_caption', 'bbox': [160, 317, 797, 335], 'res': '', 'img_idx': 0}
{'type': 'table', 'bbox': [453, 359, 822, 664], 'res': '', 'img_idx': 0}
{'type': 'table', 'bbox': [12, 360, 410, 716], 'res': '', 'img_idx': 0}
{'type': 'table_caption', 'bbox': [494, 343, 785, 356], 'res': '', 'img_idx': 0}
{'type': 'table_caption', 'bbox': [69, 318, 706, 357], 'res': '', 'img_idx': 0}

 'text', 'bbox': 11, 729, 407, 847

'text', 'bbox': 442, 754, 837, 847

'title', 'bbox': 443, 705, 559, 719

'figure', 'bbox': 10, 1, 841, 294

'figure_caption', 'bbox': 70, 317, 707, 357

'figure_caption', 'bbox': 160, 317, 797, 335

'table', 'bbox': 453, 359, 822, 664

'table', 'bbox': 12, 360, 410, 716

'table_caption', 'bbox': 494, 343, 785, 356

'table_caption', 'bbox': 69, 318, 706, 357

从运行的结果来看,它是将原始图像拆成了图像、图像标题、表格、表格标题、文字和文字标题六个分类。

模型训练

下载 PaddleDection 框架代码

PaddleDetection: PaddleDetection 的目的是为工业界和学术界提供丰富、易用的目标检测模型 (gitee.com)

下载,解压,进入 PaddleDection 主目录,安装需要的 Python 库

代码语言:javascript
复制
pip install -r .\requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple

cocotools 安装错误的话可以使用如下命令安装

代码语言:javascript
复制
git clone https://github.com/pdollar/coco.git
cd coco/PythonAPI
python setup.py build_ext --inplace
python setup.py build_ext install

数据集:这是一个英文数据集,包含 5 个类 {0: "Text", 1: "Title", 2: "List", 3:"Table", 4:"Figure"}

代码语言:javascript
复制
wget https://dax-cdn.cdn.appdomain.cloud/dax-publaynet/1.0.0/publaynet.tar.gz
tar -xzvf publaynet.tar.gz

这是一个 COCO 数据集,随便打开一张图像大概是这个样子的

它的标签文件是 json 文件,里面的内容如下

代码语言:javascript
复制
{"file_name": "PMC1087888_00001.jpg", "width": 612, "id": 410520, "height": 792}
{"segmentation": [[55.14, 456.69, 296.1, 456.69, 296.1, 467.82, 296.1, 467.82, 296.1, 480.15, 296.1, 480.15, 296.1, 491.28, 144.06, 491.28, 144.06, 503.04, 55.14, 503.04, 55.14, 491.92, 55.14, 480.15, 55.14, 468.46, 55.14, 456.69]], 
"area": 9380.344594193506, 
"iscrowd": 0, 
"image_id": 410520, 
"bbox": [55.14, 456.69, 240.96, 46.35], 
"category_id": 1, 
"id": 4010177}

第一行表示标注文件中图像信息列表,每个元素是一张图像的信息。第二行到最后一个行表示标注文件中目标物体的标注信息列表,每个元素是一个目标物体的标注信息。这里只是其中一个区域的标注,其他还有几个区域标注,这里没有列出。

代码语言:javascript
复制
{

    'segmentation':             # 物体的分割标注
    'area':                     # 物体的区域面积
    'iscrowd':                  # 是否多区域
    'image_id':                 # image id
    'bbox':                     # bbox [x1,y1,w,h]
    'category_id':              # 图片类别
    'id':                       # 区域 id
}

这里我们可以看到 category_id 为 1,表示这个区域是一个 Title。

修改配置文件 configs/picodet/legacy_model/application/layout_analysis/picodet_lcnet_x1_0_layout.yml ,内容如下

PP-PicoDet 模型原理

PP-PicoDet 是一个目标检测模型,对比于 YOLO 系列在轻量级检测中 (移动端) 表现更好

PicoDet-S 以 0.99M 参数以及 1.08G-FLOPs 实现 30.06% mAP。它在移动端 ARM-CPU 上实现了 150FPS,输入尺寸为 320。PicoDet-M 在仅 2.15M 参数和 2.5G-FLOPs 的情况下实现 34.3% mAP。PicoDet-L 在仅 3.3M 参数和 8.74G-FLOPs 情况下实现 40.9% mAP。本文提供了小、中、大三种模型来支持不同的部署场景。

  • 整体网络结构

我们先来看它的主干网 (Backbone),是百度自研的轻量级网络 ESNet。它是根据 ShuffleNet V2 进行的改进,有关 ShuffleNet V2 的内容可以参考深度学习网络模型的改进与调整 中的 ShuffleNet V2。

第一个改进是引入了 SE block,主要作用是对通道加权,增强特征的提取能力。有关 SE block 的内容可以参考深度学习网络模型的改进与调整 MobileNet V3。第二个改进是使用了一组深度可分离卷积在 stride=2 的时候,替换掉了 channel shuffle。channel shuffle 可以增强不同通道中的信息交换,但是这个信息交换是不容于 1*1 卷积的,1*1 卷积的计算速度通常比较慢,这里在每次进行下采样的时候就会替换掉 channel shuffle。第三个改进是引入了 Ghost block,主要目的是降低网络的冗余性,有关 Ghost block 的内容可以参考深度学习网络模型的改进与调整 中的 GhostNet 的 Ghost bottleneck。

Backbone 的权重占整个网络的 60% 以上,并且 Backbone 的特征提取作用也是至关重要的。优化 Backbone 对检测的性能提升还是非常有帮助的。

在 Neck 部分,使用的是 CSP-PAN。PAN 是一种双向特征融合的网络,先上采样 (深层到浅层) 再下采样 (浅层到深层)。CSP-PAN 中在每层输入的地方插入了 1*1 的卷积,用来统一通道数,这么做的好处可以减少计算量,因为不统一通道数,在 concat 融合的过程中,通道数会成倍的增加,越来越多,对于移动端是非常不友好的。在上图中所示,分层特征图通过 Backbone 输出的各层中,C3 的通道数最小为 96,那么整个 CSP-PAN 都会通过 1*1 卷积统一到 96 通道数的维度上,整个参数量减少了 73%。

CSP-PAN 每一层输入的网络结构是 CSP 的结构,有关 CSP 结构的内容可以参考 YOLO 系列介绍 (二) YOLOV4 中的内容。一般的 PAN 网络都是三层输出,但是在 CSP-PAN 中增加了一层 64 倍下采样的分支,就是上图中右上角橙色框的 P6 部分,目的是为了增大大物体的召回率。这里的下采样都使用的是深度可分离卷积 (DP)。这种操作 mAP 提升了 1 个点,速度只损失了 0.25%。

  • Sim-OTA 动态样本匹配

只有符合的标签匹配策略的样本才会定义为正样本,只有正样本所对应的特征图的像素才能够参与 loss 计算及反传。所以标签匹配策略是非常重要的,选择合适的正样本对精度提升至关重要。

Sim-OTA 是 YOLOX 作者在 OT 策略上提出的简化算法,其作用是为不同目标选择不同数量的正样本。样本采集是动态的。

SimOTA 跟其他采样方式相比,对于一些被遮挡的物体,它仍然可以采样到,比如上图中中间的头发背影。

  1. 上图是一张我们要检测的原始图像,上面有绿色框和黄色框,绿色框为 ground truth box,即人工标注出来的区域。黄色框为当前 ground truth box 的中心点为中心点作为一个特征点,向上下左右四个方向分别延伸 2.5 倍的步长 (stride,特征图对应原图的比例),不同的特征图上的特征点的黄色框是不同的。灰色网格代表 FPN 的其中一个步长 (stride) 为长度给图像打的网格,一个网格代表 feature map 中的像素点所能看到的感受野。如果 feature map 中的一个像素点对应原图的中心点在绿色框或者黄色框的区域内,那么这个像素点就属于正样本的候选区域。那么在上图中黄绿框相交的四个角的灰色网格不属于正样本区域。
  2. 计算正样本候选区域所产生的每个预测框与当前 ground truth box 的 IoU。在 YOLO 中一个灰色网格会产生三个预测框 Anchor。
  3. 将计算的 IoU 按从大到小排序,将排名靠前 10 的 IoU 求和,由于 IoU 本身值不会超过 1,所以这个和的指区间为 0~10,记该值为 dynamic_k。
  4. 计算正样本候选区域产生的预测框与当前 ground truth box 的 cost 值,得到 Cost 代价矩阵。该矩阵计算公式为,其中 λ 是平衡系数,和分别是 ground truth box 和预测框 Anchor 的分类损失和回归损失。该矩阵代表当前 gruond truth box 和预测框之间的代价关系,预测框的 cost 值越小越好。通过 Cost 矩阵,使网络能够自适应的找到每个 ground truth box 的正样本。Cost 代价矩阵由三个部分组成:1、每个 ground truth box 和灰色网格的预测框的重合程度越高,代表这个灰色网格已经尝试去拟合 ground truth box 了,因此它的 cost 代价就会越小。2、每个 ground truth box 和灰色网格的预测框的分类精度越高,代表这个灰色网格已经尝试去拟合 ground truth box 了。3、每个 ground truth box 的中心是否落在灰色网格的一定半径内,如果在一定半径内,代表这个灰色网格已经尝试去拟合 ground truth box 了。
  5. 将 Cost 矩阵的的值按从小到大的顺序排列。取前 dynamic_k 个 cost 最小的预测框作为当前 ground truth box 最终的正样本,将其余剩下的预测框作为负样本。对于不同的 ground truth box,dynamic_k 的值是不一样的。
  6. 使用求出的最终正负样本来计算分类和回归损失。

在 PP-PicoDet 中,修改了 Sim-OTA 的原始 loss,变为,该方法只在训练阶段使用,预测阶段是无损的。mAP 提升了 1%。

  • 其他优化

代码分析

ESNet 的完整代码位于 ppdet/modeling/backbones/esnet.py

代码语言:javascript
复制
class SEModule(nn.Layer):
    def __init__(self, channel, reduction=4):
        '''
        SE模块
        channel:输入通道数
        reducion:通道缩放率
        '''
        super(SEModule, self).__init__()
        # 通过全局池化将空间尺寸变成1*1,通道数不变
        self.avg_pool = AdaptiveAvgPool2D(1)
        # 1*1卷积进行通道降维
        self.conv1 = Conv2D(
            in_channels=channel,
            out_channels=channel // reduction,
            kernel_size=1,
            stride=1,
            padding=0,
            weight_attr=ParamAttr(),
            bias_attr=ParamAttr())
        # 1*1卷积进行通道升维
        self.conv2 = Conv2D(
            in_channels=channel // reduction,
            out_channels=channel,
            kernel_size=1,
            stride=1,
            padding=0,
            weight_attr=ParamAttr(),
            bias_attr=ParamAttr())

    def forward(self, inputs):
        outputs = self.avg_pool(inputs)
        outputs = self.conv1(outputs)
        outputs = F.relu(outputs)
        outputs = self.conv2(outputs)
        # 获取每个通道的权重
        outputs = F.hardsigmoid(outputs)
        # 利用每个通道的权重对输入的特征图进行特征值的调整
        return paddle.multiply(x=inputs, y=outputs)
代码语言:javascript
复制
def channel_shuffle(x, groups):
    '''
    通道洗牌
    '''
    batch_size, num_channels, height, width = x.shape[0:4]
    assert num_channels % groups == 0, 'num_channels should be divisible by groups'
    # 每通道的向量数n
    channels_per_group = num_channels // groups
    # reshape成(g,n)的矩阵
    x = paddle.reshape(
        x=x, shape=[batch_size, groups, channels_per_group, height, width])
    # 转置成(n,g)
    x = paddle.transpose(x=x, perm=[0, 2, 1, 3, 4])
    # flatten打散
    x = paddle.reshape(x=x, shape=[batch_size, num_channels, height, width])
    return x
代码语言:javascript
复制
class ConvBNLayer(nn.Layer):
    def __init__(self,
                 in_channels,
                 out_channels,
                 kernel_size,
                 stride,
                 padding,
                 groups=1,
                 act=None):
        '''
        普通卷积
        '''
        super(ConvBNLayer, self).__init__()
        self._conv = Conv2D(
            in_channels=in_channels,
            out_channels=out_channels,
            kernel_size=kernel_size,
            stride=stride,
            padding=padding,
            groups=groups,
            weight_attr=ParamAttr(initializer=KaimingNormal()),
            bias_attr=False)

        self._batch_norm = BatchNorm2D(
            out_channels,
            weight_attr=ParamAttr(regularizer=L2Decay(0.0)),
            bias_attr=ParamAttr(regularizer=L2Decay(0.0)))
        if act == "hard_swish":
            act = 'hardswish'
        self.act = act

    def forward(self, inputs):
        y = self._conv(inputs)
        y = self._batch_norm(y)
        if self.act:
            y = getattr(F, self.act)(y)
        return y
代码语言:javascript
复制
class InvertedResidual(nn.Layer):
    def __init__(self,
                 in_channels,
                 mid_channels,
                 out_channels,
                 stride,
                 act="relu"):
        '''
        stride=1构建块
        '''
        super(InvertedResidual, self).__init__()
        # 1*1卷积
        self._conv_pw = ConvBNLayer(
            in_channels=in_channels // 2,
            out_channels=mid_channels // 2,
            kernel_size=1,
            stride=1,
            padding=0,
            groups=1,
            act=act)
        # 3*3深度可分离卷积
        self._conv_dw = ConvBNLayer(
            in_channels=mid_channels // 2,
            out_channels=mid_channels // 2,
            kernel_size=3,
            stride=stride,
            padding=1,
            groups=mid_channels // 2,
            act=None)
        # SE block
        self._se = SEModule(mid_channels)
        # 1*1深度可分离卷积
        self._conv_linear = ConvBNLayer(
            in_channels=mid_channels,
            out_channels=out_channels // 2,
            kernel_size=1,
            stride=1,
            padding=0,
            groups=1,
            act=act)

    def forward(self, inputs):
        # 对特征图进行通道拆分
        x1, x2 = paddle.split(
            inputs,
            num_or_sections=[inputs.shape[1] // 2, inputs.shape[1] // 2],
            axis=1)
        # 左分支
        x2 = self._conv_pw(x2)
        x3 = self._conv_dw(x2)
        # 将1*1和3*3的结果进行合并
        x3 = paddle.concat([x2, x3], axis=1)
        # 合并后SE
        x3 = self._se(x3)
        x3 = self._conv_linear(x3)
        # 合并左右分支
        out = paddle.concat([x1, x3], axis=1)
        return channel_shuffle(out, 2)
代码语言:javascript
复制
class InvertedResidualDS(nn.Layer):
    def __init__(self,
                 in_channels,
                 mid_channels,
                 out_channels,
                 stride,
                 act="relu"):
        '''
        stride=2构建块
        '''
        super(InvertedResidualDS, self).__init__()
        # 右分支
        # 3*3深度可分离卷积
        self._conv_dw_1 = ConvBNLayer(
            in_channels=in_channels,
            out_channels=in_channels,
            kernel_size=3,
            stride=stride,
            padding=1,
            groups=in_channels,
            act=None)
        # 1*1深度可分离卷积
        self._conv_linear_1 = ConvBNLayer(
            in_channels=in_channels,
            out_channels=out_channels // 2,
            kernel_size=1,
            stride=1,
            padding=0,
            groups=1,
            act=act)
        # 左分支
        # 1*1卷积
        self._conv_pw_2 = ConvBNLayer(
            in_channels=in_channels,
            out_channels=mid_channels // 2,
            kernel_size=1,
            stride=1,
            padding=0,
            groups=1,
            act=act)
        # 3*3深度可分离卷积
        self._conv_dw_2 = ConvBNLayer(
            in_channels=mid_channels // 2,
            out_channels=mid_channels // 2,
            kernel_size=3,
            stride=stride,
            padding=1,
            groups=mid_channels // 2,
            act=None)
        # se block
        self._se = SEModule(mid_channels // 2)
        # 1*1深度可分离卷积
        self._conv_linear_2 = ConvBNLayer(
            in_channels=mid_channels // 2,
            out_channels=out_channels // 2,
            kernel_size=1,
            stride=1,
            padding=0,
            groups=1,
            act=act)
        # 3*3深度可分离卷积
        self._conv_dw_mv1 = ConvBNLayer(
            in_channels=out_channels,
            out_channels=out_channels,
            kernel_size=3,
            stride=1,
            padding=1,
            groups=out_channels,
            act="hard_swish")
        # 1*1卷积
        self._conv_pw_mv1 = ConvBNLayer(
            in_channels=out_channels,
            out_channels=out_channels,
            kernel_size=1,
            stride=1,
            padding=0,
            groups=1,
            act="hard_swish")

    def forward(self, inputs):
        # 右分支
        x1 = self._conv_dw_1(inputs)
        x1 = self._conv_linear_1(x1)
        # 左分支
        x2 = self._conv_pw_2(inputs)
        x2 = self._conv_dw_2(x2)
        x2 = self._se(x2)
        x2 = self._conv_linear_2(x2)
        # 合并左右分支
        out = paddle.concat([x1, x2], axis=1)
        out = self._conv_dw_mv1(out)
        out = self._conv_pw_mv1(out)

        return out
代码语言:javascript
复制
@register
@serializable
class ESNet(nn.Layer):
    def __init__(self,
                 scale=1.0,
                 act="hard_swish",
                 feature_maps=[4, 11, 14],
                 channel_ratio=[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]):
        '''
        ESNet Backbone
        '''
        super(ESNet, self).__init__()
        self.scale = scale
        if isinstance(feature_maps, Integral):
            feature_maps = [feature_maps]
        self.feature_maps = feature_maps
        # C3、C4、C5层的ES Block的数量
        stage_repeats = [3, 7, 3]
        # 每一步的输出通道数
        stage_out_channels = [
            -1, 24, make_divisible(128 * scale), make_divisible(256 * scale),
            make_divisible(512 * scale), 1024
        ]

        self._out_channels = []
        self._feature_idx = 0
        # 第一个普通卷积
        self._conv1 = ConvBNLayer(
            in_channels=3,
            out_channels=stage_out_channels[1],
            kernel_size=3,
            stride=2,
            padding=1,
            act=act)
        # 3*3最大池化层
        self._max_pool = MaxPool2D(kernel_size=3, stride=2, padding=1)
        self._feature_idx += 1

        # 2. bottleneck sequences
        self._block_list = []
        arch_idx = 0
        # 遍历每一个特征输出层
        for stage_id, num_repeat in enumerate(stage_repeats):
            # 遍历每一层中的重复次数
            for i in range(num_repeat):
                channels_scales = channel_ratio[arch_idx]
                mid_c = make_divisible(
                    int(stage_out_channels[stage_id + 2] * channels_scales),
                    divisor=8)
                if i == 0:
                    # 第一次进行降采样,即stride=2
                    block = self.add_sublayer(
                        name=str(stage_id + 2) + '_' + str(i + 1),
                        sublayer=InvertedResidualDS(
                            in_channels=stage_out_channels[stage_id + 1],
                            mid_channels=mid_c,
                            out_channels=stage_out_channels[stage_id + 2],
                            stride=2,
                            act=act))
                else:
                    # 之后不进行降采样,即stride=1
                    block = self.add_sublayer(
                        name=str(stage_id + 2) + '_' + str(i + 1),
                        sublayer=InvertedResidual(
                            in_channels=stage_out_channels[stage_id + 2],
                            mid_channels=mid_c,
                            out_channels=stage_out_channels[stage_id + 2],
                            stride=1,
                            act=act))
                # ES block列表
                self._block_list.append(block)
                arch_idx += 1
                self._feature_idx += 1
                self._update_out_channels(stage_out_channels[stage_id + 2],
                                          self._feature_idx, self.feature_maps)

    def _update_out_channels(self, channel, feature_idx, feature_maps):
        if feature_idx in feature_maps:
            self._out_channels.append(channel)

    def forward(self, inputs):
        y = self._conv1(inputs['image'])
        y = self._max_pool(y)
        outs = []
        for i, inv in enumerate(self._block_list):
            # 通过每一层的ES block
            y = inv(y)
            if i + 2 in self.feature_maps:
                outs.append(y)

        return outs

    @property
    def out_shape(self):
        return [ShapeSpec(channels=c) for c in self._out_channels]

CSP-PAN 的完整代码位于 ppdet/modeling/necks/csp_pan.py

代码语言:javascript
复制
class ConvBNLayer(nn.Layer):
    def __init__(self,
                 in_channel=96,
                 out_channel=96,
                 kernel_size=3,
                 stride=1,
                 groups=1,
                 act='leaky_relu'):
        '''
        普通卷积
        '''
        super(ConvBNLayer, self).__init__()
        initializer = nn.initializer.KaimingUniform()
        self.conv = nn.Conv2D(
            in_channels=in_channel,
            out_channels=out_channel,
            kernel_size=kernel_size,
            groups=groups,
            padding=(kernel_size - 1) // 2,
            stride=stride,
            weight_attr=ParamAttr(initializer=initializer),
            bias_attr=False)
        self.bn = nn.BatchNorm2D(out_channel)
        if act == "hard_swish":
            act = 'hardswish'
        self.act = act

    def forward(self, x):
        x = self.bn(self.conv(x))
        if self.act:
            x = getattr(F, self.act)(x)
        return x
代码语言:javascript
复制
class DPModule(nn.Layer):

    def __init__(self,
                 in_channel=96,
                 out_channel=96,
                 kernel_size=3,
                 stride=1,
                 act='leaky_relu',
                 use_act_in_out=True):
        '''
        深度可分离卷积DP
        '''
        super(DPModule, self).__init__()
        initializer = nn.initializer.KaimingUniform()
        self.use_act_in_out = use_act_in_out
        self.dwconv = nn.Conv2D(
            in_channels=in_channel,
            out_channels=out_channel,
            kernel_size=kernel_size,
            groups=out_channel,
            padding=(kernel_size - 1) // 2,
            stride=stride,
            weight_attr=ParamAttr(initializer=initializer),
            bias_attr=False)
        self.bn1 = nn.BatchNorm2D(out_channel)
        self.pwconv = nn.Conv2D(
            in_channels=out_channel,
            out_channels=out_channel,
            kernel_size=1,
            groups=1,
            padding=0,
            weight_attr=ParamAttr(initializer=initializer),
            bias_attr=False)
        self.bn2 = nn.BatchNorm2D(out_channel)
        if act == "hard_swish":
            act = 'hardswish'
        self.act = act

    def forward(self, x):
        x = self.bn1(self.dwconv(x))
        if self.act:
            x = getattr(F, self.act)(x)
        x = self.bn2(self.pwconv(x))
        if self.use_act_in_out and self.act:
            x = getattr(F, self.act)(x)
        return x
代码语言:javascript
复制
class DarknetBottleneck(nn.Layer):

    def __init__(self,
                 in_channels,
                 out_channels,
                 kernel_size=3,
                 expansion=0.5,
                 add_identity=True,
                 use_depthwise=False,
                 act="leaky_relu"):
        '''
        DarkNet block,包含两个卷积和一个残差连接
        '''
        super(DarknetBottleneck, self).__init__()
        hidden_channels = int(out_channels * expansion)
        conv_func = DPModule if use_depthwise else ConvBNLayer
        self.conv1 = ConvBNLayer(
            in_channel=in_channels,
            out_channel=hidden_channels,
            kernel_size=1,
            act=act)
        self.conv2 = conv_func(
            in_channel=hidden_channels,
            out_channel=out_channels,
            kernel_size=kernel_size,
            stride=1,
            act=act)
        self.add_identity = \
            add_identity and in_channels == out_channels

    def forward(self, x):
        identity = x
        out = self.conv1(x)
        out = self.conv2(out)

        if self.add_identity:
            return out + identity
        else:
            return out
代码语言:javascript
复制
class CSPLayer(nn.Layer):

    def __init__(self,
                 in_channels,
                 out_channels,
                 kernel_size=3,
                 expand_ratio=0.5,
                 num_blocks=1,
                 add_identity=True,
                 use_depthwise=False,
                 act="leaky_relu"):
        '''
        CSPDarkNet
        '''
        super().__init__()
        mid_channels = int(out_channels * expand_ratio)
        # 右分支
        self.main_conv = ConvBNLayer(in_channels, mid_channels, 1, act=act)
        # 左分支
        self.short_conv = ConvBNLayer(in_channels, mid_channels, 1, act=act)
        # 最后输出的1*1卷积
        self.final_conv = ConvBNLayer(
            2 * mid_channels, out_channels, 1, act=act)
        # 堆叠DarkNet block
        self.blocks = nn.Sequential(* [
            DarknetBottleneck(
                mid_channels,
                mid_channels,
                kernel_size,
                1.0,
                add_identity,
                use_depthwise,
                act=act) for _ in range(num_blocks)
        ])

    def forward(self, x):
        # 左分支,通过1*1的卷积,获取原特征图一半的通道数
        x_short = self.short_conv(x)
        # 右分支,通过1*1的卷积,获取原特征图一半的通道数
        x_main = self.main_conv(x)
        # 右分支通过一系列DarkNet block的堆叠
        x_main = self.blocks(x_main)
        # 拼接左右分支
        x_final = paddle.concat((x_main, x_short), axis=1)
        # 使用1*1卷积进行输出
        return self.final_conv(x_final)
代码语言:javascript
复制
class Channel_T(nn.Layer):
    def __init__(self,
                 in_channels=[116, 232, 464],
                 out_channels=96,
                 act="leaky_relu"):
        '''
        输入部分统一通道数
        '''
        super(Channel_T, self).__init__()
        self.convs = nn.LayerList()
        for i in range(len(in_channels)):
            self.convs.append(
                ConvBNLayer(
                    in_channels[i], out_channels, 1, act=act))

    def forward(self, x):
        outs = [self.convs[i](x[i]) for i in range(len(x))]
        return outs
代码语言:javascript
复制
@register
@serializable
class CSPPAN(nn.Layer):

    def __init__(self,
                 in_channels,
                 out_channels,
                 kernel_size=5,
                 num_features=3,
                 num_csp_blocks=1,
                 use_depthwise=True,
                 act='hard_swish',
                 spatial_scales=[0.125, 0.0625, 0.03125]):
        '''
        CSP-PAN Neck
        '''
        super(CSPPAN, self).__init__()
        self.conv_t = Channel_T(in_channels, out_channels, act=act)
        in_channels = [out_channels] * len(spatial_scales)
        # 输入的三层通道数
        self.in_channels = in_channels
        # 输出的三层通道数
        self.out_channels = out_channels
        # 空间尺度
        self.spatial_scales = spatial_scales
        # 特征数
        self.num_features = num_features
        # 卷积层,深度可分离卷积或普通卷积
        conv_func = DPModule if use_depthwise else ConvBNLayer

        if self.num_features == 4:
            # P6层的第一个深度可分离卷积
            self.first_top_conv = conv_func(
                in_channels[0], in_channels[0], kernel_size, stride=2, act=act)
            # P6层的第二个深度可分离卷积
            self.second_top_conv = conv_func(
                in_channels[0], in_channels[0], kernel_size, stride=2, act=act)
            self.spatial_scales.append(self.spatial_scales[-1] / 2)

        # 上采样
        self.upsample = nn.Upsample(scale_factor=2, mode='nearest')
        # 上采样模块列表
        self.top_down_blocks = nn.LayerList()
        for idx in range(len(in_channels) - 1, 0, -1):
            # 使用CSPDarkNet进行上采样
            self.top_down_blocks.append(
                CSPLayer(
                    in_channels[idx - 1] * 2,
                    in_channels[idx - 1],
                    kernel_size=kernel_size,
                    num_blocks=num_csp_blocks,
                    add_identity=False,
                    use_depthwise=use_depthwise,
                    act=act))

        # 下采样列表
        self.downsamples = nn.LayerList()
        # 下采样模块列表
        self.bottom_up_blocks = nn.LayerList()
        # 下采样中一个深度可分离卷积接一个CSPDarkNet
        for idx in range(len(in_channels) - 1):
            self.downsamples.append(
                conv_func(
                    in_channels[idx],
                    in_channels[idx],
                    kernel_size=kernel_size,
                    stride=2,
                    act=act))
            self.bottom_up_blocks.append(
                CSPLayer(
                    in_channels[idx] * 2,
                    in_channels[idx + 1],
                    kernel_size=kernel_size,
                    num_blocks=num_csp_blocks,
                    add_identity=False,
                    use_depthwise=use_depthwise,
                    act=act))

    def forward(self, inputs):
        assert len(inputs) == len(self.in_channels)
        # 统一输入通道数
        inputs = self.conv_t(inputs)

        # 上采样过程
        inner_outs = [inputs[-1]]
        for idx in range(len(self.in_channels) - 1, 0, -1):
            # 获取深层特征
            feat_heigh = inner_outs[0]
            # 获取浅层特征(比feat_heigh低一层)
            feat_low = inputs[idx - 1]
            # 对深层特征进行上采样
            upsample_feat = self.upsample(feat_heigh)
            # 合并上采样之后的高层特征和浅层特征,再送入CSPDarkNet网络
            inner_out = self.top_down_blocks[len(self.in_channels) - 1 - idx](
                paddle.concat([upsample_feat, feat_low], 1))
            inner_outs.insert(0, inner_out)

        # 下采样过程
        outs = [inner_outs[0]]
        for idx in range(len(self.in_channels) - 1):
            # 获取浅层特征
            feat_low = outs[-1]
            # 获取深层特征(比feat_low高一层)
            feat_height = inner_outs[idx + 1]
            # 对浅层特征使用深度可分离卷积进行下采样
            downsample_feat = self.downsamples[idx](feat_low)
            # 合并下采样之后的浅层特征和深层特征,再送入CSPDarkNet网络
            out = self.bottom_up_blocks[idx](paddle.concat(
                [downsample_feat, feat_height], 1))
            outs.append(out)

        top_features = None
        # 获取P6层的特征
        if self.num_features == 4:
            top_features = self.first_top_conv(inputs[-1])
            top_features = top_features + self.second_top_conv(outs[-1])
            outs.append(top_features)

        return tuple(outs)

    @property
    def out_shape(self):
        return [
            ShapeSpec(
                channels=self.out_channels, stride=1. / s)
            for s in self.spatial_scales
        ]

    @classmethod
    def from_config(cls, cfg, input_shape):
        return {'in_channels': [i.channels for i in input_shape], }

表格识别

表格识别指的是对图片上的表格进行识别,不仅要识别表格中的文字,而且需要识别表格中单元格的坐标信息。

整个过程分为两步,第一调用文本检测算法,对单行文本进行检测,得到一个一个的检测框,再对这些检测框送入到文本识别算法获得文字。第二调用表格结构预测算法获得单元格的坐标,并与文本检测的检测框坐标进行聚合,再与文本识别结果进行聚合,最终得到图像表格的 Excel 结果。

命令行使用

进入 PaddleOCR 的 ppstructure 目录

代码语言:javascript
复制
mkdir inference
cd inference
wget https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_det_infer.tar
tar -xzvf ch_PP-OCRv3_det_infer.tar
wget https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_rec_infer.tar
tar -xzvf ch_PP-OCRv3_rec_infer.tar
wget https://paddleocr.bj.bcebos.com/ppstructure/models/slanet/ch_ppstructure_mobile_v2.0_SLANet_infer.tar
tar -xzvf ch_ppstructure_mobile_v2.0_SLANet_infer.tar

在 ppstructure/table/predict_table.py 中添加执行参数

代码语言:javascript
复制
--det_model_dir=inference/ch_PP-OCRv3_det_infer --rec_model_dir=inference/ch_PP-OCRv3_rec_infer --table_model_dir=inference/ch_ppstructure_mobile_v2.0_SLANet_infer --rec_char_dict_path=../ppocr/utils/ppocr_keys_v1.txt --table_char_dict_path=../ppocr/utils/dict/table_structure_dict_ch.txt --image_dir=docs/table/table.jpg --output=../output/table 

并将工作目录设置为 ppstructure。这里的表格图片如下

执行结果,生成的 Excel 文件如下

模型训练

文字检测模型略过,文字识别模型的训练可以参考 PaddleOCR 使用指南 中的模型训练

这里主要看一下表格结构预测模型的训练

数据集:https://dax-cdn.cdn.appdomain.cloud/dax-pubtabnet/2.0.0/pubtabnet.tar.gz

下载后解压,大概样子如下所示

数据集生成

下载数据集生成工具:GitHub - WenmuZhou/TableGeneration: 通过浏览器渲染生成表格图像

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-10-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 版面分析
  • 表格识别
相关产品与服务
图像识别
腾讯云图像识别基于深度学习等人工智能技术,提供车辆,物体及场景等检测和识别服务, 已上线产品子功能包含车辆识别,商品识别,宠物识别,文件封识别等,更多功能接口敬请期待。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档