Skip to content

Latest commit

 

History

History
334 lines (208 loc) · 15.9 KB

README.md

File metadata and controls

334 lines (208 loc) · 15.9 KB

GapTree_Sort 间隙·树·排序法

hiroi-sora 个人开发的,基于文本位置的版面分析/文本排序算法。适用于将OCR得到的文本块,按人类阅读顺序进行排序。特别针对多栏布局的报刊型排版。可能也适用于PDF解析等依赖版面分析的领域。

已内置于 Umi-OCR ,为文档识别等功能提供后处理支持。

演示效果:请查看下方大图。图中从左到右有四个部分:

  1. 原始OCR结果,存在一些错误排序,特别是无法区分不同列。
  2. 经过本算法,大部分文本块得以正确排序。
  3. 算法找出的竖切线(间隙)。
  4. 算法找出的区块,顺序为布局树的前序遍历。

背景 - 文本块顺序

“文本块”指的是包含空间坐标的一行文本的信息。如:

{
    "bbox": (x0, y0, x1, y1), # 左上角、右下角坐标
    "text": "这是一行文本",
}

通过OCR(光学字符识别)可以从图片中识别出大量文本块。但是OCR原始输出的文本块列表,往往只是按简单的规则进行排序(从上到下)。如果图片中存在多列文本,则在OCR结果中,不同列的块会交叉混杂在一起,与人类阅读顺序不符。

PDF解析也有类似的问题。PDF是固定布局,所有元素(如图片、表格、文本块)的位置被嵌入到文档中。同一段落之内,不同的行往往被分为不同的文本块。这使得难以编辑或提取连续的文本流,或者提取的文本块存在顺序错乱的情况。

因此,我们需要一种版面分析手段,将离散的文本块重新组织回 行、列、小节 的版面结构。或者至少按正确的顺序串联起文本块,将其恢复为连续的文本流。

背景 - 版面分析

版面分析是指对文档页面中的内容进行结构化分析的过程,其目的是识别和分类页面上的不同区域,如标题、列、行、图像、表格等。通过版面分析,我们能得到文本块之间的关联,从而进行排序或排版重建。

目前已有的版面分析技术,主要分为 图像分析位置分析

图像分析

先对文档的图片进行版面分析,将原始图片拆分为 [标题, 正文, 表格] 不同区块,再对每个区块进行提取文字。

可能包括使用边缘检测、纹理分析、连通区域分析、颜色分析等方法来区分文本和非文本元素,以及进一步识别出标题、段落、图表、图片等不同类型的内容区块。

现有基于图像的版面分析库,往往结合了深度学习,如 PP-Structure

优点:

  • 经过充分训练的模型库,可以很好的理解图片中各个区块的逻辑关系,准确度高。
  • 可以获取不同区块的类型(标题、正文、表格)。

缺点:

  • 训练成本高:需要大量标注样本。
  • 使用成本高:神经网络推理的性能开销较高,模型库、推理库占用空间大。
  • 输入必须为图片。如果手上没有文档图片,只有文本块,则无法使用该方案。

位置分析

先通过OCR或PDF,得到一组文本块。再根据文本块的包围盒位置信息,分析出布局或顺序。

一些PDF解析库,涉及了基于元素位置的规则匹配算法。如 pdf2docx

优点:

  • 使用成本低:开销远比图像处理要小。
  • 兼容性强:可以用在不同的任务流程中,比如不同的OCR组件、PDF提取等。

缺点:

  • 如果没有额外信息,难以从位置中推断区块类型。
  • 程序难以理解各个区块的逻辑关系,准确度低,或者容易出现误划分的情况。

GapTree_Sort 间隙·树·排序法

这是一种对文本块位置进行规则匹配的版面分析算法。

简而言之:它搜索每一水平线上的文本块间隙,拼接为竖切线,将页面切割为不同的区块,将区块组织为布局树。最后,前序遍历布局树,即可得到符合人类阅读习惯的文本块排序

优点:

  • 支持 任意列数。
  • 支持 列宽不一致。
  • 支持 跨列区块。(如横跨左右两列的标题行)
  • 在使用预处理器时,支持图片按任意角度旋转,支持竖排(视为90°旋转)。
  • 参数极少,仅需提供文本块位置即可,无需额外的信息。
  • 没有超参数(需要人工设定的阈值等),可自适应不同的情况。
  • 对于区块数较少的常见布局,如1 ~ 2列:此时理想情况下能够接近线性时间复杂度O(n)。n为总文本块数。本文后面有证明。
  • 鲁棒性强,即使因为噪声(OCR误划分)导致部分区块的排序错误,也能保证大部分文本块的排序是正确的。
  • 实验代码gap_tree.py中,仅使用了Python标准库,可以方便的用其它语言重写。

