pdf文本分为两种,一种是标准的pdf格式的文本,这种无需利用ocr识别,另外一种就是图片文本,这种需要进行ocr的识别。
OCR 识别文本和文本区域
ppstructure是paddleocr里面的一个子库,可以识别文档的页眉页脚、正文、标题等等。输出是json格式的数据,里面有文本type、文本内容、文本块区域bbox和每一行的文本区域text_region等信息。
版面恢复
agument-xy-cut
一个实现 code
其实这一步针对的是比较复杂的排版,比如表格类型的。
一般的论文什么的,排版都比较固定,单栏横版和双栏横版,基本上一句
sorted_boxes = sorted(res, key=lambda x: (x['bbox'][1], x['bbox'][0]))
就可以解决了。一般的文本用不上 agument-xy-cut。我用 agument-xy-cut 来排版属于是杀鸡用了牛刀。
虽然ppstructure识别出来的文本块是无序的,但是它给出了文本块区域bbox和每一行的文本区域text_region等信息,我们可以利用文本块区域bbox的信息,来对文本块排序。
agument-xy-cut就是一种很好的算法。可以识别单栏、双栏、表格甚至更复杂的排版。
如果是一般的单栏横版的文章,我们可以跳过这一步,因为直接利用每一行的文本区域text_region进行下一步即可。
整合自然段
主要是整合自然段,因为ppstructure识别出来的文本有两个层次,第一层次是文本块区域,以bbox划分,利用上面的
agument-xy-cut算法可以进行排序。还有文本块区域内部的每一行的文本,这里我们要做的是将每一行的文本整合为自然段。
这里提供一种算法,code
不是特别精确,不过大概是能利用每一行的文本区域text_region对文本进行自然段的整合。
# 合并:段落-横排-自然段
from .merge_line import MergeLine
class MergePara(MergeLine):
def __init__(self):
super().__init__()
self.tbpuName = "多行-自然段"
self.mllhLine = 1.8 # 最大段间距
def isSameColumn(self, A, B): # 两个文块属于同一栏时,返回 True
# 获取A、B行高
if "lineHeight" in A: # 已记录
Ah = A["lineHeight"]
else: # 未记录,则写入记录
Ah = A["lineHeight"] = A["box"][3][1] - A["box"][0][1]
A["lineCount"] = 1 # 段落的行数
Bh = B["box"][3][1] - B["box"][0][1]
if abs(Bh - Ah) > Ah * self.mllhH:
return False # AB行高不符
# 行高相符,判断垂直投影是否重叠
ax1, ax2 = A["box"][0][0], A["box"][1][0]
bx1, bx2 = B["box"][0][0], B["box"][1][0]
if ax2 < bx1 or ax1 > bx2:
return False
return True # AB垂直投影重叠
def isSamePara(self, A, B): # 两个文块属于同一段落时,返回 True
ah = A["lineHeight"]
# 判断垂直距离
ly = ah * self.mllhY
lLine = ah * self.mllhLine
a, b = A["box"], B["box"]
ay, by = a[3][1], b[0][1]
if by < ay - ly or by > ay + lLine:
return False # 垂直距离过大
# 判断水平距离
lx = ah * self.mllhX
ax, bx = a[0][0], b[0][0]
if A["lineCount"] == 1: # 首行允许缩进2格
return ax - ah * 2.5 - lx <= bx <= ax + lx
else:
return abs(ax - bx) <= lx
def merge2line(self, textBlocks, i1, i2): # 合并2行
ranges = [
(0x4E00, 0x9FFF), # 汉字
(0x3040, 0x30FF), # 日文
(0xAC00, 0xD7AF), # 韩文
(0xFF01, 0xFF5E), # 全角字符
]
# 判断两端文字的结尾和开头,是否属于汉藏语族
# 汉藏语族:行间无需分割符。印欧语族:则两行之间需加空格。
separator = " "
ta, tb = textBlocks[i1]["text"][-1], textBlocks[i2]["text"][0]
fa, fb = False, False
for l, r in ranges:
if l <= ord(ta) <= r:
fa = True
if l <= ord(tb) <= r:
fb = True
if fa and fb:
separator = ""
# print(f"【{ta}】与【{tb}】是汉字集。")
# else:
# print(f"【{ta}】与【{tb}】是西文集。")
self.merge2tb(textBlocks, i1, i2, separator)
textBlocks[i1]["lineCount"] += 1 # 行数+1
def mergePara(self, textBlocks):
# 单行合并
hList = self.mergeLine(textBlocks)
# 按左上角y排序
hList.sort(key=lambda tb: tb["box"][0][1])
# 遍历每个行,寻找并合并属于同一段落的两个行
listlen = len(hList)
resList = []
for i1 in range(listlen):
tb1 = hList[i1]
if not tb1:
continue
num = 1 # 合并个数
# 遍历后续文块
for i2 in range(i1 + 1, listlen):
tb2 = hList[i2]
if not tb2:
continue
# 符合同一栏
if self.isSameColumn(tb1, tb2):
# 符合同一段,合并两行
if self.isSamePara(tb1, tb2):
self.merge2line(hList, i1, i2)
num += 1
# 同栏、不同段,说明到了下一段,则退出内循环
else:
break
if num > 1:
tb1["score"] /= num # 平均置信度
resList.append(tb1) # 装填入结果
return resList
def run(self, textBlocks, imgInfo):
# 段落合并
resList = self.mergePara(textBlocks)
# 返回新文块列表
return resList
# 合并:单行-横排
from .tbpu import Tbpu
from functools import cmp_to_key
class MergeLine(Tbpu):
def __init__(self):
self.tbpuName = "单行-横排"
# merge line limit multiple X/Y/H,单行合并时的水平/垂直/行高阈值系数,为行高的倍数
self.mllhX = 2
self.mllhY = 0.5
self.mllhH = 0.5
def isSameLine(self, A, B): # 两个文块属于同一行时,返回 True
Ax, Ay = A[1][0], A[1][1] # 块A右上角xy
Ah = A[3][1] - A[0][1] # 块A行高
Bx, By = B[0][0], B[0][1] # 块B左上角xy
Bh = B[3][1] - B[0][1] # 块B行高
lx = Ah * self.mllhX # 水平、垂直、行高 合并阈值
ly = Ah * self.mllhY
lh = Ah * self.mllhH
if abs(Bx - Ax) < lx and abs(By - Ay) < ly and abs(Bh - Ah) < lh:
return True
return False
def merge2tb(self, textBlocks, i1, i2, separator): # 合并2个tb,将i2合并到i1中。
tb1 = textBlocks[i1]
tb2 = textBlocks[i2]
b1 = tb1["box"]
b2 = tb2["box"]
# 合并两个文块box
yTop = min(b1[0][1], b1[1][1], b2[0][1], b2[1][1])
yBottom = max(b1[2][1], b1[3][1], b2[2][1], b2[3][1])
xLeft = min(b1[0][0], b1[3][0], b2[0][0], b2[3][0])
xRight = max(b1[1][0], b1[2][0], b2[1][0], b2[2][0])
b1[0][1] = b1[1][1] = yTop # y上
b1[2][1] = b1[3][1] = yBottom # y下
b1[0][0] = b1[3][0] = xLeft # x左
b1[1][0] = b1[2][0] = xRight # x右
# 合并内容
tb1["score"] += tb2["score"] # 合并置信度
tb1["text"] = tb1["text"] + separator + tb2["text"] # 合并文本
textBlocks[i2] = None # 置为空,标记删除
def mergeLine(self, textBlocks): # 单行合并
# 所有文块,按左上角点的x坐标排序
textBlocks.sort(key=lambda tb: tb["box"][0][0])
# 遍历每个文块,寻找后续文块中与它接壤、且行高一致的项,合并两个文块
resList = []
listlen = len(textBlocks)
for i1 in range(listlen):
tb1 = textBlocks[i1]
if not tb1:
continue
b1 = tb1["box"]
num = 1 # 合并个数
# 遍历后续文块
for i2 in range(i1 + 1, listlen):
tb2 = textBlocks[i2]
if not tb2:
continue
b2 = tb2["box"]
# 符合同一行,则合并
if self.isSameLine(b1, b2):
# 合并两个文块box
self.merge2tb(textBlocks, i1, i2, " ")
num += 1
if num > 1:
tb1["score"] /= num # 平均置信度
resList.append(tb1) # 装填入结果
return resList
def sortLines(self, resList): # 对文块排序,从上到下,从左到右
def sortKey(A, B):
# 先比较两个文块的水平投影是否重叠
ay1, ay2 = A["box"][0][1], A["box"][3][1]
by1, by2 = B["box"][0][1], B["box"][3][1]
# 不重叠,则按左上角y排序
if ay2 < by1 or ay1 > by2:
return 0 if ay1 == by1 else (-1 if ay1 < by1 else 1)
# 重叠,则按左上角x排序
ax, bx = A["box"][0][0], B["box"][0][0]
return 0 if ax == bx else (-1 if ax < bx else 1)
resList.sort(key=cmp_to_key(sortKey))
def run(self, textBlocks, imgInfo):
# 单行合并
resList = self.mergeLine(textBlocks)
# 结果排序
self.sortLines(resList)
# 返回新文块列表
return resList
这里的 key 需要自己手动更改,‘box’、‘score’分别对应的是paddleocr识别出来的’text_region’、‘confidence’。
(其实paddleocr里面应该也有对应的算法,here,这里面也是根据文本块区域bbox进行的排序,先y后x,也挺合适的。不过没有整合自然段的功能。就是粗暴的把文本块区域都弄在一起了。)
总结
步骤就是这样,先ocr识别文本和区域,后面根据区域进行版面恢复。版面恢复部分根据自己的需要,可以省略。
PS:突然有个问题,我发现wps+python-word处理的应该也还行,段落什么的也都分的很好,表格也识别对了。之前是觉得wps对生僻字识别的不好,所以没用,而且wps要钱hh。不过工程上的方法就是很多,只有达到效果就行,科研就不行,都是精益求精的。