划分基准 推荐度 优点 缺点/风险 适用场景 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、Detectron2 JPEGImages (图片) 官方都以图片路径列表(或 COCO json 中的 images 字段)为主。 YOLO 系列(Ultralytics YOLOv5/v8/v10) JPEGImages (图片) data.yaml 中 train/val 指向图片文件夹,自动查找同名 .txt 标注。 自定义数据集(强烈建议) JPEGImages (图片) 最安全、最灵活。
5.数据集划分代码 #!/usr/bin/env python3 # 带数据一致性检查功能:获取所有图片的ID。为每个ID查找对应的标注文件,即检查 Annotations_txt/2007_000032.txt 是否存在。报告缺失情况,告诉你哪些图片没有标注,帮你提前发现并修复数据问题。 """ 数据集划分脚本:按8:1:1比例随机划分训练集、验证集、测试集 保存为ImageSets/Main/train.txt, val.txt, test.txt """ import osimport randomimport argparsefrom pathlibimport Pathfrom typingimport List, Tupledef split_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: 是否打乱数据 """ # 验证比例总和为1 total_ratio= train_ratio+ val_ratio+ test_ratioif abs ( total_ratio- 1.0 ) > 0.001 : raise ValueError( 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. 获取所有图片文件 if not jpeg_path. exists( ) : raise FileNotFoundError( f"图片文件夹不存在: { jpeg_path} " ) # 支持多种图片格式 image_extensions= { '.jpg' , '.jpeg' , '.png' , '.bmp' } image_files= [ ] for extin image_extensions: image_files. extend( jpeg_path. glob( f'* { ext} ' ) ) image_files. extend( jpeg_path. glob( f'* { ext. upper( ) } ' ) ) if not image_files: raise FileNotFoundError( f"在 { jpeg_path} 中未找到任何图片文件" ) # 提取纯文件名(不带扩展名) image_ids= [ ] for img_filein image_files: # 保留原始文件名,去除扩展名 image_ids. append( img_file. stem) print ( f"找到 { len ( image_ids) } 张图片" ) # 2. 去重并排序(确保可复现) image_ids= list ( set ( image_ids) ) # 去重 image_ids. sort( ) # 排序,确保每次相同的顺序 # 3. 打乱顺序 if shuffle: 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" with open ( train_file, 'w' , encoding= 'utf-8' ) as f: f. write( '\n' . join( train_ids) ) with open ( val_file, 'w' , encoding= 'utf-8' ) as f: f. write( '\n' . join( val_ids) ) with open ( test_file, 'w' , encoding= 'utf-8' ) as f: 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" with open ( detail_file, 'w' , encoding= 'utf-8' ) as f: 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 ) def check_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. stemfor fin image_files] missing_annotations= [ ] for img_idin image_ids: annot_file= annot_path/ f" { img_id} .txt" if not annot_file. exists( ) : missing_annotations. append( img_id) if missing_annotations: print ( f"警告: { len ( missing_annotations) } 张图片缺少对应的标注文件" ) if len ( missing_annotations) <= 10 : print ( "缺失标注的图片ID:" ) for img_idin missing_annotations: print ( f" - { img_id} " ) else : print ( "前10个缺失标注的图片ID:" ) for img_idin missing_annotations[ : 10 ] : print ( f" - { img_id} " ) print ( f" ... 共 { len ( missing_annotations) } 个" ) else : print ( "✓ 所有图片都有对应的标注文件" ) def main ( ) : """命令行入口函数""" 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= not args. no_shuffle) # 如果需要,检查标注文件 if args. check_annotations: check_annotation_compatibility( data_root= args. data_root, jpeg_dir= args. jpeg_dir, annot_dir= "Annotations_txt" # 你的标注文件夹 ) except Exceptionas e: print ( f"错误: { e} " ) return 1 return 0 if __name__== "__main__" : exit( main( ) ) 6.Annotations文件夹XML标注转换为TXT格式 import osimport xml. etree. ElementTreeas ET# 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' ] def xml_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文件 for xml_filenamein os. listdir( xml_dir) : if not xml_filename. endswith( '.xml' ) : continue # 只处理.xml文件 # 解析XML xml_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= [ ] for objin root. iter ( 'object' ) : # 获取类别名称 cls_name= obj. find( 'name' ) . text. strip( ) . lower( ) if cls_namenot in VOC_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) with open ( txt_path, 'w' ) as f: 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)