PaddleOCR系列——《文本检测、文本识别、表格结构》数据集制作

使用PPOCRLabel可以完成文本检测模块文本识别模块表格结构识别模块这三个模块的数据集制作,分别是导出标记结果导出识别结果导出表格标注这三个功能

文本检测模块数据<--->导出标记结果

文本识别模块数据<--->导出识别结果

表格结构识别模块<--->导出表格标注

百度官方文档:
概述 - PaddleOCR 文档

注意:官方文档只参考,完全照搬会报很多错误

paddlepaddle

安装

开始使用_飞桨-源于产业实践的开源深度学习平台

我是cpu,所以这样选,gpu根据cuda选择自己版本

python -m pip install paddlepaddle==3.2.1 -i https://www.paddlepaddle.org.cn/packages/stable/cpu/

验证

安装完成后您可以使用 python 进入 python 解释器,输入import paddle ,再输入 paddle.utils.run_check()

如果出现PaddlePaddle is installed successfully!,说明您已成功安装。

下列可直接输入

python -c "import paddle; paddle.utils.run_check()"

卸载

请使用以下命令卸载 PaddlePaddle:

  • CPU 版本的 PaddlePaddle

  • python -m pip uninstall paddlepaddle
  • GPU 版本的 PaddlePaddle

  • python -m pip uninstall paddlepaddle-gpu

PPOCRLabel 

安装

python -m pip install PPOCRLabel

错误:

原因:

PPStructureV3 这个功能模块依赖了一个或多个你当前环境中没有安装,或者版本不满足要求的 Python 库。

解决方法:

方法一:安装paddleocr所有包(推荐)
python -m pip install "paddleocr[all]" -i https://pypi.tuna.tsinghua.edu.cn/simple
方法二:PP-StructureV3 所需的依赖包安装

确认当前 paddlex 版本

pip list | findstr paddlex

记下版本号,例:paddlex  3.3.6

安装 paddlex 的 OCR 扩展依赖

pip install "paddlex[ocr]==<版本号>"

例如,若版本是 3.3.6,则执行:

pip install "paddlex[ocr]==3.3.6"

启动

启动【普通模式】,用于打【检测+识别】场景的标签

PPOCRLabel --lang ch

启动 【KIE 模式】,用于打【检测+识别+关键字提取】场景的标签

PPOCRLabel --lang ch --kie True

功能介绍

图片示例:

文件:

打开目录

选择待标记图片的文件夹,文件会显示在列表。图片左侧是是因为未标注和点击,点击后会是

打开数据集路径

打开图片所在文件夹

导出标记结果

生成Label.txt文件,可直接用于文本识别模型训练。

导出识别结果

导出表格数据

识别表格数据。生成excel文件并自动打开。与gt.txt同时产生。

自动导出标记结果

用户每确认过一张图片,程序自动将标记结果写入Label.txt中。若未开启此选项,则检测到用户手动确认过5张图片后进行自动导出。

自动重新识别

勾选后,对于新标注的框内容会自动触发当前标注框的重新识别功能,不需要再去点击重新识别按钮,适合各种原因不想使用自动标注只想手动标注的场景,例如车牌识别,一张图里只有一个车牌,如果使用自动标注,需要删除很多额外识别出来的文字框,不如直接重新标注

自动保存未提交变更

默认是按确认按钮完成当前框的标记确认,有点繁琐,勾选后,切换下一张图(按快捷键D)的时候,不再弹出提示框确认是否保存未确认的标记,自动保存当前标记并切换下一张图,方便快速标记

删除

点击 “删除图像”,图片将会被删除至回收站。

编辑

右键点击也可以出来主要使用功能

矩形标注

拖动方框从左上角到右下角框选标注区域,保存四个坐标点。

多点标注

点击键盘Q,则使用四点标注模式(或点击“编辑” - “四点标注”),用户依次点击4个点后,双击左键表示标注完成。(注意:多点标注可以标很多个点,但“导出识别结果”时会报错,不要超过四个点)

编辑标签

框选框的标签,在右侧显示

复制区块

复制所选框,包括标签和大小,快捷键Ctrl+c

删除选择的区块

删除所选框

重识别此区块

调用自动标注模型,重新识别所选框内容产生新标签,会覆盖原内容(仅所选框)

单元格重实别