限制:

  • 目前仅适用于标准横排或标准竖排阅读习惯,即:横排从左到右、从上到下;竖排从上到下、从右到左。
  • 无法判断区块的类型,所有元素均视为正文。
  • 算法中仅有“行”和“列”(即区块)的排版成分,无法辨别“章节”的成分。(章节指水平方向上多个列的组合)。如果章节存在跨列的标题行,则算法可以构建正确的布局树;否则可能构建出错。
  • 同理,算法无法辨别表格。表格会被当成多个列来处理。
  • 对于非标准排版(尤其是交错列,上下方的列不对齐),布局树构建错误的几率高。

不过,算法考虑到了即使在布局树构建不合理的情况下,排序依然有很大几率是正确的。原理

测试代码

git clone 本仓库。

test.py 为测试入口,其中的 test_image 为测试图片路径。本仓库提供了一些测试样例(包括原图片和OCR结果json),可以直接使用。

  • 如果你想用自己的样例,需要导入 RapidOCR-json ,或者用其它方式获取文本块。结果可视化工具可能不兼容你的块格式,如有报错请留意。

gap_tree.py 为主要算法模块。

preprocessing.py 是预处理器,让算法能够支持旋转、竖排等情况。

paragraph_parse.py 是一个简单的段内分析器。OCR后处理步骤之一,但不属于主要算法的范畴。

visualize.py 为结果可视化工具,需要Pillow库。

算法说明

我用一个简单的案例来介绍本算法。实际的算法流程中,可能与下方步骤略有不同。实际可能将多个步骤压缩为一个以提高性能。

假设对一个带标题栏的双列布局图片进行OCR,拿到了原始结果:

               ① 设计模式在团队中的作用                |
                                                     |
       ② 从设计模式和面向     ③ 计,可以避免你的团队    |
   ④ 对象原则的视角讨论设     ⑤ 很快陷入实现的细节。    |

显然,文本块 ①~⑤ 只是按从上到下排序,不符合双栏布局的顺序。

第1步:划分行

将文本块按从上到下的顺序进行排序。(考虑到OCR结果本身已经从上到下排序,此步骤只需接近 O(n) 的时间开销)

然后从上到下遍历所有文本块,划分出不同行。划分依据可以是 两个文本块的水平投影有重叠。

注,划分行的策略,对总体结果有一定影响。目前实验代码中的划分策略可能不是最优的,待后续优化。

如下,划分出三行。【-->】

① ---------------> 设计模式在团队中的作用             |
                                                    |
② -------> 从设计模式和面向 ---> 计,可以避免你的团队  |
③ ---> 对象原则的视角讨论设 ---> 很快陷入实现的细节。  | 
第2步:求间隙

对于每一行,视为一个一维数轴,文本块是其中的线段。

求该行的“间隙”,即没有被线段覆盖到的地方。【==】

================设计模式在团队中的作用===============|
                                                   |
========从设计模式和面向=====计,可以避免你的团队=====|
====对象原则的视角讨论设======很快陷入实现的细节。====|
第3步:求竖切线

对当前在考虑的每一个间隙,检查与下一行的间隙的线段交集。

如果下一行的间隙与当前间隙不完全一致,那么要更新当前间隙,包括缩短、分裂、结束 等。例如:

当前间隙:| =================         ====  |
下行间隙:|    -----     ---------          |
更新后:  |    =====     ====               |
说明:       ↑缩短    ↑分裂            ↑结束

记录经过每一行更新的间隙,我们可以得到“竖切线”。一条竖切线由多个连续行的、同一位置的间隙所组成,能“切分”不同列。【##】

####            设计模式在团队中的作用           ##|
####                                           ##|
####    从设计模式和面向#####计,可以避免你的团队 ##|
####对象原则的视角讨论设##### 很快陷入实现的细节。##|
第4步:求区块

根据所有竖切线,我们再次遍历每一行,将文本块划分到不同区块中。

每个区块可包含多个行的文本块。划分完成后,对每个区块内的文本块,从上到下进行排序。

如下,划分出 A B C 三个区块

#### AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA ##|
#### AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA ##|
#### BBBBBBBBBBBBBBBB ##### CCCCCCCCCCCCCCCCCC ##|
#### BBBBBBBBBBBBBBBB ##### CCCCCCCCCCCCCCCCCC ##|
第5步:生成布局树

将每个区块,作为树的一个节点。我们有了很多个节点,下面将这些节点连接成树。

遍历每个节点A,找父节点F,规则为:

  1. A 的右边界,必须包含在 F 的左右边界之内。
  2. A 顶部,低于 F 的底部。
  3. F 与 A 的垂直距离(行数)最近。
  4. 可能有多个F满足3的条件(底部在同一行),取最右的一个F作为父节点。
  5. 如果没有节点满足上述,则 A 的父节点为根。

节点均连接到父节点后,我们再遍历一次所有节点,将它的子节点按从左到右的顺序排序。

例如:

#### AAAAAAAAAAAAAAA【父:根】AAAAAAAAAAAAAAAAA ##|
#### AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA ##|
#### BBBB【父:A】BBBB ##### CCCCC【父:A】CCCC ##|
#### BBBBBBBBBBBBBBBB ##### CCCCCCCCCCCCCCCCCC ##|
第6步:前序遍历布局树

对于上述样例的树,前序遍历得到的节点序列为: L = [A, B, C]

