《深入浅出OCR》第八章:【文档结构化】信息纠错与抽取

专栏介绍: 经过几个月的精心筹备,本作者推出全新系列《深入浅出OCR》专栏,对标最全OCR教程,具体章节如导图所示,将分别从OCR技术发展、方向、概念、算法、论文、数据集等各种角度展开详细介绍。
💙个人主页: GoAI |💚 公众号: GoAI的学习小屋 | 💛交流群: 704932595 |💜个人简介 : 掘金签约作者、百度飞桨PPDE、领航团团长、开源特训营导师、CSDN、阿里云社区人工智能领域博客专家、新星计划计算机视觉方向导师等,专注大数据与人工智能知识分享。

💻文章目录


👨‍💻本篇导读: 本章将介绍常见的文字识别后处理方法,按照不同的目的将内容分为两部分:文本纠错和文本结构化。文本纠错的目标是纠正 OCR输出文本中错误的文字,而文本结构化则是从OCR输出文本中定位需要的信息,并按照应用要求组织成特定的结构,方便小白或AI爱好者快速了解OCR方向知识.

《深入浅出OCR》第八章:【文档结构化】信息纠错与抽取

1.表格识别

现有的表格识别算法根据表格结构重建的原理可以分为下面四大类:

  1. 基于启发式规则的方法

  2. 基于CNN的方法

  3. 基于GCN的方法

  4. 基于End to End的方法

代表性论文被划分为上述四个类别中,具体如下表所示:

1.1中文拼写检查

中文拼写纠错(Chinese Spell Checking (CSC))任务任务通常不涉及添/删字词,只涉及替换,所以一般输入输出的句子是等长的。

1.2语法纠错

语法纠错 Grammatical Error Correction (GEC)相较于中文拼写检查,语法纠错需要增添/删除字词,因此通常是非等长纠正。

中文纠错流程一般包括: 错误识别--》候选召回--》纠错排序

1.3 中文纠错工具推荐:

(1)Pycorrector

github.com/shibing624/...

(2)correction

github.com/ccheng16/co...

(3)基于BERT的中文纠错模型

Soft-Masked BERT

2.主流纠错算法分类

纠错的关键问题分别为:语言模型字形的相似度度量 。字形的相似度度量给出真实值识别为当前结果的可能性 。语言模型给出当前识别结果最可能的几个真实值,因此,经典的解决算法有两类:

2.1 BK-tree

BK树是一种典型的树结构,用于快速查找。在上一章中,我在评价指标部分重点介绍了编辑距离概念,本章我将继续对基于BK树的编辑距离进行介绍。

首先,BK树用于纠错的核心思想是基于编辑距离,简单来说,编辑距离就是把字符串A到B,只用插入、删除和替换三种操作,最少需要多少步可以把A变成B,具体例子如下:

注:BK-tree代码目的是寻找最小的编辑距离。如果编辑距离相等,会根据上下文、词频等条件返回最优。

最小编辑距离:

定义:将一个单词变为另一个单词所需的最少编辑操作数

功能:评估两个单词之间的相似度,两单词间编辑距离越小越相似

基于BK-tree的英文纠错实战:

以输入'ekiyehu'为例 ,通过定义的BK-tree返回函数bk_tree.query(query_word, 1, min_dist=0)),设置与'ekiyehu'编辑距离为1,最小编辑距离相同的单词进行返回。

python 复制代码
import tqdm

def edit_distance(str_a: str, str_b: str) -> int:
    m, n = len(str_a), len(str_b)
    dp_table = []

    for row in range(m + 1):
        dp_table.append([0] * (n + 1))

    for i in range(m + 1):
        dp_table[i][0] = i
    for j in range(n + 1):
        dp_table[0][j] = j

    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if str_a[i - 1] == str_b[j - 1]:
                dp_table[i][j] = dp_table[i - 1][j - 1]
            else:
                dp_table[i][j] = min(
                    dp_table[i - 1][j - 1],
                    dp_table[i][j - 1],
                    dp_table[i - 1][j]
                ) + 1

    return dp_table[m][n]


class Node:

    def __init__(self, word: str):
        self.word = word
        self.branch = {}


