使用paddleocr识别图片文本的一种方案

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。不过工程上的方法就是很多,只有达到效果就行,科研就不行,都是精益求精的。

猜你喜欢

转载自blog.csdn.net/onlyyoujojo/article/details/134941891