pdf2docx开发概要:对齐隐式表格线¶
发布于:2020-09-27 | 分类:process automation
开发概要:解析表格记录了隐式表格Stream Table
的解析方式:根据文本块位置生成潜在的表格边线,然后采用与显式表格相同的处理方式解析这些边线。其中,两个遗留问题:
-
第一步假想表格线时,尽量对齐横纵边线以简化表格结构
-
兼容半隐式表格例如三线表
本文记录这两个问题的处理方案。
问题与模型简化¶
根据下图中绿色块表示的文本位置,我们已经识别出了潜在的表格区域;根据相邻块正中间插入分隔线的原则,得到了图示浅色线表示的表格结构。从显示上来说没有任何问题,因为这些边框线对于隐式表格来说都是隐藏的。
需要改进的地方是,从第二行开始,两列的分隔线可以对齐合并,这样使表格结构从6x6
简化成6x3
。
单独看一条竖直边线,它实际上可以在相邻的两个文本块之间活动;我们希望尽可能多的竖直边线通过同一x
方向位置。于是这个问题简化为:
已知一系列的区间[x1_s,x1_e], [x2_s,x2_e], ...
,确定尽可能少的垂线x1,x2,...
,使得每个区间至少有一条线穿过。
下面不妨初始化区间为:
inputs = [[8, 10], [2, 6], [4, 6], [1, 6], [4, 6], [5, 6]]
求解简化模型¶
首先设计一个区间对象,它具有左、右边界属性,类似于竖直边线允许在一定范围内左右移动。我们的目标是确定此区间内的一个具体值,也就是固定这条边线。下面的代码可以看出,如果没有优化边线位置,默认取区间中点位置。
class Interval:
def __init__(self, x_range:tuple=None):
# valid range
if x_range: x0, x1 = x_range
else: x0, x1 = -9999, 9999
self.LRange, self.URange = x0, x1
# whether the position is fixed
self._x = None
self.finalized = False
@property
def x(self):
return self._x if self.finalized else (self.LRange+self.URange)/2.0
def is_valid(self, x):
'''Whether the given position `x` locates in the valid instance range.'''
return self.LRange < x < self.URange
def finalize(self, x):
'''Finalize with given position.'''
# can be finalized for only one time
if self.finalized or not self.is_valid(x): return
self._x = x
self.finalized = True
接下来设计区间组对象,对应所有竖直边线或者所有水平边线。我们将优化组内的所有区间,得到优化后的位置common_x
。具体过程其实就是扫描线算法:
- 取每个区间的左右端点并去重得到一系列
x
坐标,x1,x2,x3,...
- 相邻两个坐标组成一个考察区间,例如
[x1,x2], [x2,x3], ...
- 取考察区间中点例如
x=(x1+x2)/2
作为候选位置,检测它通过原始区间的情况,例如用序列0,1,1...
表示经过区间2和3 - 根据穿过区间数降序排列区间中点
- 依次取区间中点直到所有区间都被固定下来
class Intervals:
'''Collection of Interval instances.'''
def __init__(self):
self._instances = []
def append(self, instance): self._instances.append(instance)
def common_x(self):
return set([instance.x for instance in self._instances])
def solve(self):
# collect interval points and sort in x-increasing order
x_points = set()
for instance in self._instances:
x_points.add(instance.LRange)
x_points.add(instance.URange)
x_points = list(x_points)
x_points.sort()
# check intersection status of each intervals
x_status = []
for i in range(len(x_points)-1):
x = (x_points[i]+x_points[i+1])/2.0
c = list(map(
lambda instance: int(instance.is_valid(x)), self._instances))
x_status.append((x,c))
# sort per count since preferring passing through more intervals
x_status.sort(key=lambda item: sum(item[1]), reverse=True)
# finalize instances
num = len(self._instances)
current_status = [0] * num
for x, status in x_status:
# terminate if all instances are finalized
if sum(current_status) == num: break
# only one line is allowed to pass through one interval -> sum(A.*B)=0
# e.g. A = [1,0,1,0], B=[0,1,0,0] -> valid
# A = [1,0,1,0], B =[1,0,0,0] -> invalid due to two lines passing through interval 1
duplicated = sum([c1*c2 for c1,c2 in zip(current_status, status)])
if duplicated: continue
# update current status
current_status = [c1+c2 for c1,c2 in zip(current_status, status)]
# now, finalize instances
for instance, instance_status in zip(self._instances, status):
if instance_status: instance.finalize(x)
直接展示一下结果,如果去掉下面代码中I.solve()
将得到{3.5, 4.0, 5.5, 5.0, 9.0}
,也就是开始所说参差不齐的情况;如果加上区间优化,则得到{9.0, 5.5}
,即实际上对齐后的边线。
if __name__ == '__main__':
inputs = [[8, 10], [2, 6], [4, 6], [1, 6], [4, 6], [5, 6]]
I = Intervals()
for x_range in inputs: I.append(Interval(x_range))
I.solve() # optimize
x = I.common_x()
print(x)
# if comment ot I.solve()
# {3.5, 4.0, 5.5, 5.0, 9.0}
# final result
# {9.0, 5.5}
回到原始问题¶
-
第一步假想表格线时,尽量对齐横纵边线以简化表格结构
类似于简化模型,分别优化横、纵边线。以竖直边线为例,注意不仅有左右区间属性(水平移动自由度),还有上、下边线属性,即引用限定竖直范围的两条水平边线。只有同时确定水平位置和上下边线后,这条竖直边线才得以确定。
-
兼容半隐式表格例如三线表
上一个问题以尽量对齐边线为原则优化边线位置,当给定显式的表格线时,需要引入优先级更高的原则:尽量通过显式的表格线。
以解析三线表中为例,已经给定了显式的水平表格线
h_i
(i=1,2,3
),考察所有可能的水平边线集合H
,如果某条水平边线H_i
的竖直活动区间[yi_1,yi_2]
涵盖显式边线h_i
的yi
坐标,并且H_i
的左右两条竖直边线的可能活动区域[xi_1,xi_2]
在h_i
的水平区间内,则确定H_i
的纵坐标y=yi
。
注意
半隐式表格的解析完全依赖于隐式表格,也就是说如果隐式表格未能正确识别原始文档的表格结构,则直接导致半隐式表格中显式表格线解析失败。