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 MergeLineclass 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 Falsereturn True # AB垂直投影重叠def isSamePara(self, A, B): # 两个文块属于同一段落时,返回 Trueah = A["lineHeight"]# 判断垂直距离ly = ah * self.mllhYlLine = ah * self.mllhLinea, 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.mllhXax, bx = a[0][0], b[0][0]if A["lineCount"] == 1: # 首行允许缩进2格return ax - ah * 2.5 - lx <= bx <= ax + lxelse:return abs(ax - bx) <= lxdef 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, Falsefor l, r in ranges:if l <= ord(ta) <= r:fa = Trueif l <= ord(tb) <= r:fb = Trueif fa and fb:separator = ""# print(f"【{ta}】与【{tb}】是汉字集。")# else:# print(f"【{ta}】与【{tb}】是西文集。")self.merge2tb(textBlocks, i1, i2, separator)textBlocks[i1]["lineCount"] += 1 # 行数+1def 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:continuenum = 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:breakif num > 1:tb1["score"] /= num # 平均置信度resList.append(tb1) # 装填入结果return resListdef run(self, textBlocks, imgInfo):# 段落合并resList = self.mergePara(textBlocks)# 返回新文块列表return resList
# 合并:单行-横排from .tbpu import Tbpufrom functools import cmp_to_keyclass MergeLine(Tbpu):def __init__(self):self.tbpuName = "单行-横排"# merge line limit multiple X/Y/H,单行合并时的水平/垂直/行高阈值系数,为行高的倍数self.mllhX = 2self.mllhY = 0.5self.mllhH = 0.5def isSameLine(self, A, B): # 两个文块属于同一行时,返回 TrueAx, Ay = A[1][0], A[1][1] # 块A右上角xyAh = A[3][1] - A[0][1] # 块A行高Bx, By = B[0][0], B[0][1] # 块B左上角xyBh = B[3][1] - B[0][1] # 块B行高lx = Ah * self.mllhX # 水平、垂直、行高 合并阈值ly = Ah * self.mllhYlh = Ah * self.mllhHif abs(Bx - Ax) < lx and abs(By - Ay) < ly and abs(Bh - Ah) < lh:return Truereturn Falsedef merge2tb(self, textBlocks, i1, i2, separator): # 合并2个tb,将i2合并到i1中。tb1 = textBlocks[i1]tb2 = textBlocks[i2]b1 = tb1["box"]b2 = tb2["box"]# 合并两个文块boxyTop = 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:continueb1 = tb1["box"]num = 1 # 合并个数# 遍历后续文块for i2 in range(i1 + 1, listlen):tb2 = textBlocks[i2]if not tb2:continueb2 = tb2["box"]# 符合同一行,则合并if self.isSameLine(b1, b2):# 合并两个文块boxself.merge2tb(textBlocks, i1, i2, " ")num += 1if num > 1:tb1["score"] /= num # 平均置信度resList.append(tb1) # 装填入结果return resListdef 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。不过工程上的方法就是很多,只有达到效果就行,科研就不行,都是精益求精的。