voc2yolo.py
yolov6\data\voc2yolo.py
目录
voc2yolo.py
1.所需的库和模块
2.def convert_label(path, lb_path, year, image_id):
3.def gen_voc07_12(voc_path):
4.def main(args):
5.if __name__ == '__main__':
1.所需的库和模块
import xml.etree.ElementTree as ET
from tqdm import tqdm
import os
import shutil
import argparse# VOC dataset (refer https://github.com/ultralytics/yolov5/blob/master/data/VOC.yaml)
# VOC2007 trainval: 446MB, 5012 images
# VOC2007 test: 438MB, 4953 images
# VOC2012 trainval: 1.95GB, 17126 images# VOC_NAMES 是一个包含 Pascal VOC 数据集类别名称的列表。
VOC_NAMES = ['aeroplane', 'bicycle', 'bird', 'boat', 'bottle', 'bus', 'car', 'cat', 'chair', 'cow', 'diningtable', 'dog','horse', 'motorbike', 'person', 'pottedplant', 'sheep', 'sofa', 'train', 'tvmonitor']
2.def convert_label(path, lb_path, year, image_id):
# 它用于将 Pascal VOC 数据集中的 XML 格式的标签文件转换为 YOLO 格式的文本标签文件。Pascal VOC 数据集的标签文件通常以 XML 格式存储,而 YOLO 格式的标签文件则是一个文本文件,其中每一行代表一个边界框,包含类别 ID 和边界框的坐标。
# 1.path :包含 XML 标签文件的目录的路径。
# 2.lb_path :输出的 YOLO 格式标签文件的路径。
# 3.year :Pascal VOC 数据集的年份,例如 2007 或 2012 。
# 4.image_id :图像的 ID,它对应于 XML 标签文件的名称(不包括 .xml 扩展名)。
def convert_label(path, lb_path, year, image_id):# 函数的目的是将边界框(bounding box)的坐标从绝对像素值转换为相对于图像宽度和高度的比例值。这种转换在目标检测任务中很常见,因为它使得模型对不同尺寸的图像具有更好的泛化能力。# 1.size :一个包含两个元素的元组或列表,分别代表图像的宽度和高度。# 2.box :一个包含四个元素的列表或元组,代表边界框的坐标,通常是 [xmin, xmax, ymin, ymax] 。def convert_box(size, box):# dw 和 dh 分别计算为图像宽度和高度的倒数。这些值将用于将边界框的宽度和高度转换为相对于图像大小的比例值。dw, dh = 1. / size[0], 1. / size[1]# x 和 y 计算边界框的中心点坐标。这是通过取边界框左右( xmin 和 xmax )或上下( ymin 和 ymax )坐标的平均值,然后减去 1 来实现的。减去 1 的操作是为了将坐标从 0 到宽度/高度的范围转换为 0 到 1 的范围。# w 和 h 分别计算边界框的宽度和高度。这是通过从 xmax 减去 xmin 和从 ymax 减去 ymin 来实现的。x, y, w, h = (box[0] + box[1]) / 2.0 - 1, (box[2] + box[3]) / 2.0 - 1, box[1] - box[0], box[3] - box[2]# 最后,函数返回边界框的中心点坐标( x 和 y )以及宽度和高度( w 和 h ),它们都已经被转换为相对于图像大小的比例值。return x * dw, y * dh, w * dw, h * dh# 使用 os.path.join 构建 XML 文件的完整路径。这个路径由用户提供的 path (标签文件所在的根目录)、数据集的年份 year (如 2007 或 2012 ),以及图像的 ID image_id 组成。# open 函数用于打开这个 XML 文件,返回一个文件对象 in_file 。in_file = open(os.path.join(path, f'VOC{year}/Annotations/{image_id}.xml'))# 使用 open 函数以写入模式( 'w' )打开输出文件,返回一个文件对象 out_file 。如果文件已存在,则覆盖;如果不存在,则创建。out_file = open(lb_path, 'w')# 使用 xml.etree.ElementTree (通常缩写为 ET )模块的 parse 方法解析打开的 XML 文件对象 in_file ,返回一个包含 XML 文件内容的树形结构对象 tree 。tree = ET.parse(in_file)# 从 tree 对象中获取 XML 文件的根元素,并将其存储在 root 变量中。root = tree.getroot()# 在 root 元素中查找 <size> 子元素,并将其存储在 size 变量中。 <size> 元素包含了图像的尺寸信息。size = root.find('size')# 在 size 元素中查找 <width> 子元素,获取其文本内容(即图像的宽度),并将其转换为整数类型,存储在变量 w 中。w = int(size.find('width').text)# 类似地,在 size 元素中查找 <height> 子元素,获取其文本内容(即图像的高度),并将其转换为整数类型,存储在变量 h 中。h = int(size.find('height').text)# 使用 iter 方法遍历 XML 根元素 root 下所有的 <object> 子元素。每个 <object> 元素代表一个边界框及其相关信息。for obj in root.iter('object'):# 对于每个 <object> 元素,查找 <name> 子元素,获取其文本内容,即边界框所属的类别名称,并将其存储在变量 cls 中。cls = obj.find('name').text# 检查类别名称 cls 是否在 VOC_NAMES 列表中。 VOC_NAMES 是一个预定义的类别名称列表,确保只处理已知的类别。# 同时检查 <object> 元素中的 <difficult> 子元素的值。如果该值为 1 ,则表示该边界框是一个困难样本(例如,对象被遮挡或难以识别),则跳过该边界框。if cls in VOC_NAMES and not int(obj.find('difficult').text) == 1:# 如果类别有效且边界框不是困难样本,则查找 <object> 元素中的 <bndbox> 子元素,它包含了边界框的坐标信息,并将其存储在变量 xmlbox 中。xmlbox = obj.find('bndbox')# 使用之前定义的 convert_box 函数,将边界框的坐标从绝对像素值转换为相对于图像宽度和高度的比例值。 convert_box 函数的参数是图像的尺寸 w 和 h ,以及边界框的坐标。bb = convert_box((w, h), [float(xmlbox.find(x).text) for x in ('xmin', 'xmax', 'ymin', 'ymax')])# 获取类别名称 cls 在 VOC_NAMES 列表中的索引,这个索引将作为类别 ID。cls_id = VOC_NAMES.index(cls) # class id# 将类别 ID 和转换后的边界框坐标 bb 转换为字符串,并使用空格连接成一个字符串。# 将这个字符串写入输出文件 out_file ,并在每个边界框后添加换行符。out_file.write(" ".join([str(a) for a in (cls_id, *bb)]) + '\n')
3.def gen_voc07_12(voc_path):
def gen_voc07_12(voc_path):# 生成voc07+12设置数据集:# 训练:# 训练图像 16551 幅图像# - 图像/train2012# - 图像/train2007# - 图像/val2012# - 图像/val2007'''Generate voc07+12 setting dataset:train: # train images 16551 images- images/train2012- images/train2007- images/val2012- images/val2007val: # val images (relative to 'path') 4952 images- images/test2007'''# 使用 os.path.join 方法来连接 voc_path (一个已经定义的变量,指向包含 Pascal VOC 数据集的目录)和子目录 'voc_07_12' ,形成数据集的根目录路径。# 这个路径指向 Pascal VOC 数据集的一个特定版本的目录,通常是数据集年份和分割的组合(例如, voc_07_12 可能代表 2007 年的数据集,包括训练和验证集)。# os.path.join(path, *paths)# 参数 :# path :第一个路径部分。# *paths :任意数量的额外路径部分。# 返回值 :# 返回一个组合后的路径字符串。dataset_root = os.path.join(voc_path, 'voc_07_12')# 使用 os.path.exists 方法检查 dataset_root 路径是否存在。 not 操作符用于检查路径是否不存在。if not os.path.exists(dataset_root):# 如果路径不存在,使用 os.makedirs 方法创建该路径及其所有必要的父目录。# makedirs 方法可以创建中间缺失的目录,并且如果目录已经存在,它不会抛出错误(与 mkdir 方法不同,后者如果目录已存在会抛出错误)。os.makedirs(dataset_root)# 这段代码负责设置数据集的不同部分(训练集、验证集)和它们对应的图像及标签文件,并将它们从原始位置复制到新的组织结构中。# dataset_settings 是一个字典,定义了数据集的不同部分( train 和 val )以及它们对应的数据集名称列表。例如,训练集包括 train2007 、 val2007 、 train2012 和 val2012 。dataset_settings = {'train': ['train2007', 'val2007', 'train2012', 'val2012'], 'val':['test2007']}# 外层循环遍历 ['images', 'labels'] 列表,分别处理图像文件和标签文件。for item in ['images', 'labels']:# 中层循环遍历 dataset_settings 字典,对于每个键值对( data_type , data_list ),其中 data_type 是 'train' 或 'val' , data_list 是对应的数据集名称列表。for data_type, data_list in dataset_settings.items():# 内层循环遍历 data_list ,对于每个数据集名称 data_name :# 构建原始数据路径 ori_path ,它指向原始 VOC 数据集的图像或标签目录。 构建新的数据路径 new_path ,它指向数据应该被复制到的目标目录。for data_name in data_list:ori_path = os.path.join(voc_path, item, data_name)new_path = os.path.join(dataset_root, item, data_type)# 如果目标目录 new_path 不存在,则使用 os.makedirs(new_path) 创建该目录及其所有必要的父目录。if not os.path.exists(new_path):os.makedirs(new_path)# 打印一条信息,表明正在将文件从 ori_path 复制到 new_path 。print(f'[INFO]: Copying {ori_path} to {new_path}') # [INFO]: 将 {ori_path} 复制到 {new_path}。# 使用 os.listdir(ori_path) 获取原始路径下的所有文件名,然后遍历这些文件名。for file in os.listdir(ori_path):# 对于每个文件,使用 shutil.copy 函数将文件从 ori_path 复制到 new_path 。# shutil.copy(src, dst, *, follow_symlinks=True)# shutil.copy() 是 Python 标准库 shutil 模块中的一个函数,用于复制文件。这个函数会将源文件复制到目标文件,如果目标文件已存在,则会覆盖它。如果目标文件是一个目录,那么源文件名将被用作目标文件名。# 参数 :# src :源文件路径。# dst :目标文件路径。如果 dst 是一个存在的目录,则会在该目录下创建一个与源文件同名的文件。# follow_symlinks (默认为 True ) :一个布尔值,指示是否应该复制符号链接指向的实际文件,而不是符号链接本身。如果设置为 False ,则复制符号链接文件。# 返回值 :# 该函数没有返回值。# 异常 :# 如果源文件不存在或无法读取,或者目标文件无法写入,可能会抛出 IOError 或 FileNotFoundError 。shutil.copy(os.path.join(ori_path, file), new_path)
4.def main(args):
def main(args):# 获取存储 Pascal VOC 数据集的根目录路径,这个路径是通过命令行参数提供的。voc_path = args.voc_path# 循环遍历一个元组列表,每个元组包含一个年份和一个图像集标识符。这个列表定义了要处理的数据集的年份和图像集。例如,它包括 2012 年的训练集和验证集,以及 2007 年的训练集、验证集和测试集。for year, image_set in ('2012', 'train'), ('2012', 'val'), ('2007', 'train'), ('2007', 'val'), ('2007', 'test'):# 使用 os.path.join 函数构建图像文件的路径。这个路径由 VOC 数据集的根目录 voc_path 、子目录 'images' 和当前的图像集标识符 image_set 组成。# f'{image_set}' 是一个格式化字符串,用于插入当前的图像集标识符。imgs_path = os.path.join(voc_path, 'images', f'{image_set}')# 类似地,构建标签文件的路径。这个路径由 VOC 数据集的根目录 voc_path 、子目录 'labels' 和当前的图像集标识符 image_set 组成。lbs_path = os.path.join(voc_path, 'labels', f'{image_set}')# 例如,如果 voc_path 是 '/path/to/VOCdevkit' ,那么对于 2012 年的训练集, imgs_path 将是 '/path/to/VOCdevkit/images/train' , lbs_path 将是 '/path/to/VOCdevkit/labels/train' 。try:# 使用 with 语句和 open 函数打开一个文本文件,该文件包含当前年份 year 和图像集 image_set 的图像 ID 列表。# 文件路径是通过 voc_path 、 VOC{year} 、 ImageSets/Main/ 和 {image_set}.txt 拼接而成的。with open(os.path.join(voc_path, f'VOC{year}/ImageSets/Main/{image_set}.txt'), 'r') as f:# file_object.read([size])# 在 Python 中, .read() 方法用于从文件对象中读取数据。这个方法是文件对象的一部分,可以通过带 open 函数打开的任何文件来调用。# 参数 :# size (可选):一个整数,指定要读取的字节数。如果未指定或为负数,则读取整个文件,直到文件末尾(EOF)。# 返回值 :# 返回一个字符串(在文本模式下打开文件时)或字节串(在二进制模式下打开文件时),包含从文件中读取的数据。# 异常 :# 如果在读取过程中遇到如文件未打开或已关闭等错误,可能会抛出 IOError 或 OSError 。# str.strip([chars])# 删除 string 字符串末尾的指定字符,默认为空白符,包括空格、换行符、回车符、制表符。# chars :指定删除的字符(默认为空白符)。# str.split(str="", num=string.count(str))# 通过指定分隔符对字符串进行切片,如果参数 num 有指定值,则分隔 num+1 个子字符串。# str :分隔符,默认为所有的空字符,包括空格、换行(\n)、制表符(\t)等。# num :分割次数。默认为 -1, 即分隔所有。# 读取文件内容,使用 strip() 方法去除可能的前后空白字符,然后使用 split() 方法将字符串分割成图像 ID 列表。image_ids = f.read().strip().split()# 检查图像路径 imgs_path 是否存在,如果不存在,则使用 os.makedirs 创建该路径及其所有必要的父目录。if not os.path.exists(imgs_path):os.makedirs(imgs_path)# 类似地,检查标签路径 lbs_path 是否存在,如果不存在,则创建。if not os.path.exists(lbs_path):os.makedirs(lbs_path)# 使用 tqdm 库来显示进度条,遍历所有的图像 ID。# desc 参数提供了进度条的描述,这里使用 f'{image_set}{year}' 作为描述。# tqdm()# tqdm() 进度条 的常见参数有:# iterable :要迭代的对象,可以是列表、元组、集合等可迭代对象。# desc :进度条的描述文本,显示在进度条的左侧。# total :迭代对象的总大小,用于计算进度百分比。如果不指定,则进度条将根据迭代对象的长度自动确定。# leave :进度条完成后是否保留在输出中。值为True或False。# ncols :进度条的宽度(以字符为单位),用于限制进度条的宽度。如果ncols的值为正整数N(N > 0),则进度条的宽度将被限制为N个字符。# 默认情况下或者ncols的值为0、负数或None,则进度条的宽度将根据终端的宽度自动调整。# bar_format :进度条的样式格式字符串。它可以包含特定的占位符,例如"{l_bar}{bar}{r_bar}"表示左边的文本、进度条本身和右边的文本。你可以在占位符中添加自定义的文本或符号来美化进度条。# unit :进度条的单位名称,用于显示在进度百分比后面。例如,如果单位为"bytes",则进度条将显示为"10/100 bytes"。# unit_divisor :进度条的单位除数,默认为1。可以用于将进度条的单位转换为更适合显示的单位格式。例如,如果单位为"bytes",但实际值是以KB为单位的,你可以将unit_divisor设置为1024,以便显示为"10/100 KB"。# color :进度条的颜色,可以是ANSI颜色代码或预定义的颜色名称。ANSI颜色代码如,“\033[31m”表示红色,“\033[32m”表示绿色。# tqdm库还提供了一些预定义的颜色名称,包括:black、red、green、yellow、blue、magenta、cyan、white。(如果设置颜色会报错,那可能是终端不支持)for id in tqdm(image_ids, desc=f'{image_set}{year}'):# 构建原始图像文件的完整路径。f = os.path.join(voc_path, f'VOC{year}/JPEGImages/{id}.jpg') # old img path# 构建新标签文件的完整路径。lb_path = os.path.join(lbs_path, f'{id}.txt') # new label path# def convert_label(path, lb_path, year, image_id): -> 它用于将 Pascal VOC 数据集中的 XML 格式的标签文件转换为 YOLO 格式的文本标签文件。# 调用 convert_label 函数,将 Pascal VOC 数据集中的 XML 格式的标签文件转换为 YOLO 格式的文本标签文件。convert_label(voc_path, lb_path, year, id) # convert labels to YOLO format# 如果原始图像文件存在,则使用 shutil.move 函数将其移动到新的图像路径 imgs_path 。if os.path.exists(f):# shutil.move(src, dst, copy_function=copy2)# shutil.move() 是 Python 标准库 shutil 模块中的一个函数,用于将文件或目录从一个地方移动到另一个地方。如果目标位置已经存在同名文件或目录,该函数会将其覆盖。# 参数 :# src :源路径,可以是文件或目录。# dst :目标路径。如果 src 是文件, dst 可以是文件或目录。如果 src 是目录, dst 必须是目录。# copy_function (默认为 copy2 ) :用于复制文件的函数。通常使用 copy2 来尽可能地保留文件的元数据,但也可以使用 copy 或其他自定义函数。# 返回值 :# 该函数没有返回值。# 异常 :# 如果源文件或目录不存在,可能会抛出 FileNotFoundError 。# 如果目标位置无法写入,可能会抛出 PermissionError 。# 其他 I/O 相关的错误。shutil.move(f, imgs_path) # move imageexcept Exception as e:# 如果在 try 块中的代码执行过程中发生任何异常,将捕获该异常并打印一条警告消息,包括异常信息 e 和当前处理的年份和图像集标识符。print(f'[Warning]: {e} {year}{image_set} convert fail!')# gen_voc07_12 方法用于将 Pascal VOC 数据集的 2007 和 2012 年版本结合起来,并转换成适合 YOLO 训练的格式。gen_voc07_12(voc_path)
5.if __name__ == '__main__':
if __name__ == '__main__':parser = argparse.ArgumentParser()parser.add_argument('--voc_path', default='VOCdevkit')args = parser.parse_args()print(args)main(args)