news 2026/5/5 2:59:53

Pascal VOC数据集划分的致命陷阱与最佳实践:为什么99%的开发者都该以JPEGImages图片文件夹为基准,而不是Annotations XML?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Pascal VOC数据集划分的致命陷阱与最佳实践:为什么99%的开发者都该以JPEGImages图片文件夹为基准,而不是Annotations XML?
划分基准推荐度优点缺点/风险适用场景
JPEGImages(图片)★★★★★源头操作,保证每张有效样本必有图;通过相同 ID 加载标注天然同步;无图无标注可立即发现并清理;主流框架均以图片列表为准需额外检查标注文件是否存在(但本身利于发现脏数据)通用首选,尤其适合自定义数据集、工业项目、可能存在脏数据的场景
Annotations(原始 XML)★★★☆☆传统 VOC 官方做法,老代码常见;若数据 100% 干净可直接使用若存在“只有 XML 无图”或“只有图无 XML”会导致加载崩溃;易掩盖数据问题仅确信数据集严格 1:1 且干净时使用(如官方 VOC2007/2012)

1.Pascal VOC 格式数据集的目录

  • 做目标检测时,只需 Annotations、ImageSets/Main、JPEGImages 三个目录;SegmentationClass 与 SegmentationObject 仅在分割任务中使用。当然train.txt、val.txt也可以自定义路径,不按以下存放。
  • 所有文件名(不含扩展名)一一对应,方便按行读取 ImageSets 里的列表后快速定位图片与标注。
VOCdevkit #举例 └── VOC2012 ├── Annotations # 每张图片对应的 XML 标注文件 │ ├── 2007_000001.xml │ ├── 2007_000002.xml │ └── … ├── ImageSets # 训练/验证/测试集的切分列表 │ ├── Action # 动作识别任务列表(可选) │ ├── Layout # 人体部位布局列表(可选) │ ├── Main # 目标检测/分类任务列表 │ │ ├── train.txt │ │ ├── val.txt │ │ └── trainval.txt │ └── Segmentation # 分割任务列表 │ ├── train.txt │ ├── val.txt │ └── trainval.txt ├── JPEGImages # 原始 JPG 图片 │ ├── 2007_000001.jpg │ ├── 2007_000002.jpg │ └── … ├── SegmentationClass # 语义分割标签图(类别级 PNG) │ ├── 2007_000001.png │ ├── 2007_000002.png │ └── … └── SegmentationObject # 实例分割标签图(实例级 PNG) ├── 2007_000001.png ├── 2007_000002.png └── …