class BKTree:

    def __init__(self):
        self.root = None
        self.word_list = []

    def build(self, word_list: list or dict) -> None:
        """
        构建 BK 树
        :param word_list: list or dict 词语列表或者词频字典
        :return: None
        """
        if not word_list:
            return None

        # 如果是词频字典形式,则将其按照词频降序排列,得到词语列表
        if type(word_list) == dict:
            word_list = [item[0] for item in sorted(word_list.items(), key=lambda x: x[1], reverse=True)]
        self.word_list = word_list

        # 首先,挑选第一个词语作为 BK 树的根结点
        self.root = Node(word_list[0])

        # 然后,依次往 BK 树中插入剩余的词语
        for word in tqdm.tqdm(word_list[1:]):
            self._build(self.root, word)

    def _build(self, parent_node: Node, word: str) -> None:
        """
        具体实现函数:构建 BK 树
        :param parent_node: Node 父节点
        :param word:        str  待添加到 BK 树的词语
        :return: None
        """
        dis = edit_distance(parent_node.word, word)

        # 判断当前距离(边)是否存在,若不存在,则创建新的结点;否则,继续沿着子树往下走
        if dis not in parent_node.branch:
            parent_node.branch[dis] = Node(word)
        else:
            self._build(parent_node.branch[dis], word)

    def query(self, query_word: str, max_dist: int, min_dist: int = 0) -> list:
        """
        BK 树查询
        :param query_word: str 查询词语
        :param max_dist:   int 最大距离
        :param min_dist:   int 最小距离
        :return: list 符合距离范围的词语列表
        """
        result = []

        self._traverse_judge_and_get(query_word, max_dist, min_dist, self.root, result)
        return result

    def _traverse_judge_and_get(self, query_word: str, max_dist: int, min_dist: int, node: Node, result: list) -> None:
        """
        具体实现函数:BK 树查询
        :param query_word: str  查询词语
        :param max_dist:   int  最大距离
        :param min_dist:   int  最小距离
        :param node:       Node 当前节点
        :param result:     list 符合距离范围的词语列表
        :return: None
        """
        if not node:
            return None

        dis = edit_distance(query_word, node.word)

        # 根据三角不等式来确定查询范围,以实现剪枝的目的
        left, right = max(1, dis - max_dist), dis + max_dist

        if dis == 0:
            for dis in range(left, right + 1):
                if dis in node.branch and min_dist <= dis <= max_dist:
                    self._traverse_and_get(node.branch[dis], result)
            return None

        for dis_range in range(left, right + 1):
            if dis_range in node.branch:
                dis_branch = edit_distance(query_word, node.branch[dis_range].word)

                # 符合距离范围的词语,将其添加到 result 列表中
                if min_dist <= dis_branch <= max_dist:
                    result.append(node.branch[dis_range].word)

                # 继续沿着子节点遍历,直到叶子节点
                self._traverse_judge_and_get(query_word, max_dist, min_dist, node.branch[dis_range], result)

    def _traverse_and_get(self, node: Node, result: list) -> None:
        """
        遍历 BK 树并获取遍历节点的词语
        :param node:       Node 当前节点
        :param result:     list 符合距离范围的词语列表
        :return: None
        """
        if not node:
            return None

        result.append(node.word)

        for dis, node_branch in node.branch.items():
            self._traverse_and_get(node_branch, result)

    def traverse_and_print(self, node: Node):
        if not node:
            print(self.root.word)
            self._traverse_and_print(self.root)
        else:
            self._traverse_and_print(node)

    def _traverse_and_print(self, node: Node):
        if node:
            for dis, child in node.branch.items():
                print(dis, child.word)
                self._traverse_and_print(child)

# def read_dict(dict_path):
#     with open(dict_path, 'rb') as f:
#         lines = f.readlines()
#         char_list = []
#         for i, line in enumerate(lines):
#             print(str(i) + '/' + str(len(lines)))
#             decode = line.decode()
#             char_list.append(decode.rstrip())
#         return char_list


if __name__ == '__main__':
    # word_list = ["game", "fame", "same", "gate", "gain", 'gay', "frame", "home", "aim", "acm", "ame", "fell", "fbcdg"]
    word_list =[]
    with open('dict_mw.txt', 'r') as f:
        lines = f.readlines()
        # char_list = []
        for line in lines:
            # print(str.replace(line,'\n',''))
            a=str.replace(line,'\n','')
            word_list.append(a)
            # print(str(i) + '/' + str(len(lines)))
            # decode = line.decode()
            # char_list.append(decode.rstrip())

    bk_tree = BKTree()
    bk_tree.build(word_list)
    query_word = 'ekiyehu'
    print(bk_tree.query(query_word, 1, min_dist=0))