以表格中的单元格为单位增加标注框(即一个单元格内的文字都标记为一个框)。标注框上鼠标右键后点击单元格重实别可利用模型自动识别单元格内的文字,和重识别此区块差不多

重新排序位置

点击后会将标注框按照从上到下、从左到右的顺序进行排列。用于解决表格结构标识时,需要手动补充矩形标识后的顺序调整问题。

图片左旋转90度

将图片旋转,但是已经标注好的框辉错乱。

图片右旋转90度

将图片旋转,但是已经标注好的框辉错乱。

区块线条颜色

更改线条颜色

扩大框

以框中心点为中心,向四周扩大

视图

显示类别

显示框标签

显示box序号

显示排序编号,在表格识别很有用

隐藏所有标注

将所有标注框隐藏,就和完全未标注一样

显示所有标注:将所有标注框再显示出来

PaddleOCR:

自动标注

点击 ”自动标注“,使用PP-OCR超轻量模型对图片文件名前图片状态为 “X” 的图片进行自动标注

重新识别

将图片中的所有检测画绘制/调整完成后,点击 “重新识别”,PP-OCR模型会对当前图片中的所有检测框重新识别

单元格重识别

和编辑一样

选择模型

选择语言模型,英文\中文\日语等。。

官方使用说明

PPOCRLabel/README_ch.md at main · PFCCLab/PPOCRLabel

文件说明

[1] PPOCRLabel以文件夹为基本标记单位,打开待标记的图片文件夹后,不会在窗口栏中显示图片,而是在点击 "选择文件夹" 之后直接将文件夹下的图片导入到程序中。

[2] 图片状态表示本张图片用户是否手动保存过,未手动保存过即为 “X”,手动保存过为 “√”。点击 “自动标注”按钮后,PPOCRLabel不会对状态为 “√” 的图片重新标注。

[3] 点击“重新识别”后,模型会对图片中的识别结果进行覆盖。因此如果在此之前手动更改过识别结果,有可能在重新识别后产生变动。

[4] PPOCRLabel产生的文件放置于标记图片文件夹下,包括一下几种,请勿手动更改其中内容,否则会引起程序出现异常。

文件名说明
Label.txt检测标签,可直接用于PPOCR检测模型训练。用户每确认5张检测结果后,程序会进行自动写入。当用户关闭应用程序或切换文件路径后同样会进行写入。
fileState.txt图片状态标记文件,保存当前文件夹下已经被用户手动确认过的图片名称。
Cache.cach缓存文件,保存模型自动识别的结果。
rec_gt.txt识别标签。可直接用于PPOCR识别模型训练。需用户手动点击菜单栏“文件” - "导出识别结果"后产生。
crop_img识别数据。按照检测框切割后的图片。与rec_gt.txt同时产生。

快捷键

快捷键说明
Ctrl + shift + R对当前图片的所有标记重新识别
W新建矩形框
Q 或 Home新建多点框
Ctrl + E编辑所选框标签
Ctrl + X--kie 模式下,修改 Box 的关键字种类
Ctrl + R重新识别所选标记
Ctrl + C【复制并粘贴】选中的标记框
Ctrl + B重新排序坐标框位置
Ctrl + 鼠标左键多选标记框
Backspace 或 Delete删除所选框
Ctrl + V 或 End确认本张图片标记
Ctrl + Shift + d删除本张图片
D下一张图片
A上一张图片
Ctrl++缩小
Ctrl--放大
↑→↓←移动标记框
Z、X、C、V、B对选中的标记框,单独移动四个顶点

数据集分割:

PPOCRLabel工具中的gen_ocr_train_val_test.py脚本确实能帮你自动划分数据集,免去手动操作的繁琐。为了让你快速上手,我先用一个表格来汇总核心的操作步骤和脚本的关键参数:

步骤/模块

关键参数/操作

说明/示例

📁 前期准备

使用PPOCRLabel完成图像标注并导出

确保生成Label.txt(检测标签)和rec_gt.txt(识别标签)

⚙️ 脚本参数

--trainValTestRatio

训练集:验证集:测试集比例,例如8:2:06:2:2

--datasetRootPath

PPOCRLabel标注数据的根路径,例如./train_data/label

--detRootPath

划分后文本检测数据集的存放路径,例如./train_data/det

--recRootPath

划分后文本识别数据集的存放路径,例如./train_data/rec

--detLabelFileName