第7步:整理文本块排序

遍历节点序列L,对每个序列,从上到下输出文本块,顺序为:

               ① 设计模式在团队中的作用                |
                                                     |
       ② 从设计模式和面向     ④ 计,可以避免你的团队    |
   ③ 对象原则的视角讨论设     ⑤ 很快陷入实现的细节。    |

显然,排序后的文本块 ①~⑤ 符合双栏布局的阅读顺序。

对于多栏布局或更复杂的非固定列布局,本算法也有较好的效果。

算法原理浅析

本算法是根据本人经验制定的一套规则,所以可能不是数学意义上的最优策略。但是,算法的部分环节仍然能解释其优越性。

为什么要求竖切线:

对于多列布局,列(或者说,区块)的左右两侧必然存在空隙,区块的内部必然不存在空隙(不然就是两列了)。

竖切线的作用有两个:其一,从水平方向上约束一个区块的左右边缘。其二,从垂直方向上约束区块的上下边缘(上下边缘必然伴随着某条竖切线的开始或结束)。

因此,仅通过查找竖切线,无需其它信息,我们就能判断所有区块的上下左右边缘。

生成布局树时,找父节点F的规则的含义

再看一遍规则:

  1. 子节点 A 的右边界,必须包含在父节点 F 的左右边界之内。
  2. A 顶部,低于 F 的底部。
  3. F 与 A 的垂直距离(行数)最近。
  4. 可能有多个F满足3的条件(底部在同一行),取最右的一个F作为父节点。
  5. 如果没有节点满足上述,则 A 的父节点为根。

按照人类阅读顺序,我们在读完一个列F后,首先向右寻找下一个列A。如果右侧存在列A,说明 F、A 是平行关系,属于同一个“章节”。因此,在布局树中,它们是兄弟节点,F不可能是A的父节点。

如果列F的右侧没有列,说明读完了当前章节,需要跳到下方,去下一个章节寻找新列。此时 F 可以下一个章节的列 A 的父节点。下一个章节中的列 A1,A2... 也是兄弟关系。将它们挂到F的子节点下,可以在前序遍历时,保证它们的顺序都晚于父节点F。

同时,对于 F→A1, F→A2... 来说,F永远在A的上方最右侧

前序遍历带来的鲁棒性优势

部分情况下,布局树的节点可能位置不正确。比如:构建布局树中,节点B搜索父节点时,误没有匹配到应有的父节点A,而向上匹配到祖先节点F(因为匹配规则是向上兼容的)。如下所示。

        F              |      F
      ↙               |    ↙  ↘
    A                  |   A     B
 ↙                    |
B   正确匹配到父节点A   |  错误,匹配到祖先节点F

但是,节点总体的顺序是正确的。与树的性质有关:上述两棵树的前序遍历顺序相同,都是F→A→B,因此B挂到A下 或挂到F下 是不会改变顺序的。

如果 B 节点下面也有子树,那么子树在全局的顺序也正确。

这个性质为本算法提供了很强的鲁棒性。即使OCR文本块划分出错,比方说没有识别到破折号,而将一行文本错误的划分为两行。这会导致布局树构建出错,原本的一个区块被划分为2 ~ 4个。但是这不会影响输出顺序,因为划分后的2 ~ 4个区块,在前序遍历中是紧密相连的,不会被别的区块所隔开。从顺序上看,划分后的2 ~ 4个区块依然表现为“1个区块”。

时间复杂度分析

设文本块数量为n。通过下述的分析,我们得知:

极端情况下(每个文本块是一个单独节点),最坏时间复杂度为 O(n^2) 。

对于常见布局,时间复杂度为 O(n) 。

第1步:划分行

排序消耗 O(nlogn) 。考虑到OCR文本本身从上到下有序,则排序仅消耗 O(n) 检查一遍。

遍历文本块划分行,消耗 O(n) 。

第2步:求间隙

主要开销是遍历所有行的单层循环。

假设有m行(必然有m<=n),则遍历所有行的开销 <= O(n) 。

第3步:求竖切线

这一步要遍历行及所有间隙。假设所有间隙数量为k(k<n)。

不管嵌套了多少层循环,总共考察了m行和k个间隙(不重复考察),总体复杂度 O(n) 。

实际上,第2、3步可以合并为一步。

第4步:求区块

遍历所有行的开销 <= O(n) 。

第5步:生成布局树

假设节点数为s(s<=n)。

两层循环,依次考察所有的节点对。最坏情况下s=n(每个文本块都是一个单独节点),复杂度为 O(n^2) 。

但是对于常见布局,我们可以认为节点数 s 是常数。比如对于标准双栏布局,不管n多大,布局树中都只有根节点+左右2个子节点。因此平均复杂度为 O(1) 。

第6步:前序遍历布局树

同上,最坏为O(n),平均为 O(1) 。

第7步:整理文本块排序

与第1步类似,由于每个节点内的文本块基本有序(从上到下),因此排序最坏消耗 O(nlogn) ,平均消耗 O(n) 。

End.