除了BK-tree可以计算字符相似度用于纠错外,Python还提供开源的difflib库可以直接实现上述功能,其用法具体如下:

ini 复制代码
query_str = '市公安局'
s1 = '上海市邮政局'
s2 = '上海市公安局'
s3 = '上海市检查院'
print(difflib.SequenceMatcher(None, query_str, s1).quick_ratio())  
print(difflib.SequenceMatcher(None, query_str, s2).quick_ratio())  
print(difflib.SequenceMatcher(None, query_str, s3).quick_ratio())  
 
# 0.4
# 0.8 
# 0.08695652173913043

2.2 基于语言模型的中文纠错

中文纠错与英文纠错一样,可以分为两步:首先确定错误的位置,然后完成错误的纠正。本节我会介绍简单的OCR文本纠错系统,对应框架图如下。

对于OCR模型的输出文字,先用语言模型判断可能错误位置,然后通过融合OCR模型和语言模型信息,输出最可能正确的纠正文本。

总结:

语言模型是NLP领域十分常用的基础模型。语言模型是对文本概率分布的建模,它可以给出一段文本出现的概率。通过出现概率,我们可以判断这段文本常见与否,常见的文本很可能是正常用法,不常见的文本则可能存在错误,很少被人使用,因此一定程度上也可以看作是文本合法性的衡量标准。

基于语言模型的中文纠错实践:

n-gram模型介绍

在中文错别字查错情景中,我们判断一个句子是否合法可以通过计算它的概率来得到,假设一个句子 S = {w1, w2, ..., wn},则问题可以转换成如下形式,其中P(S) 被称为语言模型,即用来计算一个句子合法概率的模型。

css 复制代码
P(s) = P(w1, w2, ..., wn) = P(w1) * P(w2|w1) * P(w3|w2,w1) *....*P(wn|wn-1,wn-2,...,w2,w1)

下面我将采用基于n-gram语言模型的中文分词进行实例演示,以最大化概率2-gram分词为例,算法流程如下

1、将带分词的字符串从左到右切分为w1,w2,..,wi,计算当前词与所有前驱词的概率。

2、计算该词的累计概率值,并筛选最大的累计概率则为最好的前驱点。

3、重复步骤3,直到该字符串结束。

4、从w开始,按照从右到左的顺序,依次将没歌词的最佳前驱词输出,即字符串的分词结束。

2-gram 分词相关代码:

ini 复制代码
word_dict = {}# 用于统计词语的频次
transdict = {} # 用于统计该词后面词出现的个数
def train(train_data_path):
    transdict['<BEG>'] = {}#<beg>表示开始的标识
    word_dict['<BEG>'] = 0
    for sent in open(train_data_path,encoding='utf-8'):
        word_dict['<BEG>'] +=1
        sent = sent.strip().split(' ')
        sent_list = []
        for word in sent:
            if word !='':
                sent_list.append(word)
        for i,word in enumerate(sent_list):
            if word not in word_dict:
                word_dict[word] = 1
            else:
                word_dict[word] +=1
            # 统计transdict bi-gram<word1,word2>
            word1,word2 = '',''
            # 如果是句首,则为<beg,word>
            if i == 0:
                word1,word2 = '<BEG>',word
            # 如果是句尾,则为<word,END>
            elif i == len(sent_list)-1:
                word1,word2 = word,'<END>'
            else:
                word1,word2 = word,sent_list[i+1]
            # 统计当前次后接词出现的次数
            if word not in transdict.keys():
                transdict[word1]={}
            if word2 not in transdict[word1]:
                transdict[word1][word2] =1
            else:
                transdict[word1][word2] +=1
    return word_dict,transdict
  

# 最大化概率2-gram分词
import math
word_dict = {}# 统计词频的概率
trans_dict = {}# 当前词后接词的概率
trans_dict_count = {}#记录转移词频
max_wordLength = 0# 词的最大长度
all_freq = 0 # 所有词的词频总和
train_data_path = "D:\workspace\project\NLPcase\ngram\data\train.txt"
from ngram import ngramTrain
word_dict_count,Trans_dict = ngramTrain.train(train_data_path)
all_freq = sum(word_dict_count)
max_wordLength = max([len(word) for word in word_dict_count.keys()])
for key in word_dict_count:
    word_dict[key] = math.log(word_dict_count[key]/all_freq)
