引言
随着深度学习的快速发展,目标检测任务在计算机视觉领域中占据了重要地位。无论是自动驾驶、安防监控,还是农业监测等领域,目标检测技术都得到了广泛应用。在目标检测任务中,数据集的质量直接影响模型的训练效果和检测精度,标注格式也多种多样。当前,YOLO(You Only Look Once)系列模型因其实时性和高效性在目标检测中非常受欢迎,而 VOC(PASCAL Visual Object Classes)数据集格式则广泛应用于多个经典目标检测框架,如 Faster R-CNN 和 SSD(Single Shot MultiBox Detector)。这两种格式在标注文件的结构和坐标表示上存在显著差异,因此进行数据集格式转换显得尤为重要。
在实际项目中,不同的目标检测框架往往使用不同的数据集标注格式。YOLO 模型采用简单的 .txt
文件进行标注,每一行包含一个物体的类别和位置信息,并且这些坐标是归一化的相对坐标。而 VOC 格式则使用 .xml
文件来描述图片中的物体信息,文件结构更复杂,包含图片的尺寸、物体的类别和精确的绝对坐标信息。
同时,针对数据集图片数量众多,也可采取先部分标注训练,再将训练好的模型检测未标注的图片,同时保存标注文件,这时保存的标注文件大多数是YOLO格式的,如果需要更直观的标注以及后续对数据集进行增强等则需要将YOLO格式转为VOC格式,即.txt转为.xml文件。
目前网络上关于此类数据转换可用的代码较少,因此给出一个单文件稳定可用的版本。
准备工作
两个文件夹,分别是只存放数据集图片的文件夹和只存放标注文件的文本文件夹
python的numpy库及opencv库,运行如下代码
pip install numpy opencv-python
思路介绍
YOLO格式的标注文件是归一化之后的数据,每一行有5个数值,从左到右分别是类别,中心坐标x,中心坐标y,宽度w,高度h,这些数据经过计算后即可得到xmin,ymin,xmax,ymax的值,将这些信息相应的填入xml文件中即可。
xmin = float(box[1] - 0.5 * box[3]) * w
ymin = float(box[2] - 0.5 * box[4]) * h
xmax = float(xmin + box[3] * w)
ymax = float(ymin + box[4] * h)
还有一个关键部分是xml文件的构建,如下代码构建,变量确认后替换。
out0 = '''<annotation>
<folder>%(folder)s</folder>
<filename>%(name)s</filename>
<path>%(path)s</path>
<source>
<database>None</database>
</source>
<size>
<width>%(width)d</width>
<height>%(height)d</height>
<depth>3</depth>
</size>
<segmented>0</segmented>
'''
out1 = ''' <object>
<name>%(class)s</name>
<pose>Unspecified</pose>
<truncated>0</truncated>
<difficult>0</difficult>
<bndbox>
<xmin>%(xmin)d</xmin>
<ymin>%(ymin)d</ymin>
<xmax>%(xmax)d</xmax>
<ymax>%(ymax)d</ymax>
</bndbox>
</object>
'''
out2 = '''</annotation>
'''
完整代码
完整代码如下,使用时需要替换文件夹路径并将类别按照顺序替换
# 作者:CSDN-笑脸惹桃花 https://blog.csdn.net/qq_67105081?type=blog
# github:peng-xiaobai https://github.com/peng-xiaobai/Dataset-Conversion
import os
import cv2
import numpy as np
#xml文件格式
out0 = '''<annotation>
<folder>%(folder)s</folder>
<filename>%(name)s</filename>
<path>%(path)s</path>
<source>
<database>None</database>
</source>
<size>
<width>%(width)d</width>
<height>%(height)d</height>
<depth>3</depth>
</size>
<segmented>0</segmented>
'''
out1 = ''' <object>
<name>%(class)s</name>
<pose>Unspecified</pose>
<truncated>0</truncated>
<difficult>0</difficult>
<bndbox>
<xmin>%(xmin)d</xmin>
<ymin>%(ymin)d</ymin>
<xmax>%(xmax)d</xmax>
<ymax>%(ymax)d</ymax>
</bndbox>
</object>
'''
out2 = '''</annotation>
'''
def upp2low(directory):
converted_count = 0
# 检查目录是否存在
if not os.path.exists(directory):
raise FileNotFoundError(f"Directory {directory} does not exist.")
# 遍历文件夹中的所有文件
for filename in os.listdir(directory):
file_path = os.path.join(directory, filename)
# 仅处理文件
if os.path.isfile(file_path):
# 拆分文件名和后缀
name, extension = os.path.splitext(filename)
# 检查后缀是否为大写
if extension.isupper():
new_filename = name + extension.lower()
new_file_path = os.path.join(directory, new_filename)
# 重命名文件
os.rename(file_path, new_file_path)
converted_count += 1
print(f"Renamed: {filename} -> {new_filename}")
print(f"All file suffixes in the folder are lowercase, and a total of {converted_count} files have been processed")
return converted_count
def yolo2voc(dir1,dir2,dir3,Class):
file = os.listdir(dir1)
source = {}
label = {}
for img in file:
print(img)
if img.endswith(('.png', '.jpg', '.jpeg', '.bmp', '.tif')):
img1 = os.path.join(dir1, img)
image = cv2.imread(img1) # 路径不能有中文
h, w, _ = image.shape
name, extension = os.path.splitext(img)
name1 = name + '.xml'
name2 = name + '.txt'
fxml = os.path.join(dir2, name1)
txt = os.path.join(dir3, name2)
if not os.path.exists(txt):
print(f"{name2}未找到,已跳过")
continue
fxml = open(fxml, 'w')
source['name'] = img
source['path'] = img1
source['folder'] = os.path.basename(dir1)
source['width'] = w
source['height'] = h
fxml.write(out0 % source)
lines = np.loadtxt(txt)
flag = 0
for box in lines:
if box.shape != (5,):
box = lines
flag = 1
'''把txt上的第一列(类别)转成xml上的类别'''
box_index = int(box[0])
label['class'] = Class[box_index] # 类别索引从1开始
'''把txt上的数字(归一化)转成xml上框的坐标'''
xmin = float(box[1] - 0.5 * box[3]) * w
ymin = float(box[2] - 0.5 * box[4]) * h
xmax = float(xmin + box[3] * w)
ymax = float(ymin + box[4] * h)
label['xmin'] = xmin
label['ymin'] = ymin
label['xmax'] = xmax
label['ymax'] = ymax
keys = ['xmin', 'ymin', 'xmax', 'ymax']
limits = [w, h, w, h]
for i, key in enumerate(keys):
if label[key] >= limits[i]:
label[key] = limits[i]
elif label[key] < 0:
label[key] = 0
fxml.write(out1 % label)
if flag == 1:
break
fxml.write(out2)
if __name__ == '__main__':
l = ["hat","nohat"] #所有类别
file_dir1 = r' ' #图像文件夹
file_dir2 = r' ' #xml存放文件夹
file_dir3 = r' ' #txt存放文件夹
if not os.path.exists(file_dir2):
os.makedirs(file_dir2)
upp2low(file_dir1)
yolo2voc(file_dir1,file_dir2,file_dir3,l)
print('转换已结束')