一、概述

表格单元格检测模块是表格识别任务的关键组成部分,负责在表格图像中定位和标记每个单元格区域,该模块的性能直接影响到整个表格识别过程的准确性和效率。表格单元格检测模块通常会输出各个单元格区域的边界框(Bounding Boxes),这些边界框将作为输入传递给表格识别相关产线进行后续处理。

注意:“单元格检测模型”返回的是表格中每个单元格坐标框(包括合并的),与后面的“表格结构识别模型”息息相关,当然也可以自己写算法,自己排列这些坐标框组成完整表格(官方“表格结构识别模型”实在有些一言难尽)

二、支持模型列表

支持“有线表格单元格”检测和“无线表格单元格”检测,两者识别依靠的是“表格分类模块”,会将输入表格分为“有线表格”或“无线表格”,然后调用不同的“单元格检测模型”

有线表格:RT-DETR-L_wired_table_cell_det

无线表格:RT-DETR-L_wireless_table_cell_det

三、PaddleX环境安装

官方链接:安装PaddlePaddle - PaddleX 文档

参考链接:PaddleX本地安装教程_安装paddlex-CSDN博客

四、数据准备

官方demo数据集:https://paddle-model-ecology.bj.bcebos.com/paddlex/data/cells_det_coco_examples.tar

五、标注工具Labelme

5.1 Labelme标注工具介绍

Labelme 是一个 python 语言编写,带有图形界面的图像标注软件。可用于图像分类,目标检测,图像分割等任务,在目标检测的标注任务中,标签存储为 JSON 文件。

5.2 Labelme 安装

为避免环境冲突,建议在 conda 环境下安装,可以参考:Miniconda安装与使用-CSDN博客

conda create -n labelme python=3.10 
conda activate labelme 
pip install pyqt5 
pip install labelme

5.3 Labelme 标注过程

5.3.1 准备待标注数据

  • 创建数据集根目录,如 data
  • 在 data 中创建 images 目录(必须为images目录),并将待标注图片存储在 images 目录下,如下图所示:
  • 在 data 文件夹中创建待标注数据集的类别标签文件 label.txt,并在 label.txt 中按行写入待标注数据集的类别。如下图所示:

5.3.2 启动 Labelme

终端进入到待标注数据集根目录,并启动 Labelme 标注工具:

cd path/to/data
labelme images --labels label.txt --nodata --autosave --output annotations

flags 为图像创建分类标签,传入标签路径。

nodata 停止将图像数据存储到 JSON文件。

autosave 自动存储。

output 标签文件存储路径。

5.3.3 开始图片标注

  • 启动 Labelme 后如图所示:
  • * 点击"编辑"选择标注类型,选择“创建矩形框”,右键点击再选择也可以
  • 在图片上拖动十字框选目标区域,再次点击选择目标框类别
  •  标注好后点击存储。(若在启动 Labelme 时未指定 output 字段,会在第一次存储时提示选择存储路径,若指定 autosave 字段使用自动保存,则无需点击存储按钮)。
  • 然后点击 Next Image 进行下一张图片的标注。
  •  最终标注好的标签文件如图所示:
  • annotations文件夹内
  • *调整目录得到标准Labelme格式数据集,在数据集根目录创建train_anno_list.txtval_anno_list.txt两个文本文件,并将annotations目录下的全部json文件路径按一定比例分别写入train_anno_list.txt和val_anno_list.txt,也可全部写入到train_anno_list.txt同时创建一个空的val_anno_list.txt文件,使用数据划分功能进行重新划分。

  • 数据分割代码:

  • import os
    import random
     
     
    def split_annotations(annotations_dir, train_txt, val_txt, split_ratio=4):
        """
        将annotations目录下的JSON文件按照指定比例随机分配到训练集和验证集
        Args:
            annotations_dir (str): 标注文件目录
            train_txt (str): 训练集txt文件路径
            val_txt (str): 验证集txt文件路径
            split_ratio (int): 训练集与验证集的比例,默认为4:1
        """
        # 获取annotations目录下所有JSON文件
        json_files = []
        for file in os.listdir(annotations_dir):
            if file.endswith(".json"):
                json_files.append(file)
     
        # 随机打乱文件列表
        random.shuffle(json_files)
     
        # 计算分割点
        total_files = len(json_files)
        train_size = int(total_files * split_ratio / (split_ratio + 1))
     
        # 分割文件列表
        train_files = json_files[:train_size]
        val_files = json_files[train_size:]
     
        # 构建文件路径(格式:annotations/xxx.json)
        train_paths = [f"annotations/{file}" for file in train_files]
        val_paths = [f"annotations/{file}" for file in val_files]
     
        # 写入训练集txt文件
        with open(train_txt, "w", encoding="utf-8") as f:
            for path in train_paths:
                f.write(path + "\n")
     
        # 写入验证集txt文件
        with open(val_txt, "w", encoding="utf-8") as f:
            for path in val_paths:
                f.write(path + "\n")
     
        print(f"分割完成!")
        print(f"训练集文件数: {len(train_files)}")
        print(f"验证集文件数: {len(val_files)}")
        print(f"训练集已写入: {train_txt}")
        print(f"验证集已写入: {val_txt}")
     
     
    if __name__ == "__main__":
        # 配置路径
        annotations_dir = r"E:\xxx\annotations"
        train_txt = (
            r"E:\xxx\train_anno_list.txt"
        )
        val_txt = r"E:\xxx\val_anno_list.txt"
     
        # 执行分割
        split_annotations(annotations_dir, train_txt, val_txt)

  • train_anno_list.txtval_anno_list.txt的具体填写格式如图所示:

  • 经过整理得到的最终目录结构如下:

5.3.4 格式转换

使用Labelme标注完成后,需要将数据格式 转换为coco格式。下面给出了按照上述教程使用Lableme标注完成的数据和进行数据格式转换的代码示例:

cd /path/to/paddlex
wget https://paddle-model-ecology.bj.bcebos.com/paddlex/data/det_labelme_examples.tar -P ./dataset
tar -xf ./dataset/det_labelme_examples.tar -C ./dataset/
 
python main.py -c paddlex/configs/object_detection/PicoDet-L.yaml \
    -o Global.mode=check_dataset \
    -o Global.dataset_dir=./dataset/det_labelme_examples \
    -o CheckDataset.convert.enable=True \
    -o CheckDataset.convert.src_dataset_type=LabelMe

转换完会在annotations文件夹下生成instance_train.jsoninstance_val.json文件

六、数据集快速标注

但是!!!一张表格图片可能有上百个单元格,一个个标简直要疯。所以我选择偷懒。。。

  • 先用官方模型推理图片,得到json结果
import os
import json
from tqdm import tqdm
from pathlib import Path
from paddlex import create_model


model = create_model(
    model_name="RT-DETR-L_wired_table_cell_det",
    # model_dir="/best_model/inference"#你自己模型所在目录或直接不用这个参数
)

pib_path_name = "cell_data" #图片所在文件夹名
image_suffixes = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp'}
base_img_path = f"/pic/{pib_path_name}" #图片所在文件夹路径
out_base_path = f"/out/TableCellsDetection_X_PIC/{pib_path_name}"#结果输出路径
img_path = []

for item in os.listdir(base_img_path):
    if os.path.splitext(item)[1] in image_suffixes:
        img_path.append(f"{base_img_path}/{item}")

json_list = []  # 用于收集每个页面的JSON数据
for item in tqdm(img_path):
    output = model.predict(
        item,
        threshold=0.4, #置信度
        batch_size=1 #一次加载几张,值越大,消耗越高
    )

    json_data = {}
    for idx, res in enumerate(output):
        json_data = res.json["res"]
        json_list.append(json_data)


merged_json_path = f"{out_base_path}/cell_all_data.json"
with open(merged_json_path, "w", encoding="utf-8") as f:
    # 合并逻辑:将每个页面的JSON数据放在一个列表中,方便查看
    json.dump(
        {"data": json_list},
        f,
        ensure_ascii=False,  # 支持中文显示
        indent=2  # 格式化输出,便于阅读
    )
  • 然后用脚本转成Labelme所用数据格式,再用Labelme打开修改调整,重复

    5.3 Labelme 标注过程

import json
import os
from PIL import Image  # 导入PIL库用于读取图片实际尺寸