检测标注文件名,默认为Label.txt

--recLabelFileName

识别标注文件名,默认为rec_gt.txt

--recImageDirName

识别数据集裁剪图片目录名,默认为crop_img

🖥️ 执行脚本

命令示例

python gen_ocr_train_val_test.py --trainValTestRatio 6:2:2 --datasetRootPath ./train_data/label ...

📁 准备工作与目录结构

在运行脚本之前,你需要完成以下准备工作:

  1. 完成数据标注:确保你已经使用PPOCRLabel标注好了图像,并导出了标注结果。这通常会生成:
    • 用于文本检测的标签文件 Label.txt
    • 用于文本识别的标签文件 rec_gt.txt 以及存放裁剪出文本区域的 crop_img 文件夹。
  2. 组织目录结构:推荐的目录结构如下:
train_data/
├── label/          # 存放PPOCRLabel标注的原始数据和标签文件
├── det/            # 脚本将划分好的检测数据集放在这里
└── rec/            # 脚本将划分好的识别数据集放在这里

你可以在 label 文件夹下存放多个批次标注的文件夹。

🛠️ 使用步骤与命令示例

  1. 打开终端:导航至PaddleOCR的 PPOCRLabel 目录下,因为 gen_ocr_train_val_test.py 脚本通常位于此处。
  2. 执行划分命令:根据你的需求调整参数后运行命令。
    • 一个典型的命令示例
python gen_ocr_train_val_test.py --trainValTestRatio 6:2:2 --datasetRootPath ./train_data/label --detRootPath ./train_data/det --recRootPath ./train_data/rec

这个命令会将数据按6:2:2的比例划分训练集、验证集和测试集。

    • 如果只划分训练集和验证集,可以设置测试集比例为0:
python gen_ocr_train_val_test.py --trainValTestRatio 8:2:0 --datasetRootPath ./train_data/label

此例中,检测和识别数据的输出路径使用了默认的 detRootPathrecRootPath(如 ./train_data/det./train_data/rec)。

🔍 注意事项

  • 路径问题:如果在Windows下划分数据,但计划在Linux系统下训练,需要注意文件路径的差异,可能会因路径前缀问题导致训练时找不到文件。建议尽量在最终训练的环境(如Linux)中直接进行数据划分
  • 标签文件格式:脚本运行后若在后续训练中遇到读取问题(例如递归错误或计算问题),可以检查划分后生成的标签文件(如 train.txt, val.txt)中的制表符分隔格式是否正确,以及图片路径是否有效。
  • 数据清洗:在使用PPOCRLabel标注后、划分数据集前,建议对标注数据进行抽查和清洗,纠正错误的标注框或识别结果,这对模型性能至关重要。

补充:新增gt.txt表格数据分割脚本

原gen_ocr_train_val_test.py脚本并没有关于表格数据的数据分割,于是修改了原脚本,可直接使用。

# coding:utf8
import os
import shutil
import random
import argparse


# Delete the divided train, val, and test folders and create a new empty folder
def isCreateOrDeleteFolder(path, flag):
    flagPath = os.path.join(path, flag)

    if os.path.exists(flagPath):
        shutil.rmtree(flagPath)

    os.makedirs(flagPath)
    flagAbsPath = os.path.abspath(flagPath)
    return flagAbsPath


def splitTrainVal(
        root,
        abs_train_root_path,
        abs_val_root_path,
        abs_test_root_path,
        train_txt,
        val_txt,
        test_txt,
        flag,
):
    data_abs_path = os.path.abspath(root)
    label_file_name = args.detLabelFileName if flag == "det" else args.recLabelFileName
    label_file_path = os.path.join(data_abs_path, label_file_name)

    with open(label_file_path, "r", encoding="UTF-8") as label_file:
        label_file_content = label_file.readlines()
        random.shuffle(label_file_content)
        label_record_len = len(label_file_content)

        for index, label_record_info in enumerate(label_file_content):
            image_relative_path, image_label = label_record_info.split("\t")
            image_name = os.path.basename(image_relative_path)

            if flag == "det":
                image_path = os.path.join(
                    data_abs_path, image_name
                )
            elif flag == "rec":
                image_path = os.path.join(
                    data_abs_path, args.recImageDirName, image_name
                )

            train_val_test_ratio = args.trainValTestRatio.split(":")
            train_ratio = eval(train_val_test_ratio[0]) / 10
            val_ratio = train_ratio + eval(train_val_test_ratio[1]) / 10
            cur_ratio = index / label_record_len

            if cur_ratio < train_ratio:
                image_copy_path = os.path.join(abs_train_root_path, image_name)
                shutil.copy(image_path, image_copy_path)
                train_txt.write("{}\t{}".format(image_copy_path, image_label))
            elif cur_ratio >= train_ratio and cur_ratio < val_ratio:
                image_copy_path = os.path.join(abs_val_root_path, image_name)
                shutil.copy(image_path, image_copy_path)
                val_txt.write("{}\t{}".format(image_copy_path, image_label))
            else:
                image_copy_path = os.path.join(abs_test_root_path, image_name)
                shutil.copy(image_path, image_copy_path)
                test_txt.write("{}\t{}".format(image_copy_path, image_label))