# 计算转移概率
for pre_word,post_info in Trans_dict.items():
    for post_word,count in post_info:
        word_pair = pre_word+' '+post_word
        trans_dict_count[word_pair] = float(count)
        if pre_word in word_dict_count.keys():
            trans_dict[word_pair] = math.log(count/word_dict_count[pre_word])
        else:
            trans_dict[word_pair] = word_dict[post_word]

# 估算未出现词的概率,平滑算法
def get_unk_word_prob(word):
    return math.log(1.0/all_freq**len(word))
# 获取候选词的概率
def get_word_prob(word):
    if word in word_dict:
        prob = word_dict[word]
    else:
        prob = get_unk_word_prob(word)
    return prob
# 获取转移概率
def get_word_trans_prob(pre_word,post_word):
    trans_word = pre_word+" "+post_word
    if trans_word in trans_dict:
        trans_prob = math.log(trans_dict_count[trans_word]/word_dict_count[pre_word])
    else:
        trans_prob = get_word_prob(post_word)
    return trans_prob
# 寻找node的最佳前驱节点,方法为寻找所有可能的前驱片段
def get_best_pre_nodes(sent,node,node_state_list):
    # 如果node比最大词小,则取的片段长度的长度为限
    max_seg_length = min([node,max_wordLength])
    pre_node_list = []# 前驱节点列表

    # 获得所有的前驱片段,并记录累加概率
    for segment_length in range(1,max_seg_length+1):
        segment_start_node = node - segment_length
        segment = sent[segment_start_node:node]# 获取前驱片段
        pre_node = segment_start_node# 记录对应的前驱节点
        if pre_node == 0:
            # 如果前驱片段开始节点是序列的开始节点,则概率为<S>转移到当前的概率
            segment_prob = get_word_trans_prob("<BEG>",segment)
        else:# 如果不是序列的开始节点,则按照二元概率计算
            # 获得前驱片段的一个词
            pre_pre_node = node_state_list[pre_node]["pre_node"]
            pre_pre_word = sent[pre_pre_node:pre_node]
            segment_prob = get_word_trans_prob(pre_pre_word,segment)
        pre_node_prob_sum = node_state_list[pre_node]["prob_sum"]
        # 当前node一个候选的累加概率值
        candidate_prob_sum = pre_node_prob_sum+segment_prob
        pre_node_list.append((pre_node,candidate_prob_sum))
    # 找到最大的候选概率值
    (best_pre_node, best_prob_sum) = max(pre_node_list,key=lambda d:d[1])
    return best_pre_node,best_prob_sum
def cut(sent):
    sent = sent.strip()
    # 初始化
    node_state_list = []#主要是记录节点的最佳前驱,以及概率值总和
    ini_state = {}
    ini_state['pre_node'] = -1
    ini_state['prob_sum'] = 0 #当前概率总和
    node_state_list.append(ini_state)
    # 逐个节点的寻找最佳的前驱点
    for node in range(1,len(sent)+1):
        # 寻找最佳前驱,并记录当前最大的概率累加值
        (best_pre_node,best_prob_sum) = get_best_pre_nodes(sent,node,node_state_list)
        # 添加到队列
        cur_node ={}
        cur_node['pre_node'] = best_pre_node
        cur_node['prob_sum'] = best_prob_sum
        node_state_list.append(cur_node)
    # 获得最优路径,从后到前
    best_path = []
    node = len(sent)
    best_path.append(node)
    while True:
        pre_node = node_state_list[node]['pre_node']
        if pre_node ==-1:
            break
        node = pre_node
        best_path.append(node)
    # 构建词的切分
    word_list = []
    for i in range(len(best_path)-1):
        left = best_path[i]
        right = best_path[i+1]

        word = sent[left:right]
        word_list.append(word)
    return word_list

中文纠错模型扩展:

  • 基于BERT:以为CSC时是基于token(字符)级别的预测任务,输入输出序列长度一致,因此非常类似预训练语言模型的Masked Language Modeling(MLM),因此现如今绝大多数的方法是基于MLM实现的。

  • 多模态融合:上文提到CSC涉及到字音字形,因此有一些方法则是考虑如何将Word Embedding、Glyphic Embedding和Phonetic Embedding进行结合,因此涌现出一些多模态方法;

2.3 语言模型论文

以下本人总结了最近两年结合语言模型的OCR方向论文:

3.文本结构化

