An open API service indexing awesome lists of open source software.

https://github.com/hiroi-sora/gaptree_sort_algorithm

【间隙·树·排序算法】 对OCR结果或PDF提取的文本进行版面分析,按人类阅读顺序进行排序。
https://github.com/hiroi-sora/gaptree_sort_algorithm

Last synced: 14 days ago
JSON representation

【间隙·树·排序算法】 对OCR结果或PDF提取的文本进行版面分析,按人类阅读顺序进行排序。

Awesome Lists containing this project

README

        

# GapTree_Sort 间隙·树·排序法

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

已内置于 [Umi-OCR](https://github.com/hiroi-sora/Umi-OCR) ,为文档识别等功能提供后处理支持。

演示效果:请查看下方大图。图中从左到右有四个部分:
1. 原始OCR结果,存在一些错误排序,特别是无法区分不同列。
2. 经过本算法,大部分文本块得以正确排序。
3. 算法找出的竖切线(间隙)。
4. 算法找出的区块,顺序为布局树的前序遍历。

![](test/readme_img_1.png)

## 背景 - 文本块顺序

“文本块”指的是包含空间坐标的一行文本的信息。如:
```
{
"bbox": (x0, y0, x1, y1), # 左上角、右下角坐标
"text": "这是一行文本",
}
```

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

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

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

## 背景 - 版面分析

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

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

### 图像分析

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

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

现有基于图像的版面分析库,往往结合了深度学习,如 [PP-Structure](https://github.com/PaddlePaddle/PaddleOCR/tree/release/2.7/ppstructure) 。

优点:

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

缺点:

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

### 位置分析

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

一些PDF解析库,涉及了基于元素位置的规则匹配算法。如 [pdf2docx](https://github.com/ArtifexSoftware/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](https://github.com/hiroi-sora/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