def splitTrainVal_det(
        root,
        abs_train_root_path,
        abs_val_root_path,
        abs_test_root_path,
        train_txt,
        val_txt,
        test_txt,
        train_txt_gt,
        val_txt_gt,
        test_txt_gt,
        flag,
):
    data_abs_path = os.path.abspath(root)
    label_file_name = args.detLabelFileName if flag == "det" else args.recLabelFileName
    label_file_path = os.path.join(data_abs_path, label_file_name)

    # 读取标注文件
    with open(label_file_path, "r", encoding="UTF-8") as label_file:
        label_file_content = label_file.readlines()

    # 对于det模式,同时读取gt.txt文件
    gt_file_content = None
    if flag == "det":
        gt_file_path = args.detGtFileName
        with open(gt_file_path, "r", encoding="UTF-8") as gt_file:
            gt_file_content = gt_file.readlines()

        # 验证两个文件行数是否一致
        if len(label_file_content) != len(gt_file_content):
            raise ValueError(f"Label.txt({len(label_file_content)}行) 和 gt.txt({len(gt_file_content)}行) 行数不一致!")

    # 组合数据并打乱(保持对应关系)
    if flag == "det":
        # 组合label和gt数据,形成元组列表
        combined_data = list(zip(label_file_content, gt_file_content))
        random.shuffle(combined_data)  # 同步打乱
        label_record_len = len(combined_data)
    else:
        # rec模式保持原有逻辑
        random.shuffle(label_file_content)
        label_record_len = len(label_file_content)

    for index in range(label_record_len):
        if flag == "det":
            # 从组合数据中获取对应的label和gt
            label_record_info, gt_record_info = combined_data[index]
            image_relative_path, image_label = label_record_info.strip().split("\t")
            gt_label = gt_record_info.strip()  # gt.txt的内容(假设每行是一个标注)
        else:
            label_record_info = label_file_content[index]
            image_relative_path, image_label = label_record_info.strip().split("\t")

        image_name = os.path.basename(image_relative_path)

        if flag == "det":
            image_path = os.path.join(
                data_abs_path, image_name
            )
        elif flag == "rec":
            image_path = os.path.join(
                data_abs_path, args.recImageDirName, image_name
            )

        train_val_test_ratio = args.trainValTestRatio.split(":")
        train_ratio = eval(train_val_test_ratio[0]) / 10
        val_ratio = train_ratio + eval(train_val_test_ratio[1]) / 10
        cur_ratio = index / label_record_len

        if cur_ratio < train_ratio:
            image_copy_path = os.path.join(abs_train_root_path, image_name)
            shutil.copy(image_path, image_copy_path)
            train_txt.write("{}\t{}\n".format(image_copy_path, image_label))
            train_txt_gt.write("{}\n".format(gt_label))
        elif cur_ratio >= train_ratio and cur_ratio < val_ratio:
            image_copy_path = os.path.join(abs_val_root_path, image_name)
            shutil.copy(image_path, image_copy_path)
            val_txt.write("{}\t{}\n".format(image_copy_path, image_label))
            val_txt_gt.write("{}\n".format(gt_label))
        else:
            image_copy_path = os.path.join(abs_test_root_path, image_name)
            shutil.copy(image_path, image_copy_path)
            test_txt.write("{}\t{}\n".format(image_copy_path, image_label))
            test_txt_gt.write("{}\n".format(gt_label))


# Remove the file if it exists
def removeFile(path):
    if os.path.exists(path):
        os.remove(path)