2.基于 JPEGImages(图片文件夹)进行划分

  • Pascal VOC格式的数据集时,划分训练集(train)和验证集(val)的标准做法是:
    对JPEGImages文件夹里的所有图片文件(或等价的XML列表)进行划分,生成ImageSets/Main下的train.txt、val.txt、trainval.txt等文件。这里注意的是按JPEGImages文件夹生成train.txt、val.txt也会是同样的。
  • 标准、最安全的操作流程如下
    • 操作对象:针对 JPEGImages/ 文件夹里的所有图片文件,提取它们的纯文件名(不包含.jpg扩展名),形成一个ID列表。
    • 划分动作:将这个ID列表随机打乱,然后按比例(如8:2)划分为train_ids和val_ids。更多的训练过程也会有text.txt测试集标签。
    • 保存结果:将这两个ID列表分别保存为 ImageSets/Main/train.txt 和 ImageSets/Main/val.txt。每个.txt文件的内容就是一行一个图片ID。
  • 早期 “经典 VOC 格式 + 传统 Faster R-CNN 实现”(如 py-faster-rcnn、jwyang/faster-rcnn.pytorch)场景,那些代码确实默认读取 ImageSets/Main/*.txt,而官方 VOC 数据集就是基于 XML 生成这些 txt 的。但在实际工程中(尤其是自定义数据集),图片缺失几乎不可能,但标注缺失或损坏很常见(标注漏了、文件损坏、路径错等)。因此,以图片为源头划分更安全、更鲁棒,已成为现代最佳实践。
  • 一个ID,统领全局:train.txt/val.txt里的一个ID,是你整个数据流水线的唯一钥匙,用来打开对应的图片文件、标注文件和后续的缓存文件。
  • 与缓存机制协同VOCDataset类会使用img_id作为缓存的键。因此,不同的划分(train/val)会自动生成不同的缓存文件,互不干扰。

对比基于两个文件夹不同

3.数据加载逻辑-以图片ID为中心

  • 训练代码加载时
    • 读取 txt 中的 ID
    • 加载 JPEGImages/{ID}.jpg
    • 加载对应的标注文件(Annotations/{ID}.xml 或 labels/{ID}.txt)
    • 如果标注文件不存在:报错或跳过(便于你清理数据)
# 你的代码逻辑解读def__getitem__(self,idx):img_id=self.ids[idx]# 1. 首先从 train.txt/val.txt 获取一个图片ID (如:2007_000032)img_path=os.path.join(self.img_dir,img_id+'.jpg')# 2. 拼接出图片路径annot_path=os.path.join(self.annot_dir,img_id+'.txt')# 3. 拼接出标注文件路径# ... 然后加载图片和对应的标注

4.根据模型/框架的具体建议

模型 / 框架类型推荐划分依据理由
经典 Faster R-CNN(py-faster-rcnn、jwyang 等老代码)Annotations (XML)这些代码硬依赖 VOC 标准结构,ImageSets/Main/*.txt 通常基于 XML 生成。
现代 PyTorch 实现(bubbliiiing、AIZOOTech 等国内仓库)JPEGImages (图片)它们通常用自定义 voc_annotation.py,你可以修改脚本让它遍历图片文件夹生成 train.txt。
torchvision FasterRCNN、MMDetection、Detectron2JPEGImages (图片)官方都以图片路径列表(或 COCO json 中的 images 字段)为主。
YOLO 系列(Ultralytics YOLOv5/v8/v10)JPEGImages (图片)data.yaml 中 train/val 指向图片文件夹,自动查找同名 .txt 标注。
自定义数据集(强烈建议)JPEGImages (图片)最安全、最灵活。

5.数据集划分代码

  • 按8:1:1比例随机划分训练集、验证集、测试集
#!/usr/bin/env python3# 带数据一致性检查功能:获取所有图片的ID。为每个ID查找对应的标注文件,即检查 Annotations_txt/2007_000032.txt 是否存在。报告缺失情况,告诉你哪些图片没有标注,帮你提前发现并修复数据问题。""" 数据集划分脚本:按8:1:1比例随机划分训练集、验证集、测试集 保存为ImageSets/Main/train.txt, val.txt, test.txt """importosimportrandomimportargparsefrompathlibimportPathfromtypingimportList,Tupledefsplit_dataset(data_root:str,jpeg_dir:str="JPEGImages",output_dir:str="ImageSets/Main",train_ratio:float=0.8,val_ratio:float=0.1,test_ratio:float=0.1,seed:int=42,shuffle:bool=True)->None:""" 随机划分数据集并保存划分结果 参数: data_root: VOC数据集根目录 jpeg_dir: 图片文件夹名称 output_dir: 输出文件夹名称 train_ratio: 训练集比例 val_ratio: 验证集比例 test_ratio: 测试集比例 seed: 随机种子(确保可复现) shuffle: 是否打乱数据 """# 验证比例总和为1total_ratio=train_ratio+val_ratio+test_ratioifabs(total_ratio-1.0)>0.001:raiseValueError(f"比例总和应为1.0,当前为{total_ratio}")# 设置随机种子random.seed(seed)# 构建路径jpeg_path=Path(data_root)/jpeg_dir output_path=Path(data_root)/output_dirprint("="*60)print("数据集划分工具")print("="*60)# 1. 获取所有图片文件ifnotjpeg_path.exists():raiseFileNotFoundError(f"图片文件夹不存在:{jpeg_path}")# 支持多种图片格式image_extensions={'.jpg','.jpeg','.png','.bmp'}image_files=[]forextinimage_extensions:image_files.extend(jpeg_path.glob(f'*{ext}'))image_files.extend(jpeg_path.glob(f'*{ext.upper()}'))ifnotimage_files:raiseFileNotFoundError(f"在{jpeg_path}中未找到任何图片文件")# 提取纯文件名(不带扩展名)image_ids=[]forimg_fileinimage_files:# 保留原始文件名,去除扩展名image_ids.append(img_file.stem)print(f"找到{len(image_ids)}张图片")# 2. 去重并排序(确保可复现)image_ids=list(set(image_ids))# 去重image_ids.sort()# 排序,确保每次相同的顺序# 3. 打乱顺序ifshuffle:print(f"使用随机种子{seed}打乱数据顺序...")random.shuffle(image_ids)# 4. 计算划分点total_count=len(image_ids)train_count=int(total_count*train_ratio)val_count=int(total_count*val_ratio)# 处理可能的舍入误差,确保测试集包含所有剩余图片test_count=total_count-train_count-val_count# 5. 执行划分train_ids=image_ids[:train_count]val_ids=image_ids[train_count:train_count+val_count]test_ids=image_ids[train_count+val_count:]print("\n划分结果统计:")print("-"*40)print(f"训练集:{len(train_ids)}张 ({len(train_ids)/total_count*100:.1f}%)")print(f"验证集:{len(val_ids)}张 ({len(val_ids)/total_count*100:.1f}%)")print(f"测试集:{len(test_ids)}张 ({len(test_ids)/total_count*100:.1f}%)")print(f"总计:{total_count}张")# 6. 创建输出目录output_path.mkdir(parents=True,exist_ok=True)# 7. 保存划分文件train_file=output_path/"train.txt"val_file=output_path/"val.txt"test_file=output_path/"test.txt"withopen(train_file,'w',encoding='utf-8')asf:f.write('\n'.join(train_ids))withopen(val_file,'w',encoding='utf-8')asf:f.write('\n'.join(val_ids))withopen(test_file,'w',encoding='utf-8')asf:f.write('\n'.join(test_ids))print("\n划分文件已保存:")print(f"{train_file}")print(f"{val_file}")print(f"{test_file}")# 8. 保存划分详情(可选)detail_file=output_path/"split_details.txt"withopen(detail_file,'w',encoding='utf-8')asf:f.write("数据集划分详情\n")f.write("="*40+"\n")f.write(f"数据根目录:{data_root}\n")f.write(f"随机种子:{seed}\n")f.write(f"总图片数:{total_count}\n")f.write(f"训练集:{len(train_ids)}({train_ratio*100:.1f}%)\n")f.write(f"验证集:{len(val_ids)}({val_ratio*100:.1f}%)\n")f.write(f"测试集:{len(test_ids)}({test_ratio*100:.1f}%)\n")f.write("\n划分比例: 训练集:验证集:测试集 = ")f.write(f"{train_ratio}:{val_ratio}:{test_ratio}\n")print(f"划分详情:{detail_file}")print("="*60)defcheck_annotation_compatibility(data_root:str,jpeg_dir:str="JPEGImages",annot_dir:str="Annotations_txt")->None:""" 检查图片文件和标注文件的对应关系 参数: data_root: 数据集根目录 jpeg_dir: 图片文件夹 annot_dir: 标注文件夹 """print("\n检查标注文件兼容性...")jpeg_path=Path(data_root)/jpeg_dir annot_path=Path(data_root)/annot_dir# 获取图片文件image_files=list(jpeg_path.glob('*.jpg'))image_ids=[f.stemforfinimage_files]missing_annotations=[]forimg_idinimage_ids:annot_file=annot_path/f"{img_id}.txt"ifnotannot_file.exists():missing_annotations.append(img_id)ifmissing_annotations:print(f"警告:{len(missing_annotations)}张图片缺少对应的标注文件")iflen(missing_annotations)<=10:print("缺失标注的图片ID:")forimg_idinmissing_annotations:print(f" -{img_id}")else:print("前10个缺失标注的图片ID:")forimg_idinmissing_annotations[:10]:print(f" -{img_id}")print(f" ... 共{len(missing_annotations)}个")else:print("✓ 所有图片都有对应的标注文件")defmain():"""命令行入口函数"""parser=argparse.ArgumentParser(description="数据集划分工具 (8:1:1比例)")parser.add_argument("--data_root",type=str,default="./VOC2012",help="VOC数据集根目录 (默认: ./VOC2012)")parser.add_argument("--jpeg_dir",type=str,default="JPEGImages",help="图片文件夹名称 (默认: JPEGImages)")parser.add_argument("--output_dir",type=str,default="ImageSets/Main",help="输出文件夹名称 (默认: ImageSets/Main)")parser.add_argument("--train_ratio",type=float,default=0.8,help="训练集比例 (默认: 0.8)")parser.add_argument("--val_ratio",type=float,default=0.1,help="验证集比例 (默认: 0.1)")parser.add_argument("--test_ratio",type=float,default=0.1,help="测试集比例 (默认: 0.1)")parser.add_argument("--seed",type=int,default=42,help="随机种子 (默认: 42)")parser.add_argument("--no_shuffle",action="store_true",help="不打乱数据顺序")parser.add_argument("--check_annotations",action="store_true",help="检查标注文件兼容性")args=parser.parse_args()try:# 执行划分split_dataset(data_root=args.data_root,jpeg_dir=args.jpeg_dir,output_dir=args.output_dir,train_ratio=args.train_ratio,val_ratio=args.val_ratio,test_ratio=args.test_ratio,seed=args.seed,shuffle=notargs.no_shuffle)# 如果需要,检查标注文件ifargs.check_annotations:check_annotation_compatibility(data_root=args.data_root,jpeg_dir=args.jpeg_dir,annot_dir="Annotations_txt"# 你的标注文件夹)exceptExceptionase:print(f"错误:{e}")return1return0if__name__=="__main__":exit(main())

6.Annotations文件夹XML标注转换为TXT格式

  • 输出文件夹路径nnotations_txt
importosimportxml.etree.ElementTreeasET# VOC2012的20个类别(按顺序对应class_id 1-20)VOC_CLASSES=['aeroplane','bicycle','bird','boat','bottle','bus','car','cat','chair','cow','diningtable','dog','horse','motorbike','person','pottedplant','sheep','sofa','train','tvmonitor']defxml_to_txt(xml_dir,txt_dir):""" 将VOC XML标注转换为TXT格式 :param xml_dir: XML标注文件所在目录(如VOCdevkit/VOC2007/Annotations) :param txt_dir: 输出TXT文件的保存目录 """# 创建输出目录(如果不存在)os.makedirs(txt_dir,exist_ok=True)# 遍历所有XML文件forxml_filenameinos.listdir(xml_dir):ifnotxml_filename.endswith('.xml'):continue# 只处理.xml文件# 解析XMLxml_path=os.path.join(xml_dir,xml_filename)tree=ET.parse(xml_path)root=tree.getroot()# 获取图像尺寸(可选,用于验证坐标是否合理)size=root.find('size')width=int(size.find('width').text)height=int(size.find('height').text)# 提取所有目标的标注txt_content=[]forobjinroot.iter('object'):# 获取类别名称cls_name=obj.find('name').text.strip().lower()ifcls_namenotinVOC_CLASSES:continue# 跳过不在VOC类别中的目标# 转换类别名称为class_id(1-20)cls_id=VOC_CLASSES.index(cls_name)+1# 索引+1,确保从1开始# 获取边界框坐标(xmin, ymin, xmax, ymax)bbox=obj.find('bndbox')x1=float(bbox.find('xmin').text)y1=float(bbox.find('ymin').text)x2=float(bbox.find('xmax').text)y2=float(bbox.find('ymax').text)# 确保坐标在图像范围内(可选,防止越界)x1=max(0,min(x1,width))y1=max(0,min(y1,height))x2=max(x1,min(x2,width))y2=max(y1,min(y2,height))# 写入TXT内容(格式:x1 y1 x2 y2 class_id)txt_content.append(f"{x1}{y1}{x2}{y2}{cls_id}")# 保存为TXT文件(与XML同名,后缀改为.txt)txt_filename=xml_filename.replace('.xml','.txt')txt_path=os.path.join(txt_dir,txt_filename)withopen(txt_path,'w')asf:f.write('\n'.join(txt_content))# 打印进度if(len(os.listdir(txt_dir))%100==0):print(f"已转换{len(os.listdir(txt_dir))}个文件...")print(f"转换完成!共处理{len(os.listdir(txt_dir))}个XML文件,保存至{txt_dir}")# -------------------------- 运行转换脚本 --------------------------if__name__=="__main__":# 请替换为你的实际路径xml_dir="./VOC2012/Annotations"# VOC原始XML标注目录txt_dir="./VOC2012/Annotations_txt"# 输出TXT目录(需与RCNN代码中的annot_dir一致)# 执行转换xml_to_txt(xml_dir,txt_dir)
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/1 14:55:39

堆排序和topk问题

系列文章目录 文章目录系列文章目录前言一、堆排序定义二、时间复杂度三、实现思路a.注意&#xff08;升/降&#xff09;四、topk问题前言 常见的基本排序算法有冒泡、选择、插入&#xff0c;但效率太低。 堆排序和快速排序算法则是相对高效的算法。这篇主要先介绍堆排序 一、…

作者头像 李华
网站建设 2026/5/1 11:00:52

Java毕设项目推荐-基于springboot的幼儿园管理系统的设计与实现家校互动(通知推送、留言沟通)、膳食营养规划【附源码+文档,调试定制服务】

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/5/1 3:51:50

Java毕设项目推荐-基于springboot小区团购管理设计与实现基于springboot的社区团购系统的设计与实现【附源码+文档,调试定制服务】

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/5/1 12:25:23

《AI应用架构师:在AI驱动数字转型的浪潮中破浪前行》

AI应用架构师&#xff1a;在AI驱动数字转型的浪潮中破浪前行 引言&#xff1a;为什么你的AI项目总是“翻车”&#xff1f; 凌晨三点&#xff0c;某零售企业的技术总监盯着电脑屏幕发呆——他们花了120万采购的AI推荐系统上线3个月&#xff0c;用户转化率没涨反降&#xff0c;…

作者头像 李华
网站建设 2026/5/1 10:52:44

android kotlinx.serialization用法和封装全解

替代Gson、fastJson等传统java的json解析工具。抛弃传统的反射类解析字段&#xff0c;利用kotlin的inlinereified特性和android可以预编译的特点&#xff0c;在编译阶段的时候&#xff0c;还原最后的类型&#xff0c;来实现的json序列化与反序列化。 性能效率&#xff1a;不做评…

作者头像 李华