3.1 版面分析

概念及背景

随着OCR和版面分析处理技术不断发展,信息电子化的效率已经大幅度提高,人工智能已经能够将很多格式规整的资料轻松转化为可编辑的电子文稿;但生活中更多的内容资料往往是复杂的,具有字体样式多元、颜色丰富的特点,因此实现复杂版面的识别至关重要。

版面分析指的是对图片形式的文档进行区域划分,定位其中的关键区域,如文字、标题、表格、图片。因此,常见于文档识别应用中,是文档信息理解的重要步骤,可以对文档内的图像、文本、公式、表格信息和位置关系进行自动分析、识别和理解。

目前大多数都是用规则引擎来做,但随着业务量增大规则将不能满足需求,端到端版面分析模型也应运而生。但是在将文档图片输入到模型之前有研究者发现存在图像扭曲的现象,因此又出来了文档恢复这一方向,用于将扭曲的文档图像进行还原。

参考:PaddleOCR

3.2 NLP结构化

在得到文本识别的结果后,对于版面分析要求不高的业务应用,通常可以借助NLP相关工具来得到结构化信息。通常有以下两种做法:

(1)规则模板解析

以身份证识别为例,对于识别结果可以借助正则表达式来高效抽取"身份证号"、"性别"、"地址"这样的特征明显的信息。而且对于这样的信息,规则维护起来也很容易,很少会发生变化,不像"姓名"没有固定的特征表达形式。

(2)NLP信息抽取

同样以身份证识别为例,对于识别结果可以借助NLP信息抽取工具来得到结构化信息,如使用针对身份证信息训练的命名实体识别模型即可端到端的抽取出"姓名"、"性别"、"地址"等所有的信息。也可以借助分类模型来识别出各段文本对应的类型,随后将对应类型结果拼接,通过这种方式也能得到结构化信息。重点是如何灵活的运用NLP信息抽取工具了。

传统的大多数的都是用规则引擎来做,但随着业务量增大规则将不能满足需求,端到端的版面分析模型也应运而生。但是在将文档图片输入到模型之前有研究者发现存在图像扭曲的现象,因此又出来了文档恢复这一方向,用于将扭曲的文档图像进行还原。

4. 基于体检报告的版面分析实战

本项目为基于体检报告的版面分析实战,采用Paddle提供的PP-Structure框架,其是一个可用于复杂文档结构分析和处理的OCR工具包。

PP-Structure主要特性如下:

  • 支持对图片形式的文档进行版面分析,可以划分文字、标题、表格、图片以及列表5类区域(与Layout-Parser联合使用)
  • 支持文字、标题、图片以及列表区域提取为文字字段 支持表格区域进行结构化分析,最终结果输出Excel文件
  • 支持python whl包和命令行两种方式,简单易用
  • 支持版面分析和表格结构化两类任务自定义训练。

4.1.环境准备

1 相关环境安装

diff 复制代码
!pip install -U https://paddleocr.bj.bcebos.com/whl/layoutparser-0.0.0-py3-none-any.whl
!pip install "paddleocr>=2.2" --no-deps -r requirements.txt
!pip install PyMuPDF

2.引入PPStructure等工具库

python 复制代码
import datetime
import os
import fitz  # fitz就是pip install PyMuPDF
import cv2
import shutil
from paddleocr import PPStructure,draw_structure_result,save_structure_res

4.2.版面分析

版面分析对文档数据进行区域分类,其中包括版面分析工具的Python脚本使用、提取指定类别检测框、性能指标以及自定义训练版面分析模型。

ini 复制代码
import cv2
import layoutparser as lp
#image = cv2.imread('../20220623110401-0.png')
image = cv2.imread('../report_ex/pngs/20220623110401-2123.png')
image = image[..., ::-1]

# 加载模型
model = lp.PaddleDetectionLayoutModel(config_path="lp://PubLayNet/ppyolov2_r50vd_dcn_365e_publaynet/config",
                                threshold=0.5,
                                label_map={0: "Text", 1: "Title", 2: "List", 3:"Table", 4:"Figure"},
                                enforce_cpu=False,
                                enable_mkldnn=True)
# 检测
layout = model.detect(image)

# 显示结果
show_img = lp.draw_box(image, layout, box_width=3, show_element_type=True)
bash 复制代码
#切换路径
cd  /home/aistudio/PaddleOCR
css 复制代码
#最终版面分析效果,查看Table和Figure部分
show_img
xml 复制代码
<PIL.Image.Image image mode=RGB size=3168x4608 at 0x7FC69B6611D0>