def genDetRecTrainVal(args):
    detAbsTrainRootPath = isCreateOrDeleteFolder(args.detRootPath, "train")
    detAbsValRootPath = isCreateOrDeleteFolder(args.detRootPath, "val")
    detAbsTestRootPath = isCreateOrDeleteFolder(args.detRootPath, "test")

    recAbsTrainRootPath = isCreateOrDeleteFolder(args.recRootPath, "train")
    recAbsValRootPath = isCreateOrDeleteFolder(args.recRootPath, "val")
    recAbsTestRootPath = isCreateOrDeleteFolder(args.recRootPath, "test")

    removeFile(os.path.join(args.detRootPath, "train.txt"))
    removeFile(os.path.join(args.detRootPath, "val.txt"))
    removeFile(os.path.join(args.detRootPath, "test.txt"))

    removeFile(os.path.join(args.detRootPath, "train_gt.txt"))
    removeFile(os.path.join(args.detRootPath, "val_gt.txt"))
    removeFile(os.path.join(args.detRootPath, "test_gt.txt"))

    removeFile(os.path.join(args.recRootPath, "train.txt"))
    removeFile(os.path.join(args.recRootPath, "val.txt"))
    removeFile(os.path.join(args.recRootPath, "test.txt"))

    detTrainTxt = open(
        os.path.join(args.detRootPath, "train.txt"), "a", encoding="UTF-8"
    )
    detValTxt = open(
        os.path.join(args.detRootPath, "val.txt"), "a", encoding="UTF-8"
    )
    detTestTxt = open(
        os.path.join(args.detRootPath, "test.txt"), "a", encoding="UTF-8"
    )

    detTrainTxt_gt = open(
        os.path.join(args.detRootPath, "train_gt.txt"), "a", encoding="UTF-8"
    )
    detValTxt_gt = open(
        os.path.join(args.detRootPath, "val_gt.txt"), "a", encoding="UTF-8"
    )
    detTestTxt_gt = open(
        os.path.join(args.detRootPath, "test_gt.txt"), "a", encoding="UTF-8"
    )

    recTrainTxt = open(
        os.path.join(
            args.recRootPath, "train.txt"), "a", encoding="UTF-8"
    )
    recValTxt = open(os.path.join(
        args.recRootPath, "val.txt"), "a", encoding="UTF-8"
    )
    recTestTxt = open(
        os.path.join(args.recRootPath, "test.txt"), "a", encoding="UTF-8"
    )

    splitTrainVal_det(
        args.datasetRootPath,
        detAbsTrainRootPath,
        detAbsValRootPath,
        detAbsTestRootPath,
        detTrainTxt,
        detValTxt,
        detTestTxt,
        detTrainTxt_gt,
        detValTxt_gt,
        detTestTxt_gt,
        "det",
    )

    for root, dirs, files in os.walk(args.datasetRootPath):
        for dir in dirs:
            if dir == "crop_img":
                splitTrainVal(
                    root,
                    recAbsTrainRootPath,
                    recAbsValRootPath,
                    recAbsTestRootPath,
                    recTrainTxt,
                    recValTxt,
                    recTestTxt,
                    "rec",
                )
            else:
                continue
        break

    # 关闭文件句柄(重要!避免数据丢失)
    detTrainTxt.close()
    detValTxt.close()
    detTestTxt.close()
    recTrainTxt.close()
    recValTxt.close()
    recTestTxt.close()


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "--trainValTestRatio",
        type=str,
        default="8:2:0",
        help="ratio of trainset:valset:testset",
    )
    parser.add_argument(
        "--datasetRootPath",
        type=str,
        default="F:/table_data/all_data/",
        help="path to the dataset marked by ppocrlabel, E.g, dataset folder named 1,2,3...",
    )
    parser.add_argument(
        "--detRootPath",
        type=str,
        default="F:/table_data/det",
        help="the path where the divided detection dataset is placed",
    )
    parser.add_argument(
        "--recRootPath",
        type=str,
        default="F:/table_data/rec",
        help="the path where the divided recognition dataset is placed",
    )
    parser.add_argument(
        "--detLabelFileName",
        type=str,
        default="Label.txt",  # 改为文件名(因为后面会拼接路径)
        help="the name of the detection annotation file",
    )
    parser.add_argument(
        "--recLabelFileName",
        type=str,
        default="rec_gt.txt",  # 改为文件名
        help="the name of the recognition annotation file",
    )
    parser.add_argument(
        "--recImageDirName",
        type=str,
        default="crop_img",  # 改为文件夹名(因为后面会拼接路径)
        help="the name of the folder where the cropped recognition dataset is located",
    )
    parser.add_argument(
        "--detGtFileName",
        type=str,
        default="gt.txt",  # 改为文件名(因为后面会使用绝对路径)
        help="the name of the detection gt annotation file",
    )

    args = parser.parse_args()

    # 修正参数:如果detGtFileName是相对路径,则拼接datasetRootPath
    if not os.path.isabs(args.detGtFileName):
        args.detGtFileName = os.path.join(args.datasetRootPath, args.detGtFileName)

    genDetRecTrainVal(args)

