Python 实现 VOC格式目标检测数据集的按类别分拣脚本

Python 实现 VOC格式目标检测数据集的按类别分拣脚本

flyfish

从多层级的混合标注数据集中,筛选并拆分出 dog(狗)和 cat(猫)两类的图片数据

  1. 多层目录自动扫描识别

    递归遍历源根目录下的所有子文件夹,自动识别出同时包含 jpg 图片文件夹和 xml 标注文件夹的有效数据目录;处理过程会完整保留源数据的目录层级结构,不会打乱原有文件组织方式。

  2. VOC标注文件解析与类别筛选

    解析标准VOC格式的XML标注文件,提取每个标注目标的类别名称;仅保留 dogcat 两类目标。

    单张图片包含多个同类别目标时会自动去重,同一张图只会被归类一次;

    若单张图片同时标注了狗和猫,则这张图片会同时被分到 dogcat 两个类别目录中。

  3. 按类别分类拷贝图片

    根据XML标注的类别结果,将同名jpg图片拷贝到目标根目录下的对应类别文件夹;使用 shutil.copy2 拷贝文件,会保留图片的原始创建时间、修改时间等元数据;目标路径中缺失的目录会自动创建,无需手动提前建立文件夹。

得到的结果

分类好的图片数据集

在配置的目标根目录(TARGET_ROOT)下,会生成 dogcat 两个独立的分类文件夹;每个文件夹内都会完整复刻源数据的目录层级结构,分别存放标注中包含对应类别的jpg图片。

python 复制代码
import os
import shutil
import xml.etree.ElementTree as ET

# ===================== 路径配置 =====================
# 源文件夹根目录(存放多层目录数据的顶层文件夹)
SOURCE_ROOT = r"E:\data"
# 目标输出根目录(程序会自动创建 dog / cat 子文件夹)
TARGET_ROOT = r"E:\animal\"
# =====================================================

def parse_voc_xml(xml_file_path):
    """
    解析VOC格式XML,提取目标类别(dog / cat)
    返回:去重后的类别列表
    """
    target_classes = []
    try:
        tree = ET.parse(xml_file_path)
        root = tree.getroot()

        # 遍历所有标注目标
        for obj in root.findall("object"):
            class_name = obj.find("name").text.strip()
            # 匹配目标类别
            if class_name == "dog":
                target_classes.append("dog")
            elif class_name == "cat":
                target_classes.append("cat")

        # 去重:单张图多个同类别目标,只归类一次
        return list(set(target_classes))

    except Exception as e:
        print(f"XML解析失败:{xml_file_path},错误:{str(e)}")
        return []

def copy_image_with_structure(src_jpg, rel_path, target_class):
    """
    按照原目录层级结构拷贝图片到对应类别目录
    :param src_jpg: 源图片完整路径
    :param rel_path: 源目录相对根路径,用于复刻目录结构
    :param target_class: 目标类别文件夹名
    """
    # 拼接目标完整路径:目标根目录/类别/原目录层级/图片名
    target_dir = os.path.join(TARGET_ROOT, target_class, rel_path)
    target_jpg = os.path.join(target_dir, os.path.basename(src_jpg))

    # 自动创建不存在的目录
    os.makedirs(target_dir, exist_ok=True)

    # 拷贝文件并保留原始元数据(创建/修改时间等)
    shutil.copy2(src_jpg, target_jpg)
    print(f"已拷贝:{os.path.basename(src_jpg)} → {target_class}/{rel_path}")

def main():
    print("========== 开始扫描并处理文件 ==========\n")

    # 递归遍历源根目录下所有文件夹
    for root_dir, sub_dirs, files in os.walk(SOURCE_ROOT):
        # 核心判断:当前目录下同时存在 jpg 和 xml 两个子文件夹,才作为有效数据目录
        if "jpg" in sub_dirs and "xml" in sub_dirs:
            # 计算当前目录相对于源根目录的路径,用于保留层级结构
            relative_path = os.path.relpath(root_dir, SOURCE_ROOT)
            
            jpg_folder = os.path.join(root_dir, "jpg")
            xml_folder = os.path.join(root_dir, "xml")

            print(f"\n找到目标目录:{relative_path}")
            print(f"   JPG文件夹:{jpg_folder}")
            print(f"   XML文件夹:{xml_folder}")

            # 遍历当前目录下所有XML标注文件
            for xml_name in os.listdir(xml_folder):
                if not xml_name.lower().endswith(".xml"):
                    continue  # 跳过非XML文件

                xml_path = os.path.join(xml_folder, xml_name)
                file_stem = os.path.splitext(xml_name)[0]  # 提取文件名前缀
                jpg_path = os.path.join(jpg_folder, file_stem + ".jpg")  # 匹配同名图片

                # 校验对应图片是否存在,不存在则跳过
                if not os.path.exists(jpg_path):
                    print(f"跳过:{xml_name} 无匹配JPG文件")
                    continue

                # 解析XML获取标注类别
                detected_classes = parse_voc_xml(xml_path)

                # 按类别拷贝图片到对应目录
                for cls in detected_classes:
                    copy_image_with_structure(jpg_path, relative_path, cls)

    print("\n========== 任务全部完成 ==========")

if __name__ == "__main__":
    main()