4.3.表格识别

完成版面分析后,表格识别将表格图片转换为excel文档,其中包含对于表格文本的检测和识别以及对于表格结构和单元格坐标的预测。

ini 复制代码
!python -m pip install paddlepaddle==2.1.2


#转换预测结果文档(针对单个文件夹对应图片进行测试)
!pwd
table_engine = PPStructure(show_log=True)
save_folder = './result'
img_dir = './imgs'

files = os.listdir(img_dir)  
for fi in files:
    # 找到文件对应子目录
    # print(fi)
    fi_d = os.path.join(img_dir,fi)  
    # print(fi_d)  
    for img in os.listdir(fi_d):
        img_path = os.path.join(fi_d,img)
        img = cv2.imread(img_path)
        result = table_engine(img)
        # 保存在每张图片对应的子目录下
        save_structure_res(result, os.path.join(save_folder,fi),os.path.basename(img_path).split('.')[0])
ini 复制代码
/home/aistudio
[2022/08/28 22:57:53] ppocr DEBUG: Namespace(alpha=1.0, benchmark=False, beta=1.0, cls_batch_num=6, cls_image_shape='3, 48, 192', cls_model_dir=None, cls_thresh=0.9, cpu_threads=10, crop_res_save_dir='./output', det=True, det_algorithm='DB', det_db_box_thresh=0.6, det_db_score_mode='fast', det_db_thresh=0.3, det_db_unclip_ratio=1.5, det_east_cover_thresh=0.1, det_east_nms_thresh=0.2, det_east_score_thresh=0.8, det_fce_box_type='poly', det_limit_side_len=960, det_limit_type='max', det_model_dir='/home/aistudio/.paddleocr/whl/det/ch/ch_PP-OCRv3_det_infer', det_pse_box_thresh=0.85, det_pse_box_type='quad', det_pse_min_area=16, det_pse_scale=1, det_pse_thresh=0, det_sast_nms_thresh=0.2, det_sast_polygon=False, det_sast_score_thresh=0.5, draw_img_save_dir='./inference_results', drop_score=0.5, e2e_algorithm='PGNet', e2e_char_dict_path='./ppocr/utils/ic15_dict.txt', e2e_limit_side_len=768, e2e_limit_type='max', e2e_model_dir=None, e2e_pgnet_mode='fast', e2e_pgnet_score_thresh=0.5, e2e_pgnet_valid_set='totaltext', enable_mkldnn=False, fourier_degree=5, gpu_mem=500, help='==SUPPRESS==', image_dir=None, image_orientation=False, ir_optim=True, kie_algorithm='LayoutXLM', label_list=['0', '180'], lang='ch', layout=True, layout_dict_path='/opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages/paddleocr/ppocr/utils/dict/layout_dict/layout_cdla_dict.txt', layout_model_dir='/home/aistudio/.paddleocr/whl/layout/picodet_lcnet_x1_0_fgd_layout_cdla_infer', layout_nms_threshold=0.5, layout_score_threshold=0.5, max_batch_size=10, max_text_length=25, merge_no_span_structure=True, min_subgraph_size=15, mode='structure', ocr=True, ocr_order_method=None, ocr_version='PP-OCRv3', output='./output', precision='fp32', process_id=0, rec=True, rec_algorithm='SVTR_LCNet', rec_batch_num=6, rec_char_dict_path='/opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages/paddleocr/ppocr/utils/ppocr_keys_v1.txt', rec_image_shape='3, 48, 320', rec_model_dir='/home/aistudio/.paddleocr/whl/rec/ch/ch_PP-OCRv3_rec_infer', recovery=False, save_crop_res=False, save_log_path='./log_output/', save_pdf=False, scales=[8, 16, 32], ser_dict_path='../train_data/XFUND/class_list_xfun.txt', ser_model_dir=None, shape_info_filename=None, show_log=True, sr_batch_num=1, sr_image_shape='3, 32, 128', sr_model_dir=None, structure_version='PP-Structurev2', table=True, table_algorithm='TableAttn', table_char_dict_path='/opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages/paddleocr/ppocr/utils/dict/table_structure_dict_ch.txt', table_max_len=488, table_model_dir='/home/aistudio/.paddleocr/whl/table/ch_ppstructure_mobile_v2.0_SLANet_infer', total_process_num=1, type='ocr', use_angle_cls=False, use_dilation=False, use_gpu=True, use_mp=False, use_onnx=False, use_pdserving=False, use_space_char=True, use_tensorrt=False, use_xpu=False, vis_font_path='./doc/fonts/simfang.ttf', warmup=False)
[2022/08/28 22:57:56] ppocr DEBUG: dt_boxes num : 28, elapse : 0.02681589126586914
[2022/08/28 22:57:56] ppocr DEBUG: rec_res num  : 28, elapse : 0.048107147216796875
[2022/08/28 22:57:57] ppocr DEBUG: dt_boxes num : 47, elapse : 0.0371546745300293
[2022/08/28 22:57:57] ppocr DEBUG: rec_res num  : 47, elapse : 0.08329129219055176
[2022/08/28 22:57:57] ppocr DEBUG: dt_boxes num : 62, elapse : 0.045961618423461914
[2022/08/28 22:57:57] ppocr DEBUG: rec_res num  : 62, elapse : 0.10823178291320801
[2022/08/28 22:57:57] ppocr DEBUG: dt_boxes num : 1, elapse : 0.005411624908447266
[2022/08/28 22:57:57] ppocr DEBUG: rec_res num  : 1, elapse : 0.0036361217498779297
[2022/08/28 22:57:57] ppocr DEBUG: dt_boxes num : 1, elapse : 0.0047397613525390625
[2022/08/28 22:57:57] ppocr DEBUG: rec_res num  : 1, elapse : 0.003546476364135742
[2022/08/28 22:57:57] ppocr DEBUG: dt_boxes num : 1, elapse : 0.006383657455444336
[2022/08/28 22:57:57] ppocr DEBUG: rec_res num  : 1, elapse : 0.0035703182220458984
[2022/08/28 22:57:57] ppocr DEBUG: dt_boxes num : 1, elapse : 0.010457992553710938
[2022/08/28 22:57:57] ppocr DEBUG: rec_res num  : 1, elapse : 0.004181385040283203
[2022/08/28 22:57:57] ppocr DEBUG: dt_boxes num : 1, elapse : 0.011874675750732422
[2022/08/28 22:57:57] ppocr DEBUG: rec_res num  : 1, elapse : 0.009016752243041992
[2022/08/28 22:57:57] ppocr DEBUG: dt_boxes num : 1, elapse : 0.004208803176879883
[2022/08/28 22:57:57] ppocr DEBUG: rec_res num  : 1, elapse : 0.003774404525756836