数据集需注意问题:

文本识别模块

PP-OCRv5_server_rec(PP-OCRv5 服务端版文本识别模块)原生仅针对单行文本识别设计,无法直接识别多行文本;训练该 Rec 模块时,数据集图片也不能直接使用多行文本图片,需预处理为单行后再训练。

多行文本图片 → Det模块检测出每个文本行的坐标框 → 裁剪每个框得到单行文本图 → Rec模块逐行识别 → 拼接各行结果(按行顺序)

问题统计(会持续更新)

1、界面打不开或闪退

输入启动命令后没有页面打开,只是终端黑框闪一下(正确运行应该是下方),

等待20~30s,如果还打不开就用绝对路径打开尝试

采用调试模式打印路径:

python -c "import PPOCRLabel, os; print(os.path.dirname(PPOCRLabel.__file__))"

会输出PPOCRLabel的目录,示例: 

D:\anaconda3\envs\PPOCRLabel\lib\site-packages\PPOCRLabel

绝对路径启动 :

python D:\anaconda3\envs\PPOCRLabel\lib\site-packages\PPOCRLabel\PPOCRLabel.py --lang ch

2、“导出识别结果”报错

原因多点标注方框坐标点不能超过4个,否则导出时报上方错误。

解决:当需要导出识别结果时,使用“矩形标注”或“多点标注”不超出四个点,下面必报错。

注意

①、多点标注时,方框不方正会导致图片内容扭曲;

②、多点标注对“导出标记结果”无影响。

3、“导出表格标注”问题

3.1、导出表格数据excel不生成弹出,提示导出成功,但gt.txt为空

安装下面两个库

pip3 install premailer

pip3 install pywin32

3.2、点击右上角“表格识别”后导出excel与实际表格样式不同

常于复杂表格或自定义表格。

点击右上角“表格识别”,点击就可以官方模型自动导出的,但是空格不会标注,如果表格太复杂还可能标不全,手动修改标注后新增数据导出不了,而且已经手动标注后,点击“表格识别”会将以前标注覆盖。

解决:不用官方“表格识别”,在图片路径建立tableRec_excel_output文件夹,然后在文件夹中建立.xlsx文件,文件名是图片名,例如:aaa.png,bbb.png,那.xlsx文件就应该是aaa.xlsx,bbb.xlsx

然后手动在xlsx文件中按照图片或pdf创建表格,表格框架一定要相同,有数据的地方可以以任意数据进行占位标记(例如:1)。当然,不占位也没问题。
例如:

如果训练时遇到IndexError: list index out of range错误,就是标注数据时xlsx文件表格结构会非常差和PPOCRLabel标注结构不同。

根据图片自动生成xlsx文件脚本

import os
from openpyxl import Workbook


