Labelme标注转YOLO格式:从报错到解决方案的完整实战指南
第一次尝试将Labelme标注的JSON文件转换为YOLO格式时,我本以为这会是个简单的过程——毕竟网上有现成的转换脚本。但现实给了我一记响亮的耳光:编码错误、路径问题、环境崩溃接踵而至。如果你也正在这个转换过程中挣扎,这篇文章或许能帮你节省数小时的调试时间。
1. 环境准备与基础配置
在开始转换之前,确保你的环境满足以下基本要求:
- Python 3.6或更高版本
- 已安装Labelme和OpenCV
- 基本的命令行操作知识
我推荐使用conda创建一个独立环境:
conda create -n labelme2yolo python=3.8 conda activate labelme2yolo pip install labelme opencv-python scikit-learn常见安装问题:
- 如果遇到OpenCV安装失败,可以尝试:
pip install opencv-python-headless - Labelme安装可能需要额外的依赖,在Ubuntu上可能需要:
sudo apt-get install python3-pyqt5
2. JSON文件读取的三大陷阱与解决方案
2.1 编码问题:为什么json.load()会失败
最初的代码直接使用json.load(open(json_path)),这在大多数情况下能工作,直到遇到特殊字符。更健壮的做法是:
with open(json_path, 'r', encoding='utf-8') as f: data = json.load(f)为什么这很重要:
- 明确指定编码避免因系统默认编码不同导致的解析失败
with语句确保文件正确关闭- 统一处理可能存在的BOM头
2.2 数据完整性检查
在转换前应该验证JSON结构:
required_keys = ['version', 'flags', 'shapes', 'imagePath', 'imageData'] if not all(key in data for key in required_keys): raise ValueError("Invalid Labelme JSON structure")2.3 大文件处理技巧
当处理大型标注文件时,可以使用迭代解析:
import ijson def parse_large_json(file_path): with open(file_path, 'rb') as f: for shape in ijson.items(f, 'shapes.item'): yield shape3. 路径处理的正确姿势
3.1 绝对路径 vs 相对路径
原始代码中的硬编码路径是许多错误的根源。应该:
import os from pathlib import Path output_dir = Path(json_dir) / 'YOLODataset' output_dir.mkdir(parents=True, exist_ok=True)路径处理最佳实践:
- 使用
pathlib替代os.path(Python3.4+) - 永远不要假设路径分隔符(Windows用
\,Linux用/) - 使用
resolve()获取绝对路径
3.2 跨平台兼容性方案
这段代码可以确保在任何操作系统上都能正确工作:
def get_platform_safe_path(base_path, *subpaths): return str(Path(base_path).joinpath(*subpaths).resolve())4. 标注转换的核心算法剖析
4.1 边界框坐标转换原理
YOLO格式需要的是归一化的中心坐标和宽高:
def convert_to_yolo(shape, img_width, img_height): points = np.array(shape['points']) x_min, y_min = points.min(axis=0) x_max, y_max = points.max(axis=0) x_center = (x_min + x_max) / 2 / img_width y_center = (y_min + y_max) / 2 / img_height width = (x_max - x_min) / img_width height = (y_max - y_min) / img_height return x_center, y_center, width, height4.2 特殊形状处理
Labelme支持多种形状类型,需要分别处理:
| 形状类型 | 处理方式 |
|---|---|
| rectangle | 直接转换 |
| circle | 计算半径后转换 |
| polygon | 取最小外接矩形 |
| line | 转换为细长矩形 |
if shape['shape_type'] == 'circle': return convert_circle(shape, img_w, img_h) elif shape['shape_type'] == 'polygon': return convert_polygon(shape, img_w, img_h) else: return convert_rectangle(shape, img_w, img_h)5. 数据集分割与YAML配置
5.1 智能数据集分割
不要简单随机分割,应考虑:
from sklearn.model_selection import StratifiedShuffleSplit def balanced_split(json_files, labels, test_size=0.2): sss = StratifiedShuffleSplit(n_splits=1, test_size=test_size) for train_idx, test_idx in sss.split(json_files, labels): return [json_files[i] for i in train_idx], [json_files[i] for i in test_idx]5.2 动态YAML生成
自动生成的dataset.yaml应该包含:
def generate_yaml(output_path, class_map): content = f"""train: {output_path}/images/train val: {output_path}/images/val nc: {len(class_map)} names: {list(class_map.keys())} """ with open(f"{output_path}/dataset.yaml", 'w') as f: f.write(content)6. 完整解决方案代码
经过多次迭代,这是最终的稳健版本核心代码:
import json from pathlib import Path import numpy as np from sklearn.model_selection import StratifiedShuffleSplit class LabelmeToYOLOConverter: def __init__(self, json_dir): self.json_dir = Path(json_dir) self.output_dir = self.json_dir.parent / 'YOLODataset' self.class_map = self._build_class_map() def _build_class_map(self): classes = set() for json_file in self.json_dir.glob('*.json'): with open(json_file, 'r', encoding='utf-8') as f: data = json.load(f) classes.update(shape['label'] for shape in data['shapes']) return {cls: idx for idx, cls in enumerate(sorted(classes))} def convert_all(self, val_ratio=0.2): # 创建输出目录结构 (self.output_dir / 'labels/train').mkdir(parents=True, exist_ok=True) (self.output_dir / 'labels/val').mkdir(parents=True, exist_ok=True) (self.output_dir / 'images/train').mkdir(parents=True, exist_ok=True) (self.output_dir / 'images/val').mkdir(parents=True, exist_ok=True) # 数据集分割与转换 json_files = list(self.json_dir.glob('*.json')) train_files, val_files = self._split_dataset(json_files, val_ratio) for files, split in [(train_files, 'train'), (val_files, 'val')]: for json_file in files: self._convert_single(json_file, split) # 生成YAML配置文件 self._generate_yaml_config() def _split_dataset(self, json_files, val_ratio): # 获取每个文件的类别分布用于分层抽样 labels = [] for json_file in json_files: with open(json_file, 'r', encoding='utf-8') as f: data = json.load(f) labels.append(tuple(sorted(set(shape['label'] for shape in data['shapes'])))) sss = StratifiedShuffleSplit(n_splits=1, test_size=val_ratio) train_idx, val_idx = next(sss.split(json_files, labels)) return [json_files[i] for i in train_idx], [json_files[i] for i in val_idx] def _convert_single(self, json_file, split): with open(json_file, 'r', encoding='utf-8') as f: data = json.load(f) # 转换每个标注形状 yolo_annotations = [] img_h, img_w = data['imageHeight'], data['imageWidth'] for shape in data['shapes']: if shape['shape_type'] == 'circle': ann = self._convert_circle(shape, img_w, img_h) else: ann = self._convert_polygon(shape, img_w, img_h) yolo_annotations.append(ann) # 保存YOLO格式标注 txt_path = self.output_dir / f'labels/{split}/{json_file.stem}.txt' with open(txt_path, 'w') as f: for ann in yolo_annotations: f.write(' '.join(map(str, ann)) + '\n') # 保存图像(实际项目中可能需要从imageData解码) img_path = self.output_dir / f'images/{split}/{json_file.stem}.jpg' # 这里应该是实际的图像保存代码,示例中省略 def _generate_yaml_config(self): config = { 'train': str(self.output_dir / 'images/train'), 'val': str(self.output_dir / 'images/val'), 'nc': len(self.class_map), 'names': list(self.class_map.keys()) } with open(self.output_dir / 'dataset.yaml', 'w') as f: yaml.dump(config, f, sort_keys=False)7. 高级技巧与性能优化
7.1 多进程加速处理
对于大型数据集,可以使用多进程:
from multiprocessing import Pool def process_file(json_file): # 单个文件的处理逻辑 pass with Pool(processes=4) as pool: pool.map(process_file, json_files)7.2 内存优化策略
处理超大标注文件时:
def stream_process(json_file): with open(json_file, 'r') as f: for line in f: if '"shapes":' in line: # 流式处理标注 pass7.3 验证转换结果
转换后应该验证:
def validate_conversion(yolo_dir): for txt_file in Path(yolo_dir).glob('*.txt'): with open(txt_file) as f: for line in f: cls, x, y, w, h = map(float, line.split()) assert 0 <= x <= 1, f"Invalid x in {txt_file}" assert 0 <= y <= 1, f"Invalid y in {txt_file}" assert 0 <= w <= 1, f"Invalid w in {txt_file}" assert 0 <= h <= 1, f"Invalid h in {txt_file}"