VisDrone2019数据集标注转换实战:从TXT到VOC格式的完整指南
VisDrone2019数据集标注转换实战:从TXT到VOC格式的完整指南
无人机视觉数据正成为计算机视觉研究的热点领域,而VisDrone2019作为该领域最具代表性的数据集之一,其丰富的场景标注为算法训练提供了宝贵资源。但在实际应用中,研究人员常面临一个现实问题:原始TXT标注格式与主流框架所需的VOC格式不兼容。本文将手把手带你完成这场数据格式的"翻译"之旅。
1. 理解VisDrone2019标注体系
在开始转换前,我们需要深入理解原始标注文件的结构逻辑。每个TXT文件对应一张图像,每行代表一个标注对象,包含8个关键字段:
<bbox_left>,<bbox_top>,<bbox_width>,<bbox_height>,<score>,<object_category>,<truncation>,<occlusion>关键字段解析表:
| 字段序号 | 名称 | 数据类型 | 说明 |
|---|---|---|---|
| 0 | bbox_left | int | 边界框左上角x坐标 |
| 1 | bbox_top | int | 边界框左上角y坐标 |
| 2 | bbox_width | int | 边界框宽度(像素) |
| 3 | bbox_height | int | 边界框高度(像素) |
| 4 | score | int | 置信度(1表示有效标注) |
| 5 | object_category | int | 对象类别编码(0-11) |
| 6 | truncation | int | 截断程度(0-1) |
| 7 | occlusion | int | 遮挡程度(0-2) |
特别需要注意的是对象类别编码系统:
name_dict = { '0': 'ignored regions', '1': 'pedestrian', '2': 'people', '3': 'bicycle', '4': 'car', '5': 'van', '6': 'truck', '7': 'tricycle', '8': 'awning-tricycle', '9': 'bus', '10': 'motor', '11': 'others' }提示:在实际应用中,"ignored regions"(0)和"others"(11)这两类通常需要特殊处理,建议在转换时进行过滤。
2. 构建VOC格式XML模板
VOC格式的核心是XML描述文件,每个文件对应一张图像的所有标注。我们需要创建符合PASCAL VOC标准的XML结构:
from xml.dom import minidom def create_voc_template(doc, img_path, img_w, img_h, img_d): annotation = doc.createElement("annotation") doc.appendChild(annotation) # 基础信息节点 folder = doc.createElement('folder') folder.appendChild(doc.createTextNode('visdrone')) annotation.appendChild(folder) filename = doc.createElement('filename') filename.appendChild(doc.createTextNode(os.path.basename(img_path))) annotation.appendChild(filename) # 图像尺寸信息 size = doc.createElement('size') for dim, val in [('width', img_w), ('height', img_h), ('depth', img_d)]: elem = doc.createElement(dim) elem.appendChild(doc.createTextNode(str(val))) size.appendChild(elem) annotation.appendChild(size) return annotation关键节点说明:
size:必须包含图像的实际宽、高和通道数segmented:标记是否包含分割信息(VisDrone中设为0)- 每个
object节点对应一个检测目标
3. 完整转换流程实现
下面给出完整的转换脚本,包含错误处理和进度显示:
import os import cv2 import time from xml.dom import minidom from tqdm import tqdm # 进度条库 def convert_visdrone_to_voc(txt_dir, img_dir, output_dir): """主转换函数 Args: txt_dir: 原始TXT标注目录 img_dir: 对应图像目录 output_dir: XML输出目录 """ if not os.path.exists(output_dir): os.makedirs(output_dir) txt_files = [f for f in os.listdir(txt_dir) if f.endswith('.txt')] for txt_file in tqdm(txt_files, desc='Converting annotations'): base_name = os.path.splitext(txt_file)[0] img_path = os.path.join(img_dir, base_name + '.jpg') txt_path = os.path.join(txt_dir, txt_file) try: img = cv2.imread(img_path) if img is None: raise FileNotFoundError(f"Image {img_path} not found") img_h, img_w = img.shape[:2] doc = minidom.Document() # 创建基础模板 annotation = create_voc_template(doc, img_path, img_w, img_h, 3) # 处理每个标注对象 with open(txt_path, 'r') as f: for line in f: box = line.strip().split(',') if len(box) < 8: # 跳过不完整行 continue # 转换坐标格式 x_min = int(box[0]) y_min = int(box[1]) x_max = x_min + int(box[2]) y_max = y_min + int(box[3]) # 创建object节点 obj_node = create_object_node(doc, box) annotation.appendChild(obj_node) # 保存XML文件 output_path = os.path.join(output_dir, base_name + '.xml') with open(output_path, 'w') as x: x.write(doc.toprettyxml()) except Exception as e: print(f"Error processing {txt_file}: {str(e)}") continue关键工具函数:
def create_object_node(doc, box_data): """创建单个对象的XML节点""" object_node = doc.createElement('object') # 对象类别 class_id = box_data[5] class_name = name_dict.get(class_id, 'unknown') # 基础属性 for elem, val in [('name', class_name), ('pose', 'Unspecified'), ('truncated', box_data[6]), ('difficult', '0')]: node = doc.createElement(elem) node.appendChild(doc.createTextNode(str(val))) object_node.appendChild(node) # 边界框坐标 bndbox = doc.createElement('bndbox') for dim, val in [('xmin', box_data[0]), ('ymin', box_data[1]), ('xmax', str(int(box_data[0]) + int(box_data[2]))), ('ymax', str(int(box_data[1]) + int(box_data[3])))]: node = doc.createElement(dim) node.appendChild(doc.createTextNode(str(val))) bndbox.appendChild(node) object_node.appendChild(bndbox) return object_node4. 实战中的优化技巧
在实际项目中,我们发现了几个可以显著提升处理效率的技巧:
批量处理加速方案:
- 使用多进程处理(适合大型数据集)
- 预先加载类别映射字典到内存
- 对图像尺寸信息进行缓存
from multiprocessing import Pool def batch_convert(args): """多进程处理函数""" txt_file, txt_dir, img_dir, output_dir = args try: # 转换逻辑... return True except Exception as e: return False # 使用示例 if __name__ == '__main__': args_list = [(f, txt_dir, img_dir, output_dir) for f in os.listdir(txt_dir)] with Pool(processes=4) as pool: # 4个进程 results = pool.map(batch_convert, args_list) print(f"成功转换 {sum(results)}/{len(results)} 个文件")常见问题解决方案:
- 坐标越界处理:
# 在create_object_node函数中添加边界检查 x_max = min(int(box_data[0]) + int(box_data[2]), img_w-1) y_max = min(int(box_data[1]) + int(box_data[3]), img_h-1)- 特殊类别过滤:
# 在转换前过滤不需要的类别 if class_id in ['0', '11']: # 忽略区域和其他类 continue- 内存优化:
# 使用生成器逐行读取大文件 def read_annotations(txt_path): with open(txt_path, 'r') as f: for line in f: yield line.strip().split(',')5. 验证转换结果
转换完成后,建议进行结果验证。这里提供一个简单的验证脚本:
import xml.etree.ElementTree as ET def validate_xml(xml_path, img_dir): """验证XML文件的正确性""" try: tree = ET.parse(xml_path) root = tree.getroot() # 检查图像是否存在 img_file = root.find('filename').text img_path = os.path.join(img_dir, img_file) if not os.path.exists(img_path): return False # 检查标注对象 for obj in root.iter('object'): name = obj.find('name').text bbox = obj.find('bndbox') for corner in ['xmin', 'ymin', 'xmax', 'ymax']: if not bbox.find(corner).text.isdigit(): return False return True except: return False验证指标参考表:
| 检查项 | 方法 | 预期结果 |
|---|---|---|
| 文件完整性 | 检查XML格式 | 符合XML规范 |
| 图像对应 | 检查filename节点 | 图像文件存在 |
| 坐标有效性 | 检查bndbox值 | 在图像尺寸范围内 |
| 类别合法性 | 检查name节点 | 在预设类别中 |
对于大规模数据集,建议抽样检查,比如:
import random def random_check(output_dir, img_dir, sample_size=20): """随机抽样检查""" xml_files = [f for f in os.listdir(output_dir) if f.endswith('.xml')] samples = random.sample(xml_files, min(sample_size, len(xml_files))) results = [] for xml in samples: xml_path = os.path.join(output_dir, xml) results.append(validate_xml(xml_path, img_dir)) print(f"抽样检查通过率:{sum(results)}/{len(results)}")6. 高级应用:与深度学习框架集成
转换后的VOC格式可以直接用于主流框架。以下是几个典型示例:
TensorFlow Object Detection API:
# 在label_map.pbtxt中定义类别 item { id: 1 name: 'pedestrian' } item { id: 2 name: 'people' } # ...其他类别PyTorch处理示例:
from torchvision.datasets import VOCDetection class VisDroneVOC(VOCDetection): """自定义VisDrone数据集类""" def __init__(self, root, transform=None): super().__init__(root=root, year='2012', # 使用VOC2012的解析方式 image_set='train', transform=transform) def parse_voc_xml(self, node): # 可在此自定义解析逻辑 return super().parse_voc_xml(node)YOLO格式转换技巧: 如果需要YOLO格式,可以在VOC基础上进一步转换:
def voc_to_yolo(voc_xml, img_w, img_h): """将VOC格式转为YOLO格式(中心坐标+宽高,归一化)""" tree = ET.parse(voc_xml) root = tree.getroot() yolo_lines = [] for obj in root.iter('object'): bbox = obj.find('bndbox') xmin = float(bbox.find('xmin').text) ymin = float(bbox.find('ymin').text) xmax = float(bbox.find('xmax').text) ymax = float(bbox.find('ymax').text) # 计算中心点和宽高 x_center = ((xmin + xmax) / 2) / img_w y_center = ((ymin + ymax) / 2) / img_h width = (xmax - xmin) / img_w height = (ymax - ymin) / img_h # 获取类别ID class_name = obj.find('name').text class_id = list(name_dict.values()).index(class_name) yolo_lines.append(f"{class_id} {x_center} {y_center} {width} {height}") return yolo_lines在实际项目中,这种格式转换往往是数据处理流水线的第一步。完整的处理流程通常还包括:
- 数据集拆分(训练/验证/测试集)
- 数据增强策略设计
- 类别平衡处理
- 困难样本挖掘
掌握这些格式转换技巧后,你可以更灵活地在不同框架间迁移实验,这对算法研发效率提升至关重要。