def create_excel_for_images(input_dir, output_dir):
    """
    搜索指定目录下的图片文件,并为每张图片在输出目录中创建一个同名的Excel文件。

    :param input_dir: 要搜索图片的目录路径。
    """
    # 定义需要搜索的图片文件后缀
    image_extensions = ('.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff')

    output_dir = f"{input_dir}\\{output_dir}"

    # 1. 检查输入目录是否存在
    if not os.path.isdir(input_dir):
        print(f"错误:输入的路径 '{input_dir}' 不是一个有效的目录。")
        return

    # 2. 创建输出目录,如果它不存在的话
    if not os.path.exists(output_dir):
        os.makedirs(output_dir)
        print(f"已创建输出目录: {output_dir}")

    # 3. 遍历输入目录下的所有文件
    image_found_count = 0
    excel_created_count = 0
    print(f"\n正在搜索目录 '{input_dir}' 下的图片文件...")
    for filename in os.listdir(input_dir):
        # 检查文件后缀是否为图片格式(不区分大小写)
        if filename.lower().endswith(image_extensions):
            image_found_count += 1

            # 获取文件名(不含后缀)
            file_name_without_ext = os.path.splitext(filename)[0]

            # 构建完整的图片路径(用于打印信息)
            image_path = os.path.join(input_dir, filename)

            # 构建要创建的Excel文件路径
            excel_filename = f"{file_name_without_ext}.xlsx"
            excel_path = os.path.join(output_dir, excel_filename)

            print(f"  -> 找到图片: {image_path}")

            # 4. 检查Excel文件是否已存在,如果不存在则创建
            if os.path.exists(excel_path):
                print(f"     警告: Excel文件 '{excel_path}' 已存在,将跳过创建。")
                continue

            try:
                # 使用openpyxl创建一个新的工作簿
                wb = Workbook()
                # 获取默认的工作表
                # ws = wb.active
                # 可以在这里向工作表中添加一些默认内容,例如文件名
                # ws['A1'] = f"这是为图片 '{filename}' 创建的Excel文件。"
                # 保存工作簿
                wb.save(excel_path)
                excel_created_count += 1
                print(f"     成功创建Excel文件: {excel_path}")
            except Exception as e:
                print(f"     错误: 创建Excel文件 '{excel_path}' 时失败: {e}")

    # 5. 打印总结信息
    print("\n--- 处理完成 ---")
    print(f"在目录 '{input_dir}' 中总共找到 {image_found_count} 个图片文件。")
    print(f"在目录 '{output_dir}' 中成功创建了 {excel_created_count} 个Excel文件。")


if __name__ == "__main__":
    path = "F:\\img"
    output_dir = "tableRec_excel_output"
    # 调用主函数
    create_excel_for_images(path, output_dir)

检测gt.txt文件看xlsx文件表格结构和PPOCRLabel标注结构是否不同脚本

import json
import os


def check_td_cells_match(label_json):
    """
    检查表格标注的<td>数与cells数是否匹配
    :param label_json: 标注的JSON字符串或字典
    :return: (td_count, cells_count, is_match, error_msg)
    """
    error_msg = ""
    try:
        if isinstance(label_json, str):
            label = json.loads(label_json)
        else:
            label = label_json

        # 检查必要的键是否存在
        if "html" not in label:
            error_msg = "JSON缺少html字段"
            return 0, 0, False, error_msg
        if "structure" not in label["html"] or "tokens" not in label["html"]["structure"]:
            error_msg = "JSON缺少html.structure.tokens字段"
            return 0, 0, False, error_msg
        if "cells" not in label["html"]:
            error_msg = "JSON缺少html.cells字段"
            return 0, 0, False, error_msg

        # 统计<td>结构单元数(以<td开头的token)
        structure_tokens = label["html"]["structure"]["tokens"]
        td_count = sum(1 for token in structure_tokens if token.startswith("<td"))

        # 统计cells数
        cells_count = len(label["html"]["cells"])

        # 检查是否匹配
        is_match = td_count == cells_count

        return td_count, cells_count, is_match, error_msg

    except json.JSONDecodeError as e:
        error_msg = f"JSON解析错误: {str(e)}"
        return 0, 0, False, error_msg
    except Exception as e:
        error_msg = f"处理错误: {str(e)}"
        return 0, 0, False, error_msg