In [ ]

yaml 复制代码
#查看识别结果
!tree result
yaml 复制代码
result
└── 1
    └── 20220623110401-0
        ├── [136, 1142, 3033, 2449]_0.xlsx
        ├── [138, 2167, 3040, 3259]_0.xlsx
        ├── [140, 392, 3032, 1056]_0.xlsx
        └── res_0.txt

查看上述导出其中之一的EXCEL文档

参考及推荐资料:

后续章节将对上述部分进一步介绍,结合现有工具重点对表格识别、关键信息抽取等方向进行详细介绍,敬请期待!

相关推荐
LNTON羚通12 分钟前
摄像机视频分析软件下载LiteAIServer视频智能分析平台玩手机打电话检测算法技术的实现
算法·目标检测·音视频·监控·视频监控
哭泣的眼泪4082 小时前
解析粗糙度仪在工业制造及材料科学和建筑工程领域的重要性
python·算法·django·virtualenv·pygame
Microsoft Word2 小时前
c++基础语法
开发语言·c++·算法
天才在此3 小时前
汽车加油行驶问题-动态规划算法(已在洛谷AC)
算法·动态规划
莫叫石榴姐3 小时前
数据科学与SQL:组距分组分析 | 区间分布问题
大数据·人工智能·sql·深度学习·算法·机器学习·数据挖掘
茶猫_4 小时前
力扣面试题 - 25 二进制数转字符串
c语言·算法·leetcode·职场和发展
肥猪猪爸6 小时前
使用卡尔曼滤波器估计pybullet中的机器人位置
数据结构·人工智能·python·算法·机器人·卡尔曼滤波·pybullet
readmancynn7 小时前
二分基本实现
数据结构·算法
萝卜兽编程7 小时前
优先级队列
c++·算法
盼海7 小时前
排序算法(四)--快速排序
数据结构·算法·排序算法