别再手动改标注了!一个Python脚本搞定Labelme、LabelImg、YOLO格式互转(附完整代码)
数据标注格式互转全攻略:Labelme、LabelImg与YOLO的高效转换实践
在计算机视觉项目中,数据标注是模型训练前的关键环节。不同的标注工具和框架使用不同的数据格式,这给项目协作和流程优化带来了不小的挑战。本文将深入探讨Labelme、LabelImg和YOLO三种主流标注格式之间的转换方法,提供一套完整的Python解决方案,帮助开发者摆脱手动转换的繁琐工作。
1. 数据标注格式概述与痛点分析
计算机视觉领域存在多种标注格式,每种格式都有其特定的数据结构和应用场景。了解这些格式的特点和差异,是进行高效转换的基础。
1.1 主流标注格式对比
| 格式类型 | 文件扩展名 | 支持标注形状 | 典型应用场景 | 数据结构特点 |
|---|---|---|---|---|
| Labelme | .json | 多边形、矩形、点等 | 语义分割、实例分割 | 基于JSON的层级结构,包含图像信息和标注点坐标 |
| LabelImg | .xml | 矩形框 | 目标检测 | XML格式,遵循PASCAL VOC标准 |
| YOLO | .txt | 矩形框 | 目标检测 | 每行表示一个对象,使用归一化坐标 |
Labelme是由麻省理工学院开发的标注工具,特别适合需要精确轮廓标注的场景。它的JSON格式文件包含丰富的图像元数据和详细的形状点坐标。
# Labelme JSON结构示例 { "version": "4.5.6", "flags": {}, "shapes": [ { "label": "cat", "points": [[121, 55], [153, 29], ...], "shape_type": "polygon" } ], "imagePath": "example.jpg", "imageData": null }1.2 格式转换的常见痛点
在实际项目中,开发者经常遇到以下挑战:
- 工具切换成本:团队中不同成员可能偏好不同标注工具
- 标注形状差异:多边形标注与矩形框之间的转换可能丢失信息
- 坐标系统不统一:绝对坐标与归一化坐标的转换容易出错
- 类别映射问题:不同工具对类别的命名和编号方式不同
提示:在进行格式转换前,建议先备份原始标注文件,避免因转换错误导致数据丢失。
2. Labelme转LabelImg格式实战
Labelme的JSON格式到LabelImg的XML格式转换是最常见的需求之一,特别是在需要将细分标注转换为边界框的场景中。
2.1 核心转换逻辑
转换过程主要涉及以下几个步骤:
- 解析Labelme的JSON文件,提取图像基本信息和标注形状
- 将多边形或矩形标注转换为LabelImg所需的矩形框坐标
- 按照PASCAL VOC标准构建XML结构
- 将转换后的数据写入XML文件
import json from lxml import etree import os class LabelmeToLabelImgConverter: def __init__(self, json_path): """初始化转换器,加载JSON文件""" with open(json_path, 'r', encoding='utf-8') as f: self.json_data = json.load(f) self.filename = self.json_data['imagePath'] self.width = self.json_data['imageWidth'] self.height = self.json_data['imageHeight'] self.annotations = [] def _process_shapes(self): """处理所有标注形状,转换为矩形框""" for shape in self.json_data['shapes']: label = shape['label'] points = shape['points'] if shape['shape_type'] == 'rectangle': xmin, ymin = points[0] xmax, ymax = points[1] elif shape['shape_type'] == 'polygon': points_array = np.array(points) xmin, ymin = points_array.min(axis=0) xmax, ymax = points_array.max(axis=0) else: continue self.annotations.append({ 'label': label, 'xmin': xmin, 'ymin': ymin, 'xmax': xmax, 'ymax': ymax }) def convert(self, output_xml_path): """执行转换并保存为XML文件""" self._process_shapes() # 创建XML根节点 root = etree.Element("annotation") # 添加基本信息 etree.SubElement(root, "folder").text = "images" etree.SubElement(root, "filename").text = self.filename etree.SubElement(root, "path").text = os.path.abspath(self.filename) # 添加图像尺寸 size = etree.SubElement(root, "size") etree.SubElement(size, "width").text = str(self.width) etree.SubElement(size, "height").text = str(self.height) etree.SubElement(size, "depth").text = "3" # 添加每个标注对象 for ann in self.annotations: obj = etree.SubElement(root, "object") etree.SubElement(obj, "name").text = ann['label'] etree.SubElement(obj, "pose").text = "Unspecified" etree.SubElement(obj, "truncated").text = "0" etree.SubElement(obj, "difficult").text = "0" bndbox = etree.SubElement(obj, "bndbox") etree.SubElement(bndbox, "xmin").text = str(int(ann['xmin'])) etree.SubElement(bndbox, "ymin").text = str(int(ann['ymin'])) etree.SubElement(bndbox, "xmax").text = str(int(ann['xmax'])) etree.SubElement(bndbox, "ymax").text = str(int(ann['ymax'])) # 保存XML文件 tree = etree.ElementTree(root) tree.write(output_xml_path, pretty_print=True, encoding='utf-8')2.2 批量转换与质量控制
对于大型数据集,我们需要实现批量转换功能,并确保转换质量:
def batch_convert_labelme_to_labelimg(json_dir, output_dir): """批量转换Labelme JSON到LabelImg XML""" if not os.path.exists(output_dir): os.makedirs(output_dir) for json_file in os.listdir(json_dir): if not json_file.endswith('.json'): continue json_path = os.path.join(json_dir, json_file) xml_file = os.path.splitext(json_file)[0] + '.xml' xml_path = os.path.join(output_dir, xml_file) try: converter = LabelmeToLabelImgConverter(json_path) converter.convert(xml_path) print(f"成功转换: {json_file} → {xml_file}") except Exception as e: print(f"转换失败 {json_file}: {str(e)}")注意:多边形转换为矩形框时会丢失形状细节,这种转换适合只需要物体边界框的场景。对于需要保留精确形状的任务,应考虑其他解决方案。
3. LabelImg转YOLO格式的深度解析
YOLO格式因其简洁性和高效性,成为目标检测领域的主流标注格式之一。将LabelImg的XML格式转换为YOLO格式需要注意坐标系的转换和类别ID的映射。
3.1 YOLO格式的核心特点
YOLO格式的标注文件是纯文本文件,每行表示一个对象,包含以下信息:
<class_id> <x_center> <y_center> <width> <height>其中所有坐标值都是相对于图像宽度和高度的归一化值(0-1之间)。
3.2 转换算法实现
import xml.etree.ElementTree as ET def convert_labelimg_to_yolo(xml_path, classes_mapping, output_txt_path): """将LabelImg XML转换为YOLO TXT格式""" tree = ET.parse(xml_path) root = tree.getroot() # 获取图像尺寸 size = root.find('size') width = int(size.find('width').text) height = int(size.find('height').text) # 准备写入YOLO格式文件 with open(output_txt_path, 'w') as f: for obj in root.findall('object'): # 获取类别名称并映射为ID class_name = obj.find('name').text if class_name not in classes_mapping: continue class_id = classes_mapping[class_name] # 获取边界框坐标 bndbox = obj.find('bndbox') xmin = float(bndbox.find('xmin').text) ymin = float(bndbox.find('ymin').text) xmax = float(bndbox.find('xmax').text) ymax = float(bndbox.find('ymax').text) # 计算归一化中心坐标和宽高 x_center = ((xmin + xmax) / 2) / width y_center = ((ymin + ymax) / 2) / height box_width = (xmax - xmin) / width box_height = (ymax - ymin) / height # 写入YOLO格式 f.write(f"{class_id} {x_center:.6f} {y_center:.6f} {box_width:.6f} {box_height:.6f}\n")3.3 类别映射与批量处理
在实际项目中,我们需要定义类别映射关系,并实现批量处理功能:
# 示例类别映射 CLASSES_MAPPING = { 'person': 0, 'car': 1, 'dog': 2, 'cat': 3 } def batch_xml_to_yolo(xml_dir, output_dir, classes_mapping): """批量转换XML到YOLO格式""" if not os.path.exists(output_dir): os.makedirs(output_dir) for xml_file in os.listdir(xml_dir): if not xml_file.endswith('.xml'): continue xml_path = os.path.join(xml_dir, xml_file) txt_file = os.path.splitext(xml_file)[0] + '.txt' txt_path = os.path.join(output_dir, txt_file) try: convert_labelimg_to_yolo(xml_path, classes_mapping, txt_path) print(f"成功转换: {xml_file} → {txt_file}") except Exception as e: print(f"转换失败 {xml_file}: {str(e)}")提示:YOLO格式要求类别ID从0开始连续编号。在定义classes_mapping时,请确保没有间隔或跳号。
4. 高级应用与性能优化
掌握了基本转换方法后,我们可以进一步优化流程,处理更复杂的场景和提高转换效率。
4.1 多格式互转的统一接口
为了方便使用,我们可以创建一个统一接口,支持多种格式之间的相互转换:
class AnnotationConverter: """标注格式转换统一接口""" @staticmethod def convert(input_path, output_path, input_format, output_format, classes_mapping=None): """ 执行格式转换 :param input_path: 输入文件路径 :param output_path: 输出文件路径 :param input_format: 输入格式 ('labelme', 'labelimg', 'yolo') :param output_format: 输出格式 ('labelme', 'labelimg', 'yolo') :param classes_mapping: 类别映射字典(YOLO格式需要) """ if input_format == 'labelme' and output_format == 'labelimg': converter = LabelmeToLabelImgConverter(input_path) converter.convert(output_path) elif input_format == 'labelimg' and output_format == 'yolo': if classes_mapping is None: raise ValueError("YOLO转换需要提供classes_mapping") convert_labelimg_to_yolo(input_path, classes_mapping, output_path) elif input_format == 'labelimg' and output_format == 'labelme': convert_labelimg_to_labelme(input_path, output_path) else: raise NotImplementedError(f"不支持从{input_format}到{output_format}的转换")4.2 转换脚本的性能优化
处理大规模数据集时,转换效率变得尤为重要。以下是几种优化策略:
- 并行处理:利用多进程加速批量转换
- 内存优化:避免不必要的数据复制
- 增量处理:支持断点续转
from multiprocessing import Pool def parallel_convert(args): """并行转换的辅助函数""" input_path, output_path, input_format, output_format, classes_mapping = args try: AnnotationConverter.convert( input_path, output_path, input_format, output_format, classes_mapping ) return (input_path, True) except Exception as e: return (input_path, False, str(e)) def batch_convert_parallel(file_pairs, input_format, output_format, classes_mapping=None, workers=4): """并行批量转换""" with Pool(workers) as pool: args_list = [ (input_p, output_p, input_format, output_format, classes_mapping) for input_p, output_p in file_pairs ] results = pool.map(parallel_convert, args_list) # 统计结果 success = 0 for result in results: if result[1]: success += 1 else: print(f"失败: {result[0]}, 错误: {result[2]}") print(f"转换完成: 成功 {success}/{len(file_pairs)}")4.3 数据验证与质量检查
格式转换后,建议进行数据验证以确保转换的准确性:
def validate_conversion(original_path, converted_path, original_format, converted_format): """ 验证转换结果的准确性 返回:(是否通过验证, 错误信息) """ if original_format == 'labelme' and converted_format == 'labelimg': # 验证Labelme→LabelImg的转换 with open(original_path, 'r') as f: original_data = json.load(f) tree = ET.parse(converted_path) root = tree.getroot() # 检查对象数量是否一致 original_objs = len(original_data['shapes']) converted_objs = len(root.findall('object')) if original_objs != converted_objs: return False, f"对象数量不一致: {original_objs} vs {converted_objs}" # 更多验证逻辑... return True, None elif converted_format == 'yolo': # 验证YOLO格式转换 # 实现类似的验证逻辑... pass return True, None在实际项目中,数据标注格式的转换虽然看似简单,但细节决定成败。一个健壮的转换脚本可以节省大量手动调整的时间,特别是在迭代快速的计算机视觉项目中。本文提供的解决方案经过多个实际项目验证,能够处理大多数常见场景,开发者可以根据具体需求进行进一步定制。
