docs: 添加涉密文件自检工具实施计划
This commit is contained in:
330
UmiOCR-data/py_src/ocr/tbpu/parser_tools/gap_tree.py
Normal file
330
UmiOCR-data/py_src/ocr/tbpu/parser_tools/gap_tree.py
Normal file
@@ -0,0 +1,330 @@
|
||||
# 【间隙·树·排序算法】 GapTree_Sort_Algorithm
|
||||
# 对OCR结果或PDF提取的文本进行版面分析,按人类阅读顺序进行排序。
|
||||
# Author: hiroi-sora
|
||||
# https://github.com/hiroi-sora/GapTree_Sort_Algorithm
|
||||
|
||||
from typing import Callable
|
||||
|
||||
|
||||
class GapTree:
|
||||
def __init__(self, get_bbox: Callable):
|
||||
"""
|
||||
:param get_bbox: 函数,传入单个文本块,
|
||||
返回该文本块左上角、右下角的坐标元组 (x0, y0, x1, y1)
|
||||
"""
|
||||
self.get_bbox = get_bbox
|
||||
|
||||
# ======================= 调用接口 =====================
|
||||
# 对文本块列表排序
|
||||
def sort(self, text_blocks: list):
|
||||
"""
|
||||
对文本块列表,按人类阅读顺序进行排序。
|
||||
|
||||
:param text_blocks: 文本块对象列表
|
||||
:return: 排序后的文本块列表
|
||||
"""
|
||||
|
||||
# 封装块单元,并求页面左右边缘
|
||||
units, page_l, page_r = self._get_units(text_blocks, self.get_bbox)
|
||||
# 求行和竖切线
|
||||
cuts, rows = self._get_cuts_rows(units, page_l, page_r)
|
||||
# 求布局树
|
||||
root = self._get_layout_tree(cuts, rows)
|
||||
# 求树节点序列
|
||||
nodes = self._preorder_traversal(root)
|
||||
# 求排序后的 原始文本块序列
|
||||
new_text_blocks = self._get_text_blocks(nodes)
|
||||
|
||||
# 测试:缓存中间变量,以便调试输出
|
||||
self.current_rows = rows
|
||||
self.current_cuts = cuts
|
||||
self.current_nodes = nodes
|
||||
|
||||
return new_text_blocks
|
||||
|
||||
# 获取以区块为单位的文本块二层列表
|
||||
def get_nodes_text_blocks(self):
|
||||
"""
|
||||
获取以区块为单位的文本块二层列表。需要在 sort 后调用。
|
||||
|
||||
:return: [ [区块1的text_blocks], [区块2的text_blocks]... ]
|
||||
"""
|
||||
result = []
|
||||
for node in self.current_nodes:
|
||||
tbs = []
|
||||
if node["units"]:
|
||||
for unit in node["units"]:
|
||||
tbs.append(unit[1])
|
||||
result.append(tbs)
|
||||
return result
|
||||
|
||||
# ======================= 封装块单元列表 =====================
|
||||
# 将原始文本块,封装为 ( (x0,y0,x2,y2), 原始 ) 。并检查页边界。
|
||||
def _get_units(self, text_blocks, get_bbox):
|
||||
# 封装单元列表 units [ ( (x0,y0,x2,y2), 原始文本块 ), ... ]
|
||||
units = []
|
||||
page_l, page_r = float("inf"), -1 # 记录文本块的左右最值,作为页边界
|
||||
for tb in text_blocks:
|
||||
x0, y0, x2, y2 = get_bbox(tb)
|
||||
units.append(((x0, y0, x2, y2), tb))
|
||||
if x0 < page_l:
|
||||
page_l = x0
|
||||
if x2 > page_r:
|
||||
page_r = x2
|
||||
units.sort(key=lambda a: a[0][1]) # 按顶部从上到下排序
|
||||
return units, page_l, page_r
|
||||
|
||||
# ======================= 求行和竖切线 =====================
|
||||
"""
|
||||
扫描所有文本块,获取所有行和竖切线。
|
||||
一个行,由一组垂直位置接近的文本块所组成。
|
||||
一条竖切线,由多个连续行中,同一位置的间隙所组成。间隙划分同一行中不同列的文本块。
|
||||
输入:一个页面上的文本块单元列表 units=[ ( (x0,y0,x2,y2), _ ) ] 。必须按上到下排序。
|
||||
返回:
|
||||
竖切线列表 cuts=[ ( 左边缘x, 右边缘x, 起始行号, 结束行号 ) ] 。从左到右排序
|
||||
页面上的行 rows=[ [unit...] ] 。从上到下,从左到右排序
|
||||
"""
|
||||
|
||||
def _get_cuts_rows(self, units, page_l, page_r):
|
||||
# 使用间隙组 gaps2 更新 gaps1 。返回: 更新后的gaps1 , gaps1中被移除的间隙
|
||||
def update_gaps(gaps1, gaps2):
|
||||
flags1 = [True for _ in gaps1] # gaps1[i] 是否彻底移除
|
||||
flags2 = [True for _ in gaps2] # gaps2[i] 是否新加入
|
||||
new_gaps1 = []
|
||||
for i1, g1 in enumerate(gaps1):
|
||||
l1, r1, _ = g1
|
||||
for i2, g2 in enumerate(gaps2): # 对每一个gap1,考察所有gap2
|
||||
l2, r2, _ = g2
|
||||
# 计算交集的起点和终点
|
||||
inter_l = max(l1, l2)
|
||||
inter_r = min(r1, r2)
|
||||
# 如果交集有效
|
||||
if inter_l <= inter_r:
|
||||
# 更新 gap1 左右边缘
|
||||
new_gaps1.append((inter_l, inter_r, g1[2]))
|
||||
flags1[i1] = False # 旧的 gap1 不应移除
|
||||
flags2[i2] = False # 新的 gap2 不应添加
|
||||
# gap2 新加入
|
||||
for i2, f2 in enumerate(flags2):
|
||||
if f2:
|
||||
new_gaps1.append(gaps2[i2])
|
||||
# 记录 gaps1 彻底移除的项
|
||||
del_gaps1 = []
|
||||
for i1, f1 in enumerate(flags1):
|
||||
if f1:
|
||||
del_gaps1.append(gaps1[i1])
|
||||
|
||||
return new_gaps1, del_gaps1
|
||||
|
||||
# ========================================
|
||||
|
||||
page_l -= 1 # 保证页面左右边缘不与文本块重叠
|
||||
page_r += 1
|
||||
# 存放所有行。“row”指同一水平线上的单元块(可能属于多列)。 [ [unit...] ]
|
||||
rows = []
|
||||
# 已生成完毕的竖切线。[ ( 左边缘x, 右边缘x , 起始行号, 结束行号 ) ]
|
||||
completed_cuts = []
|
||||
# 考察中的间隙。 [ (左边缘x, 右边缘x , 开始行号) ]
|
||||
gaps = []
|
||||
row_index = 0 # 当前行号
|
||||
unit_index = 0 # 当前块号
|
||||
# 从上到下遍历所有文本行
|
||||
l_units = len(units)
|
||||
while unit_index < l_units:
|
||||
# ========== 查找当前行 row ==========
|
||||
unit = units[unit_index] # 当前行最顶部的块
|
||||
u_bottom = unit[0][3]
|
||||
row = [unit] # 当前行
|
||||
# 查找当前行的剩余块
|
||||
for i in range(unit_index + 1, len(units)):
|
||||
next_u = units[i]
|
||||
next_top = next_u[0][1]
|
||||
if next_top > u_bottom:
|
||||
break # 下一块的顶部超过当前底部,结束本行
|
||||
row.append(next_u) # 当前行添加块
|
||||
unit_index = i # 步进 已遍历的块序号
|
||||
# ========== 查找当前行的间隙 row_gaps ==========
|
||||
row.sort(key=lambda x: (x[0][0], x[0][2])) # 当前行中的块 从左到右排序
|
||||
row_gaps = [] # 当前行的间隙 [ ( ( 左边缘l, 右边缘r ), 开始行号) ]
|
||||
search_start = page_l # 本轮搜索的线段起始点为页面左边缘
|
||||
for u in row: # 遍历当前行的块
|
||||
l = u[0][0] # 块左侧
|
||||
r = u[0][2] # 块右侧
|
||||
# 若块起始点大于搜索起始点,那么将这部分加入到结果
|
||||
if l > search_start:
|
||||
row_gaps.append((search_start, l, row_index))
|
||||
# 若块结束点大于搜索起始点,更新搜索起始点
|
||||
if r > search_start:
|
||||
search_start = r
|
||||
# 页面右边缘 加入最后一个间隙
|
||||
row_gaps.append((search_start, page_r, row_index))
|
||||
# ========== 更新考察中的间隙组 ==========
|
||||
gaps, del_gaps = update_gaps(gaps, row_gaps)
|
||||
# gaps 中被移除的项,加入生成完毕的竖切线 completed_cuts
|
||||
row_max = row_index - 1 # 竖切线结束行号
|
||||
for dg1 in del_gaps:
|
||||
completed_cuts.append((*dg1, row_max))
|
||||
# ========== End ==========
|
||||
rows.append(row) # 总行列表添加当前行
|
||||
unit_index += 1
|
||||
row_index += 1
|
||||
# 遍历结束,收集 gaps 中剩余的间隙,组成延伸到最后一行的竖切线
|
||||
row_max = len(rows) - 1 # 竖切线结束行号
|
||||
for g in gaps:
|
||||
completed_cuts.append((*g, row_max))
|
||||
completed_cuts.sort(key=lambda c: c[0])
|
||||
return completed_cuts, rows
|
||||
|
||||
# ======================= 求布局树 =====================
|
||||
"""
|
||||
一个布局树节点表示一个区块。定义:
|
||||
node = {
|
||||
"x_left": 节点左边缘x,
|
||||
"x_right": 右边缘x,
|
||||
"r_top": 顶部的行号,
|
||||
"r_bottom": 底部的行号,
|
||||
"units": [], # 节点内部的文本块列表(除了根节点为空,其它节点非空)
|
||||
"children": [], # 子节点,有序
|
||||
}
|
||||
"""
|
||||
|
||||
def _get_layout_tree(self, cuts, rows):
|
||||
# 竖切线,将一个横行切开,断开的区域为“间隙”。
|
||||
# 生成每一行对应的间隙 (左侧,右侧) 坐标列表
|
||||
rows_gaps = [[] for _ in rows]
|
||||
for g_i, cut in enumerate(cuts):
|
||||
for r_i in range(cut[2], cut[3] + 1):
|
||||
rows_gaps[r_i].append((cut[0], cut[1]))
|
||||
|
||||
root = { # 根节点
|
||||
"x_left": cuts[0][0] - 1,
|
||||
"x_right": cuts[-1][1] + 1,
|
||||
"r_top": -1,
|
||||
"r_bottom": -1,
|
||||
"units": [],
|
||||
"children": [],
|
||||
}
|
||||
completed_nodes = [root] # 已经完成结束的节点
|
||||
now_nodes = [] # 当前正在考虑的节点。无顺序
|
||||
|
||||
# ========== 结束一个节点,加入节点树 ==========
|
||||
def complete(node):
|
||||
node_r = node["x_right"] - 2 # 当前节点右边界
|
||||
max_nodes = [] # 符合父节点条件的,最低的完成节点列表
|
||||
max_r = -2 # 符合父节点条件的最低行数
|
||||
# 在完成列表中,寻找父节点
|
||||
for com_node in completed_nodes:
|
||||
# 父节点的垂直投影必须包含当前右界
|
||||
if node_r < com_node["x_left"] or node_r > com_node["x_right"] + 0.0001:
|
||||
continue
|
||||
# 父节点底部必须在当前之上
|
||||
if com_node["r_bottom"] >= node["r_top"]:
|
||||
continue
|
||||
# 遇到更低的符合条件节点
|
||||
if com_node["r_bottom"] > max_r:
|
||||
max_r = com_node["r_bottom"]
|
||||
max_nodes = [com_node]
|
||||
continue
|
||||
# 遇到同样低的符合条件节点
|
||||
if com_node["r_bottom"] == max_r:
|
||||
max_nodes.append(com_node)
|
||||
continue
|
||||
# 在最低列表中,寻找最右的节点作为父节点
|
||||
max_node = max(max_nodes, key=lambda n: n["x_right"])
|
||||
max_node["children"].append(node) # 加入父节点
|
||||
completed_nodes.append(node) # 加入完成列表
|
||||
|
||||
# ========== 遍历每行,更新节点树 ==========
|
||||
for r_i, row in enumerate(rows):
|
||||
row_gaps = rows_gaps[r_i] # 当前行的间隙组
|
||||
u_i = g_i = 0 # 当前考察的 文本块、间隙下标
|
||||
|
||||
# ========== 检查是否有正在考虑的节点 可以结束 ==========
|
||||
new_nodes = []
|
||||
for node in now_nodes: # 遍历节点
|
||||
l_flag = r_flag = False # 标记节点左右边缘是否延续
|
||||
completed_flag = False # 标记节点是否可以结束
|
||||
x_left = node["x_left"] # 左右边缘坐标
|
||||
x_right = node["x_right"]
|
||||
for gap in row_gaps: # 遍历该行所有间隙
|
||||
if gap[1] == x_left: # 节点左边缘被间隙右侧延续
|
||||
l_flag = True
|
||||
if gap[0] == x_right: # 右边缘被间隙左侧延续
|
||||
r_flag = True
|
||||
# 任意间隙在本节点下方,打断本节点
|
||||
if x_left < gap[0] < x_right or x_left < gap[1] < x_right:
|
||||
completed_flag = True
|
||||
break
|
||||
if not l_flag or not r_flag: # 左右任意一个边缘无法延续
|
||||
completed_flag = True
|
||||
if completed_flag: # 节点结束,加入节点树
|
||||
complete(node)
|
||||
else: # 节点继续
|
||||
node["r_bottom"] = r_i
|
||||
new_nodes.append(node)
|
||||
now_nodes = new_nodes
|
||||
|
||||
# ========== 从左到右遍历,将文本块加入对应列的节点 ==========
|
||||
while u_i < len(row):
|
||||
unit = row[u_i] # 当前块
|
||||
# ========== 当前块 unit 位于间隙 g_i 与 g_i+1 之间的区间 ==========
|
||||
x_l = row_gaps[g_i][1] # 左间隙 g_i 的右边界
|
||||
x_r = row_gaps[g_i + 1][0] # 右间隙 g_i+1 的左边界
|
||||
# 检查区间是否正确
|
||||
if unit[0][0] + 0.0001 > x_r: # 块比右间隙更右,说明到了下一个区间
|
||||
g_i += 1 # 间隙步进,块不步进
|
||||
continue
|
||||
# ========== 检查当前块可否加入已有的节点 ==========
|
||||
flag = False
|
||||
for node in now_nodes:
|
||||
# 若某个节点的左右侧坐标,与当前块一致,则当前块加入节点
|
||||
if node["x_left"] == x_l and node["x_right"] == x_r:
|
||||
node["units"].append(unit)
|
||||
flag = True
|
||||
break
|
||||
if flag:
|
||||
u_i += 1 # 块步进
|
||||
continue
|
||||
# ========== 根据当前块创建新的节点,加入待考虑节点 ==========
|
||||
now_nodes.append(
|
||||
{
|
||||
"x_left": x_l,
|
||||
"x_right": x_r,
|
||||
"r_top": r_i,
|
||||
"r_bottom": r_i,
|
||||
"units": [unit],
|
||||
"children": [],
|
||||
}
|
||||
)
|
||||
u_i += 1 # 块步进
|
||||
# 将剩余节点也加入节点树
|
||||
for node in now_nodes:
|
||||
complete(node)
|
||||
# 整理所有节点
|
||||
for node in completed_nodes:
|
||||
# 所有子节点 按从左到右排序
|
||||
node["children"].sort(key=lambda n: n["x_left"])
|
||||
# 所有块单元 按从上到下排序
|
||||
node["units"].sort(key=lambda u: u[0][1])
|
||||
return root
|
||||
|
||||
# ======================= 前序遍历布局树,求节点序列 =====================
|
||||
def _preorder_traversal(self, root):
|
||||
if not root:
|
||||
return []
|
||||
stack = [root]
|
||||
result = []
|
||||
while stack:
|
||||
node = stack.pop()
|
||||
result.append(node)
|
||||
# 将当前节点的子节点逆序压入栈中,以保证左子节点先于右子节点处理
|
||||
stack += reversed(node["children"])
|
||||
return result
|
||||
|
||||
# ======================= 从节点序列中,提取原始文本块序列 =====================
|
||||
def _get_text_blocks(self, nodes):
|
||||
result = []
|
||||
for node in nodes:
|
||||
for unit in node["units"]:
|
||||
result.append(unit[1])
|
||||
return result
|
||||
@@ -0,0 +1,98 @@
|
||||
# =========================================
|
||||
# =============== 按行预处理 ===============
|
||||
# =========================================
|
||||
|
||||
from statistics import median # 中位数
|
||||
from math import atan2, cos, sin, sqrt, pi, radians, degrees
|
||||
|
||||
from umi_log import logger
|
||||
|
||||
angle_threshold = 3 # 进行一些操作的最小角度阈值
|
||||
angle_threshold_rad = radians(angle_threshold)
|
||||
|
||||
|
||||
# 计算两点之间的距离
|
||||
def _distance(point1, point2):
|
||||
return sqrt((point2[0] - point1[0]) ** 2 + (point2[1] - point1[1]) ** 2)
|
||||
|
||||
|
||||
# 计算一个box的旋转角度
|
||||
def _calculateAngle(box):
|
||||
# 获取宽高
|
||||
width = _distance(box[0], box[1])
|
||||
height = _distance(box[1], box[2])
|
||||
# 选择距离较大的两个顶点对,计算角度弧度值
|
||||
if width < height:
|
||||
angle_rad = atan2(box[2][1] - box[1][1], box[2][0] - box[1][0])
|
||||
else:
|
||||
angle_rad = atan2(box[1][1] - box[0][1], box[1][0] - box[0][0])
|
||||
# 标准化角度到[-pi/2, pi/2)范围(加上阈值)
|
||||
if angle_rad < -pi / 2 + angle_threshold_rad:
|
||||
angle_rad += pi
|
||||
elif angle_rad >= pi / 2 + angle_threshold_rad:
|
||||
angle_rad -= pi
|
||||
return angle_rad
|
||||
|
||||
|
||||
# 估计一组文本块的旋转角度
|
||||
def _estimateRotation(textBlocks):
|
||||
# blocks["box"] = [左上角,右上角,右下角,左下角]
|
||||
angle_rads = (_calculateAngle(block["box"]) for block in textBlocks)
|
||||
median_angle = median(angle_rads) # 中位数
|
||||
return median_angle
|
||||
|
||||
|
||||
# 获取旋转后的标准bbox。angle_threshold为执行旋转的阈值(最小角度值)。
|
||||
def _getBboxes(textBlocks, rotation_rad):
|
||||
# 角度低于阈值(接近0°),则不进行旋转,以提高性能。
|
||||
if abs(rotation_rad) <= angle_threshold_rad:
|
||||
bboxes = [
|
||||
( # 直接构造bbox
|
||||
min(x for x, y in tb["box"]),
|
||||
min(y for x, y in tb["box"]),
|
||||
max(x for x, y in tb["box"]),
|
||||
max(y for x, y in tb["box"]),
|
||||
)
|
||||
for tb in textBlocks
|
||||
]
|
||||
# 否则,进行旋转操作。
|
||||
else:
|
||||
logger.debug(f"文本块预处理旋转 {degrees(rotation_rad):.2f} °")
|
||||
bboxes = []
|
||||
min_x, min_y = float("inf"), float("inf") # 初始化最小的x和y坐标
|
||||
cos_angle = cos(-rotation_rad) # 计算角度正弦值
|
||||
sin_angle = sin(-rotation_rad)
|
||||
for tb in textBlocks:
|
||||
box = tb["box"]
|
||||
rotated_box = [ # 旋转box的每个顶点
|
||||
(cos_angle * x - sin_angle * y, sin_angle * x + cos_angle * y)
|
||||
for x, y in box
|
||||
]
|
||||
# 解包旋转后的顶点坐标,分别得到所有x和y的值
|
||||
xs, ys = zip(*rotated_box)
|
||||
# 构建标准bbox (左上角x, 左上角y, 右下角x, 右下角y)
|
||||
bbox = (min(xs), min(ys), max(xs), max(ys))
|
||||
bboxes.append(bbox)
|
||||
min_x, min_y = min(min_x, bbox[0]), min(min_y, bbox[1])
|
||||
# 如果旋转后存在负坐标,将所有包围盒平移,使得最小的x和y坐标为0,确保所有坐标非负
|
||||
if min_x < 0 or min_y < 0:
|
||||
bboxes = [
|
||||
(x - min_x, y - min_y, x2 - min_x, y2 - min_y)
|
||||
for (x, y, x2, y2) in bboxes
|
||||
]
|
||||
return bboxes
|
||||
|
||||
|
||||
# 预处理 textBlocks ,将包围盒 ["box"] 转为标准化 bbox ,同时去除 ["text"] 不完整的项
|
||||
def linePreprocessing(textBlocks):
|
||||
textBlocks = [i for i in textBlocks if i.get("text", False)]
|
||||
# 判断角度
|
||||
rotation_rad = _estimateRotation(textBlocks)
|
||||
# 获取标准化bbox
|
||||
bboxes = _getBboxes(textBlocks, rotation_rad)
|
||||
# 写入tb
|
||||
for i, tb in enumerate(textBlocks):
|
||||
tb["normalized_bbox"] = bboxes[i]
|
||||
# 按y排序
|
||||
textBlocks.sort(key=lambda tb: tb["normalized_bbox"][1])
|
||||
return textBlocks
|
||||
173
UmiOCR-data/py_src/ocr/tbpu/parser_tools/paragraph_parse.py
Normal file
173
UmiOCR-data/py_src/ocr/tbpu/parser_tools/paragraph_parse.py
Normal file
@@ -0,0 +1,173 @@
|
||||
# 段落分析器
|
||||
# 对已经是一个列区块之内的文本块,判断其段落关系。
|
||||
|
||||
from typing import Callable
|
||||
import unicodedata
|
||||
|
||||
|
||||
# 传入前句尾字符和后句首字符,返回分隔符
|
||||
def word_separator(letter1, letter2):
|
||||
|
||||
# 判断Unicode字符是否属于中文、日文或韩文字符集
|
||||
def is_cjk(character):
|
||||
cjk_unicode_ranges = [
|
||||
(0x4E00, 0x9FFF), # 中文
|
||||
(0x3040, 0x30FF), # 日文
|
||||
(0x1100, 0x11FF), # 韩文
|
||||
(0x3130, 0x318F), # 韩文兼容字母
|
||||
(0xAC00, 0xD7AF), # 韩文音节
|
||||
# 全角符号
|
||||
(0x3000, 0x303F), # 中文符号和标点
|
||||
(0xFE30, 0xFE4F), # 中文兼容形式标点
|
||||
(0xFF00, 0xFFEF), # 半角和全角形式字符
|
||||
]
|
||||
return any(start <= ord(character) <= end for start, end in cjk_unicode_ranges)
|
||||
|
||||
if is_cjk(letter1) and is_cjk(letter2):
|
||||
return ""
|
||||
|
||||
# 特殊情况:前文为连字符。
|
||||
if letter1 == "-":
|
||||
return ""
|
||||
# 特殊情况:后文为任意标点符号。
|
||||
if unicodedata.category(letter2).startswith("P"):
|
||||
return ""
|
||||
# 其它正常情况加空格
|
||||
return " "
|
||||
|
||||
|
||||
TH = 1.2 # 行高用作对比的阈值
|
||||
|
||||
|
||||
class ParagraphParse:
|
||||
def __init__(self, get_info: Callable, set_end: Callable) -> None:
|
||||
"""
|
||||
:param get_info: 函数,传入单个文本块,
|
||||
返回该文本块的信息元组: ( (x0, y0, x1, y1), "文本" )
|
||||
:param set_end: 函数,传入单个文本块 和文本尾部的分隔符,该函数要将分隔符保存。
|
||||
"""
|
||||
self.get_info = get_info
|
||||
self.set_end = set_end
|
||||
|
||||
# ======================= 调用接口:对文本块列表进行结尾分隔符预测 =====================
|
||||
def run(self, text_blocks: list):
|
||||
"""
|
||||
对属于一个区块内的文本块列表,进行段落分析,预测每个文本块结尾的分隔符。
|
||||
|
||||
:param text_blocks: 文本块对象列表
|
||||
:return: 排序后的文本块列表
|
||||
"""
|
||||
# 封装块单元
|
||||
units = self._get_units(text_blocks, self.get_info)
|
||||
# 执行分析
|
||||
self._parse(units)
|
||||
return text_blocks
|
||||
|
||||
# ======================= 封装块单元列表 =====================
|
||||
# 将原始文本块,封装为 ( (x0,y0,x2,y2), ("开头","结尾"), 原始 ) 。
|
||||
def _get_units(self, text_blocks, get_info):
|
||||
units = []
|
||||
for tb in text_blocks:
|
||||
bbox, text = get_info(tb)
|
||||
units.append((bbox, (text[0], text[-1]), tb))
|
||||
return units
|
||||
|
||||
# ======================= 分析 =====================
|
||||
|
||||
# 执行分析
|
||||
def _parse(self, units):
|
||||
units.sort(key=lambda a: a[0][1]) # 确保从上到下有序
|
||||
para_l, para_top, para_r, para_bottom = units[0][0] # 当前段的左右
|
||||
para_line_h = para_bottom - para_top # 当前段行高
|
||||
para_line_s = None # 当前段行间距
|
||||
now_para = [units[0]] # 当前段的块
|
||||
paras = [] # 总的段
|
||||
paras_line_space = [] # 总的段的行间距
|
||||
# 取 左右相等为一个自然段的主体
|
||||
for i in range(1, len(units)):
|
||||
l, top, r, bottom = units[i][0] # 当前块上下左右边缘
|
||||
h = bottom - top
|
||||
ls = top - para_bottom # 行间距
|
||||
# 检测是否同一段
|
||||
if ( # 左右边缘都相等
|
||||
abs(para_l - l) <= para_line_h * TH
|
||||
and abs(para_r - r) <= para_line_h * TH
|
||||
# 行间距不大
|
||||
and (para_line_s == None or ls < para_line_s + para_line_h * 0.5)
|
||||
):
|
||||
# 更新数据
|
||||
para_l = (para_l + l) / 2
|
||||
para_r = (para_r + r) / 2
|
||||
para_line_h = (para_line_h + h) / 2
|
||||
para_line_s = ls if para_line_s == None else (para_line_s + ls) / 2
|
||||
# 添加到当前段
|
||||
now_para.append(units[i])
|
||||
else: # 非同一段,归档上一段,创建新一段
|
||||
paras.append(now_para)
|
||||
paras_line_space.append(para_line_s)
|
||||
now_para = [units[i]]
|
||||
para_l, para_r, para_line_h = l, r, bottom - top
|
||||
para_line_s = None
|
||||
para_bottom = bottom
|
||||
# 归档最后一段
|
||||
paras.append(now_para)
|
||||
paras_line_space.append(para_line_s)
|
||||
|
||||
# 合并只有1行的段,添加到上/下段作为首/尾句
|
||||
for i1 in reversed(range(len(paras))):
|
||||
para = paras[i1]
|
||||
if len(para) == 1:
|
||||
l, top, r, bottom = para[0][0]
|
||||
up_flag = down_flag = False
|
||||
# 上段末尾条件:左对齐,右不超,行间距够小
|
||||
if i1 > 0:
|
||||
# 检查左右
|
||||
up_l, up_top, up_r, up_bottom = paras[i1 - 1][-1][0]
|
||||
up_dist, up_h = abs(up_l - l), up_bottom - up_top
|
||||
up_flag = up_dist <= up_h * TH and r <= up_r + up_h * TH
|
||||
# 检查行间距
|
||||
if (
|
||||
paras_line_space[i1 - 1] != None
|
||||
and top - up_bottom > paras_line_space[i1 - 1] + up_h * 0.5
|
||||
):
|
||||
up_flag = False
|
||||
# 下段开头条件:右对齐/单行超出,左缩进
|
||||
if i1 < len(paras) - 1:
|
||||
down_l, down_top, down_r, down_bottom = paras[i1 + 1][0][0]
|
||||
down_h = down_bottom - down_top
|
||||
# 左对齐或缩进
|
||||
if down_l - down_h * TH <= l <= down_l + down_h * (1 + TH):
|
||||
if len(paras[i1 + 1]) > 1: # 多行,右对齐
|
||||
down_flag = abs(down_r - r) <= down_h * TH
|
||||
else: # 单行,右可超出
|
||||
down_flag = down_r - down_h * TH < r
|
||||
# 检查行间距
|
||||
if (
|
||||
paras_line_space[i1 + 1] != None
|
||||
and down_top - bottom > paras_line_space[i1 + 1] + down_h * 0.5
|
||||
):
|
||||
down_flag = False
|
||||
|
||||
# 选择添加到上还是下段
|
||||
if up_flag and down_flag: # 两段都符合,则选择垂直距离更近的
|
||||
if top - up_bottom < down_top - bottom:
|
||||
paras[i1 - 1].append(para[0])
|
||||
else:
|
||||
paras[i1 + 1].insert(0, para[0])
|
||||
elif up_flag: # 只有一段符合,直接选择
|
||||
paras[i1 - 1].append(para[0])
|
||||
elif down_flag:
|
||||
paras[i1 + 1].insert(0, para[0])
|
||||
if up_flag or down_flag:
|
||||
del paras[i1]
|
||||
del paras_line_space[i1]
|
||||
|
||||
# 刷新所有段,添加end
|
||||
for para in paras:
|
||||
for i1 in range(len(para) - 1):
|
||||
letter1 = para[i1][1][1] # 行1结尾字母
|
||||
letter2 = para[i1 + 1][1][0] # 行2开头字母
|
||||
sep = word_separator(letter1, letter2)
|
||||
self.set_end(para[i1][2], sep)
|
||||
self.set_end(para[-1][2], "\n")
|
||||
return units
|
||||
Reference in New Issue
Block a user