def process_json_txt_file(file_path, output_errors=True):
    """
    处理每行都是JSON的txt文件,检查<td>数与cells数是否匹配
    :param file_path: txt文件路径
    :param output_errors: 是否输出错误信息
    :return: (total_lines, matched_lines, error_lines, detail_results)
    """
    if not os.path.exists(file_path):
        print(f"错误:文件不存在 - {file_path}")
        return 0, 0, 0, []

    total_lines = 0
    matched_lines = 0
    error_lines = 0
    detail_results = []

    print(f"\n开始处理文件:{file_path}")
    print("-" * 80)

    with open(file_path, "r", encoding="utf-8") as f:
        for line_num, line in enumerate(f, 1):
            line = line.strip()
            total_lines += 1

            # 跳过空行
            if not line:
                detail_results.append({
                    "line_num": line_num,
                    "status": "empty",
                    "td_count": 0,
                    "cells_count": 0,
                    "error_msg": "空行"
                })
                continue

            # 检查当前行的JSON
            td_count, cells_count, is_match, error_msg = check_td_cells_match(line)

            if error_msg:
                error_lines += 1
                status = "error"
                if output_errors:
                    print(f"\n第{line_num}行 - 错误:")
                    print(f"  错误信息:{error_msg}")
                    print(f"  原始内容:{line[:100]}..." if len(line) > 100 else f"  原始内容:{line}")
            else:
                if is_match:
                    matched_lines += 1
                    status = "matched"
                else:
                    status = "mismatched"

                # 输出不匹配的行信息
                if not is_match and output_errors:
                    print(f"\n第{line_num}行 - 不匹配:")
                    print(f"  <td>结构单元数:{td_count}")
                    print(f"  cells条目数:{cells_count}")
                    print(f"  差异数:{abs(td_count - cells_count)}")

            detail_results.append({
                "line_num": line_num,
                "status": status,
                "td_count": td_count,
                "cells_count": cells_count,
                "error_msg": error_msg,
                "is_match": is_match
            })

    # 输出统计结果
    print("-" * 80)
    print(f"\n处理完成!")
    print(f"总行数:{total_lines}")
    print(f"匹配行数:{matched_lines}")
    print(f"不匹配行数:{total_lines - matched_lines - error_lines}")
    print(f"错误行数:{error_lines}")
    print(f"匹配率:{matched_lines / total_lines * 100:.2f}%" if total_lines > 0 else "匹配率:0.00%")

    return total_lines, matched_lines, error_lines, detail_results


# 用法示例
if __name__ == "__main__":
    # 请替换为你的txt文件路径
    txt_file_path = "F:\\img\\gt.txt"  # 例如:"标注数据.json.txt"

    # 处理文件
    # output_errors=True 表示输出错误和不匹配的详细信息
    total, matched, errors, details = process_json_txt_file(txt_file_path, output_errors=True)

    # 如果需要将结果保存到文件(可选)
    save_results = False
    if save_results:
        with open("检查结果.txt", "w", encoding="utf-8") as f:
            f.write("表格标注检查结果\n")
            f.write("=" * 50 + "\n")
            f.write(f"检查文件:{txt_file_path}\n")
            f.write(f"检查时间:{pd.Timestamp.now().strftime('%Y-%m-%d %H:%M:%S')}\n")  # 需要导入pandas
            f.write(f"总行数:{total}\n")
            f.write(f"匹配行数:{matched}\n")
            f.write(f"不匹配行数:{total - matched - errors}\n")
            f.write(f"错误行数:{errors}\n")
            f.write(f"匹配率:{matched / total * 100:.2f}%\n")
            f.write("\n详细结果:\n")
            f.write("-" * 50 + "\n")
            for result in details:
                f.write(f"第{result['line_num']}行 - 状态:{result['status']}\n")
                if result['error_msg']:
                    f.write(f"  错误:{result['error_msg']}\n")
                else:
                    f.write(
                        f"  <td>数:{result['td_count']} | cells数:{result['cells_count']} | 匹配:{result['is_match']}\n")
            f.write("\n" + "=" * 50 + "\n")
        print("\n结果已保存到:检查结果.txt")

4、“导出识别结果”不完整

Label.txt文件保存信息完整,但crop_img文件夹下不会保存相应截图,rec_gt.txt也不会保存相应信息

原因Label.txt文件中"difficult""true"

方法一:手动搜索替换将Label.txt文件中"difficult": true替换为"difficult": false

方法二:手动标注框后不要按 ctrl + r 自动识别

标好所有框之后直接点击重新识别。对识别不对的文字或者数据进行人为修改。

5、数据标注问题

无论是文字检测还是文字识别,都是以行为单位,在标注表格时,标注满整个表格会出问题。下面是错误标注和正确标注的对应结果。****************************************************************************************************

下面这几种标注形式会导致问题:

  1. 整个单元格被认为一个单位
  2. 框选面积大导致的识别错误

****************************************************************************************************

表格标注时应尽量按照下方格式:

  1. 以行为单位标注
  2. 尽量贴紧行文字,避免占满整个单元格

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xin_yao_xin

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值