def convert_to_labelme_json(input_json_path, output_dir, img_root_dir=""):
    """
    将给定的单元格标注JSON转换为labelme格式的JSON文件
    :param input_json_path: 输入标注JSON文件路径
    :param output_dir: 输出labelme格式JSON文件的目录
    :param img_root_dir: 图片文件根目录(用于读取真实尺寸)
    """
    # 1. 读取并解析输入JSON文件
    try:
        with open(input_json_path, "r", encoding="utf-8") as f:
            input_data = json.load(f)
        # 提取核心data列表,做容错处理
        data_list = input_data.get("data", [])
        if not isinstance(data_list, list):
            raise ValueError("输入JSON的data字段不是有效列表")
    except FileNotFoundError:
        raise FileNotFoundError(f"输入JSON文件不存在:{input_json_path}")
    except json.JSONDecodeError as e:
        raise ValueError(f"JSON解析失败,格式错误:{e}")
    except Exception as e:
        raise Exception(f"读取输入文件异常:{e}")

    # 2. 确保输出目录存在
    os.makedirs(output_dir, exist_ok=True)

    # 3. 处理每条数据,生成对应的labelme格式JSON文件
    processed_count = 0
    for data_item in data_list:
        # 3.1 提取并处理文件名(从完整input_path中提取文件名)
        input_path = data_item.get("input_path", "")
        if not input_path:
            continue
        # 提取文件名(如AAA.png、BBB.png)
        filename = os.path.basename(input_path)
        # 生成输出JSON文件名(将.png或.jpg替换为.json)
        base_name = os.path.splitext(filename)[0]
        output_json_filename = f"{base_name}.json"
        output_json_path = os.path.join(output_dir, output_json_filename)

        # 3.2 提取boxes列表,做容错处理
        boxes = data_item.get("boxes", [])
        if not isinstance(boxes, list):
            continue

        # 3.3 构建labelme格式的shapes列表
        shapes = []
        for box in boxes:
            # 提取坐标信息,做容错处理
            coordinate = box.get("coordinate", [])
            if len(coordinate) != 4:
                continue  # 跳过无效坐标(要求左、上、右、下四个值)

            # 解析坐标:[x1, y1, x2, y2]
            x1, y1, x2, y2 = coordinate

            # 构建labelme格式的shape
            shape = {
                "label": "cell",
                "points": [[float(x1), float(y1)], [float(x2), float(y2)]],
                "group_id": None,
                "description": "",
                "shape_type": "rectangle",
                "flags": {},
                "mask": None,
            }
            shapes.append(shape)

        if not shapes:  # 无有效标注的图像跳过
            print(f"警告:{filename}无有效单元格标注,已跳过")
            continue

        # 3.4 读取图片实际宽高
        img_width = 300
        img_height = 200
        try:
            # 拼接完整图片路径
            if img_root_dir:
                img_full_path = os.path.join(img_root_dir, filename)
            else:
                img_full_path = input_path  # 若未指定根目录,使用原始文件路径

            # 打开图片并获取真实尺寸
            with Image.open(img_full_path) as img:
                img_width, img_height = img.size
                print(f"成功读取{filename}真实尺寸:{img_width}×{img_height}")
        except FileNotFoundError:
            print(f"警告:图片文件不存在({img_full_path}),将使用默认尺寸")
        except Exception as e:
            print(f"警告:读取图片尺寸失败({e}),将使用默认尺寸")

        # 3.5 构建labelme格式JSON数据
        labelme_data = {
            "version": "5.10.1",
            "flags": {},
            "shapes": shapes,
            "imagePath": f"../images/{filename}",  # 相对路径,可根据实际情况调整
            "imageData": None,
            "imageHeight": img_height,
            "imageWidth": img_width,
        }

        # 3.6 将结果写入输出JSON文件
        try:
            with open(output_json_path, "w", encoding="utf-8") as f:
                json.dump(labelme_data, f, ensure_ascii=False, indent=2)
            print(f"已生成:{output_json_path}")
            processed_count += 1
        except Exception as e:
            print(f"警告:写入{output_json_filename}失败:{e},已跳过")
            continue

    print(f"\n转换完成!共处理{processed_count}张图像")


# 示例使用(可直接运行,修改输入输出路径即可)
if __name__ == "__main__":
    # 配置输入JSON路径、输出目录annotations
    INPUT_JSON_PATH = r"E:\data\cell_all_data.json"
    OUTPUT_DIR = r"E:\data\annotations"

    # 关键配置:图片文件根目录(你的图片存放的文件夹)
    # 示例:如果图片存放在 gt_to_coco/images 下,填写该路径
    IMG_ROOT_DIR = r"F:\data\images"  # 请修改为你的实际图片根目录

    # 执行转换
    convert_to_labelme_json(INPUT_JSON_PATH, OUTPUT_DIR, img_root_dir=IMG_ROOT_DIR)

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