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

news2024/11/24 20:53:40

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1304218.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

多线程 - 学习笔记

前置知识 什么是线程和进程? 进程: 是程序的一次执行,一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间&#xff0c;一个进程可以有多个线程&#xff0c;比如在Windows系统中&#xff0c;一个运行的xx.exe就是一个进程。 线程: 进程中的一个执行流&#xff0…

我的软考拿证之路

我的软考拿证之路 前言2021年上半年2022年下半年2023年上半年2023年下半年 前言 在2020年8月份&#xff0c;准备参加软考高级项目管理师考试&#xff0c;经历了疫情三年&#xff0c;停考3次&#xff0c;经历了教材更新&#xff0c;经历了考试形式由纸考换机考&#xff0c;共参…

实现TensorBoard可视化网络的参数

前言 最近在做神经网络相关的实验&#xff0c;为了方便神经网络参数是否变化&#xff0c;学习一下TensorBoard可视化网络的参数的方法&#xff0c;这里使用pytorch实现。 实现 当使用PyTorch训练一个简单的神经网络时&#xff0c;可以使用TensorBoardX来可视化网络的参数。以…

机器学习---KNN最近邻算法

1、KNN最近邻算法 K最近邻(k-Nearest Neighbor&#xff0c;KNN)分类算法&#xff0c;是一个理论上比较成熟的方法&#xff0c;也是最简单的机器学习算法之一&#xff0c;有监督算法。该方法的思路是&#xff1a;如果一个样本在特征空间中的k个最相似的样本中的大多数属于某一个…

小米耳机定制音效选项灰色无法开启使用_开启定制音效_音效模式设置

使用环境&#xff1a;Redmi K50 Ultra &#xff0b;MIUI 14.0.11&#xff0b;定制音效选项是灰色的无法开启及音效模式无法选择 定制音效无法开启 音效模式无法选择&#xff08;需下载小米耳机APP才能设置&#xff09;&#xff0c;根据提示解决问题即可 解决方法&#xff1a;关…

ICC2:low power与pg strategy(pg_mesh)

我正在「拾陆楼」和朋友们讨论有趣的话题,你⼀起来吧? 拾陆楼知识星球入口 用pg_strategy创建power stripe,示例如下: set pd_list {{DEFAULT_VA VDD_DIG VDD_DIG VSS} {PD_DSP VDD_DIG VDD_DSP VSS} } ;#两个电源域,DEFAULT_VA和PD_DSP是对应voltage area名字,其中D…

硬件开发笔记(十六):RK3568底板电路mipi摄像头接口原理图分析、mipi摄像头详解

若该文为原创文章&#xff0c;转载请注明原文出处 本文章博客地址&#xff1a;https://hpzwl.blog.csdn.net/article/details/134922307 红胖子网络科技博文大全&#xff1a;开发技术集合&#xff08;包含Qt实用技术、树莓派、三维、OpenCV、OpenGL、ffmpeg、OSG、单片机、软硬…

【神行百里】pandas查询加速之行索引篇

最近进行大数据处理的时候&#xff0c;发现我以前常用的pandas查询方法太慢了&#xff0c;太慢了&#xff0c;真是太慢了&#xff0c;查阅资料&#xff0c;遂发现了一种新的加速方法&#xff0c;能助力我飞上天&#xff0c;和太阳肩并肩&#xff0c;所以记录下来。 1. 场景说明…

章鱼网络进展月报 | 2023.11.1-11.30

章鱼网络大事摘要 1、2023年12月&#xff0c;Octopus 2.0 将会正式启动。 2、隐私协议 Secret Network 宣布使用 Octopus Network 构建的 NEAR-IBC 连接 NEAR 生态。 3、Louis 受邀作为嘉宾&#xff0c;在 NEARCON2023 的多链网络主题沙龙中发言&#xff1a;我们依然处于区…

Emacs之Plantuml用于复杂UML类图(Markdown用于简单类图)(一百三十二)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a;多媒…

FTP、U盘等传统数据安全摆渡方法的6个弊端

数据安全摆渡&#xff0c;即数据在不同的网络之间&#xff0c;进行安全流转。做网间隔离的初衷&#xff0c;就是为了保护数据安全&#xff0c;但是在数据摆渡时&#xff0c;除了安全&#xff0c;企业还是需要考虑其他的要素&#xff0c;比如可靠性、易用性、兼容性等等。而传统…

解决:TypeError: write() argument must be str, not Tag

解决&#xff1a;TypeError: write() argument must be str, not Tag 文章目录 解决&#xff1a;TypeError: write() argument must be str, not Tag背景报错问题报错翻译报错位置代码报错原因解决方法今天的分享就到此结束了 背景 在使用之前的代码时&#xff0c;报错&#xf…

Qt搭建MQTT编程环境

QT 部署官方MQTT模块 在Qt Creator中&#xff0c;默认是没有部署Mqtt模块的&#xff0c;在使用QT编程之前&#xff0c;先把mqtt 模块部署到开发环境当中。参考博文&#xff1a;Qt开发技术&#xff1a;mqtt介绍、QtMqtt编译和开发环境搭建_长沙红胖子-CSDN博客 下载 Qt官方在g…

HTML 块级元素与行内元素有哪些以及注意、总结

行内元素和块级元素是HTML中的两种元素类型&#xff0c;它们在页面中的显示方式和行为有所不同。 块级元素&#xff08;Block-level Elements&#xff09;&#xff1a; 常见的块级元素有div、p、h1-h6、ul、ol、li、table、form等。 块级元素会独占一行&#xff0c;即使没有…

WPF仿网易云搭建笔记(4):信息流控制之消息订阅

文章目录 专栏和Gitee仓库前言消息订阅最简单的案例简单用例父组件订阅子组件回调 结果 消息订阅机制消息token是A还是B?传递消息的载体。双重token重复订阅问题 结论 专栏和Gitee仓库 WPF仿网易云 Gitee仓库 WPF仿网易云 CSDN博客专栏 前言 上一篇文章中&#xff0c;我们简单…

最大公因数,最小公倍数详解

前言 对于初学编程的小伙伴们肯定经常遇见此类问题&#xff0c;而且为之头疼&#xff0c;今天我来给大家分享一下&#xff0c;最大公因数和最小公倍数的求法。让我们开始吧&#xff01; 文章目录 1&#xff0c;最大公因数法1法2法3 2&#xff0c;最小公倍数3&#xff0c;尾声 …

修改 vCenter Server 的 FQDN | hostname | PNID

目录 1 先决条件2. 修改 VC 7.0.3 hostname&#xff08;1&#xff09;备份 VCSA&#xff08;2&#xff09;为VCSA的新hostname创建DNS记录&#xff08;3&#xff09;修改 VCSA 的hostname① 进入vCenter Server VAMI② 查看当前 FQDN③ 编辑网络设置④ 选择网络适配器⑤ 修改 h…

【Linux】 线程池

线程池 什么是线程池&#xff1f; 一次预先申请一批线程&#xff0c;让这批线程有任务&#xff0c;就处理任务&#xff1b;没任务&#xff0c;就处于等待状态。 为什么要有线程池&#xff1f; 以空间换时间&#xff0c;预先申请一批线程&#xff0c;当有任务到来&#xff0c;可…

arkts编译报错-arkts-limited-stdlib错误【Bug已完美解决-鸿蒙开发】

文章目录 项目场景:问题描述原因分析:解决方案:适配指导案例此Bug解决方案总结项目场景: arkts编译报错-arkts-limited-stdlib错误。 我用Deveco studio4.0 beta2开发应用,报arkts-limited-stdlib错误 报错内容为: ERROR: ArKTS:ERROR File: D:/prRevivw/3792lapplica…

基于深度学习的yolov5入侵检测系统

欢迎大家点赞、收藏、关注、评论啦 &#xff0c;由于篇幅有限&#xff0c;只展示了部分核心代码。 文章目录 一项目简介IntroductionYOLOv5 Overview入侵检测系统架构1. 数据采集2. YOLOv5模型训练3. 实时监测4. 告警与反馈 性能评估与优化 二、功能三、系统四. 总结 一项目简…