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提取的文本进行版面分析,按人类阅读顺序进行排序。
- Host: GitHub
- URL: https://github.com/hiroi-sora/gaptree_sort_algorithm
- Owner: hiroi-sora
- License: mit
- Created: 2024-01-22T09:23:51.000Z (over 1 year ago)
- Default Branch: main
- Last Pushed: 2024-02-28T16:08:26.000Z (about 1 year ago)
- Last Synced: 2025-03-25T20:15:38.266Z (about 1 month ago)
- Language: Python
- Homepage:
- Size: 5.87 MB
- Stars: 125
- Watchers: 5
- Forks: 17
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
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. 算法找出的区块,顺序为布局树的前序遍历。
## 背景 - 文本块顺序
“文本块”指的是包含空间坐标的一行文本的信息。如:
```
{
"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