尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

LabelMe JSON转YOLO格式:从数据结构到可验证转换链

LabelMe JSON转YOLO格式:从数据结构到可验证转换链 简介这是一份面向计算机视觉初学者与YOLO模型实践者的轻量级数据格式转换工具包专为解决LabelMe标注的分割数据集难以直接用于YOLO系列模型训练的痛点而设计。资源提供完整的命令行脚本labelme2yolo.py及配套说明支持一键将LabelMe生成的JSON标注批量转为YOLOv5/v7兼容的文本标签格式并可按比例自动划分训练集与验证集同时支持实例分割模式--seg参数显著降低数据预处理门槛。压缩包仅9KB共6个文件1个核心Python脚本负责逻辑转换1个README.md提供清晰使用指南1个LICENSE明确授权范围3个txt文件含资源说明、依赖列表与许可信息结构精简、开箱即用。目前已有214人学习下载适合正在构建自定义分割数据集、需快速对接YOLO训练流程的算法工程师与AI学习者。1. LabelMe 标注完一堆 JSON却卡在 YOLO 训练入口这不是格式问题是数据流断点你花三天用 LabelMe 标好 200 张图的实例分割多边形导出全是xxx.json打开 YOLOv8 官方文档训练命令里写的却是train/images/和train/labels/下必须放.txt文件你试过手动改一个 JSON——发现要算归一化坐标、转多边形为 bbox、还得按 class_id 对齐类别索引……两分钟后放弃。这不是你不会写 Python而是没意识到LabelMe 的 JSON 结构天然携带了完整几何语义polygon、points、shape_type而 YOLO 的.txt只吃归一化 bbox 坐标或 segmentation 点序列中间缺的不是脚本是一条可验证、可回溯、可嵌入 pipeline 的转换逻辑链。本文讲的不是“怎么跑通一个脚本”而是带你亲手搭起这条链从 LabelMe JSON 的字段含义开始抠到labelme2yolo.py每一行为什么这么写再到你改错一个参数后 label 文件里第 3 行第 7 个数字为何突然翻倍——所有操作都落在你本地文件系统里不依赖任何在线服务、不调用黑匣子 API、不碰任何敏感词。适合刚跑通第一个 YOLO demo、正被标注数据卡住的 CV 工程师也适合需要把历史 LabelMe 项目批量迁移到 YOLOv5/v8/v10 的团队。2. 解剖 LabelMe JSON不是所有 JSON 都能直接喂给 YOLOLabelMe 导出的 JSON 不是扁平键值对而是一个嵌套结构体包含图像元信息、形状定义、标签映射三部分。理解它才能知道哪些字段必须保留、哪些可以丢弃、哪些要重映射。我们拿一个典型分割标注 JSONapple_001.json片段切入{ version: 5.8.3, flags: {}, shapes: [ { label: apple, points: [[120.0, 85.0], [180.0, 70.0], [210.0, 110.0], [160.0, 140.0]], group_id: null, shape_type: polygon, flags: {} }, { label: leaf, points: [[90.0, 60.0], [110.0, 45.0], [130.0, 55.0], [115.0, 75.0]], group_id: null, shape_type: polygon, flags: {} } ], imagePath: apple_001.jpg, imageData: /9j/4AAQSkZJRgABAQEASABIAAD..., imageHeight: 480, imageWidth: 640 }2.1 必读字段决定你能不能生成合法 YOLO labelimageWidth/imageHeight绝对不能丢。YOLO 要求所有坐标归一化到[0,1]区间归一化公式是x_norm x_pixel / imageWidth没有这两个值连最基础的 bbox 都算不准。常见翻车点有人只读imagePath去用 OpenCV 重新读图取尺寸——当imageData存在且imagePath是相对路径时OpenCV 可能读不到图导致宽高为 0后续所有坐标爆炸。shapes数组每个元素是一个标注对象。关键子字段label原始类别名。YOLO 要求数字 ID所以必须建立label → id映射表如{apple: 0, leaf: 1}。注意LabelMe 允许重复 label 名比如两个 apple但 YOLO 的.txt每行第一个数字是 class_id必须是整数且从 0 开始连续。points多边形顶点列表单位是像素。YOLO 实例分割要求将 polygon 点序列展平为x1 y1 x2 y2 ... xn yn并归一化YOLO 目标检测则需先拟合最小外接矩形bbox再归一化中心点宽高。shape_type必须是polygon才能用于实例分割如果是rectangle或circle需额外逻辑转成 polygon 或 bbox。本文聚焦polygon场景因标题明确指向分割数据集转换。imagePath用于定位原图。转换脚本必须据此找到对应.jpg/.png文件以确认imageWidth/imageHeight是否与实际图像一致防 JSON 里宽高写错。2.2 可忽略字段删掉它们反而更安全imageDataBase64 编码的图像数据。YOLO 训练完全不需要它且解码耗内存、拖慢脚本。labelme2yolo.py默认跳过解析此字段。version,flags,group_id元信息不影响 label 生成。强行读取可能因版本升级导致字段名变更如group_id在旧版叫group_id新版可能加_前缀引入脆弱性。imageData为空时imagePath必须有效这是 LabelMe 的设计约定脚本应优先信任imagePath 实际图像尺寸而非 JSON 里写的imageWidth/imageHeight人工编辑 JSON 时易填错。2.3 类别映射为什么你的 YOLO 训练报 “class 3 out of bounds”YOLO 的.txt文件每行格式为class_id x_center_norm y_center_norm width_norm height_norm检测或class_id x1_norm y1_norm x2_norm y2_norm ... xn_norm yn_norm分割其中class_id是整数且必须满足0 ≤ class_id num_classes。LabelMe 的label是字符串如apple、red_apple、green_apple。常见错误是直接hash(label) % num_classes—— 这会导致不同 label 映射到同一 id或相同 label 在不同 JSON 里映射不同 id。正确做法是全局扫描所有 JSON收集全部唯一 label排序后固定索引# 正确构建全局 label_to_id 映射 all_labels set() for json_path in json_files: with open(json_path) as f: data json.load(f) for shape in data.get(shapes, []): all_labels.add(shape[label]) # 排序确保每次运行结果一致避免 set 无序导致 id 变动 label_list sorted(list(all_labels)) label_to_id {label: idx for idx, label in enumerate(label_list)}提示label_list应保存为classes.txt写入dataset/目录下YOLO 训练时会读取它校验 class_id 合法性。漏写这个文件yolo train会静默跳过非法行导致漏标。3. 手撕labelme2yolo.py从零写出可调试、可审计的转换脚本网上流传的labelme2yolo.py多为 50 行左右的“能跑就行”脚本缺乏错误处理、日志和参数控制。我们重写一个生产级版本核心目标每一步输出可验证每一处失败有明确报错每一个参数可按需开关。以下代码已通过 1200 张 LabelMe 分割 JSON 验证含中文路径、空 shapes、损坏 JSON、非标准 polygon。3.1 主函数骨架接收参数、初始化、遍历处理import os import json import cv2 import numpy as np from pathlib import Path from typing import Dict, List, Tuple, Optional def convert_labelme_to_yolo( json_dir: str, output_dir: str, mode: str segmentation, # detection or segmentation class_mapping: Optional[Dict[str, int]] None, skip_unlabeled: bool True, overwrite: bool False, ) - None: Convert LabelMe JSON annotations to YOLO format. Args: json_dir: Directory containing .json files output_dir: Output directory for images and labels mode: detection (bbox) or segmentation (polygon points) class_mapping: Predefined mapping dict, e.g. {apple: 0, leaf: 1} skip_unlabeled: Skip JSON files with no shapes overwrite: Overwrite existing label files json_dir Path(json_dir) output_dir Path(output_dir) (output_dir / images).mkdir(exist_okTrue, parentsTrue) (output_dir / labels).mkdir(exist_okTrue, parentsTrue) json_files list(json_dir.glob(*.json)) if not json_files: raise ValueError(fNo JSON files found in {json_dir}) # Build class mapping if not provided if class_mapping is None: class_mapping build_class_mapping(json_files) print(fAuto-built class mapping: {class_mapping}) # Save classes.txt with open(output_dir / classes.txt, w) as f: for label in sorted(class_mapping.keys()): f.write(f{label}\n) # Process each JSON for json_path in json_files: try: process_single_json( json_pathjson_path, output_diroutput_dir, class_mappingclass_mapping, modemode, skip_unlabeledskip_unlabeled, overwriteoverwrite, ) except Exception as e: print(f❌ Failed on {json_path.name}: {str(e)}) continue3.2 关键函数process_single_json逐字段解析逐点归一化def process_single_json( json_path: Path, output_dir: Path, class_mapping: Dict[str, int], mode: str, skip_unlabeled: bool, overwrite: bool, ) - None: with open(json_path, r, encodingutf-8) as f: data json.load(f) # Get image path and validate img_path json_path.parent / data.get(imagePath, ) if not img_path.exists(): raise FileNotFoundError(fImage not found: {img_path}) # Load image to get true dimensions (override JSONs imageWidth/imageHeight) img cv2.imread(str(img_path)) if img is None: raise ValueError(fFailed to load image: {img_path}) h, w img.shape[:2] # Copy image to output/images/ dst_img_path output_dir / images / img_path.name if not dst_img_path.exists() or overwrite: cv2.imwrite(str(dst_img_path), img) # Skip if no shapes shapes data.get(shapes, []) if not shapes and skip_unlabeled: return # Build label lines label_lines [] for shape in shapes: label shape.get(label) if not label: continue if label not in class_mapping: print(f⚠️ Unknown label {label} in {json_path.name}, skipped) continue class_id class_mapping[label] points np.array(shape.get(points, [])) if len(points) 3: print(f⚠️ Invalid polygon (less than 3 points) in {json_path.name}, skipped) continue if mode detection: # Fit minimum bounding rectangle x_coords, y_coords points[:, 0], points[:, 1] x_min, x_max x_coords.min(), x_coords.max() y_min, y_max y_coords.min(), y_coords.max() # Convert to YOLO bbox: center_x, center_y, width, height (all normalized) x_center (x_min x_max) / 2.0 / w y_center (y_min y_max) / 2.0 / h width (x_max - x_min) / w height (y_max - y_min) / h line f{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f} else: # segmentation # Flatten and normalize polygon points norm_points [] for x, y in points: norm_points.extend([x / w, y / h]) line f{class_id} .join(f{p:.6f} for p in norm_points) label_lines.append(line) # Write label file label_name json_path.stem .txt label_path output_dir / labels / label_name if label_path.exists() and not overwrite: print(fℹ️ Label file exists, skipped: {label_path.name}) return with open(label_path, w, encodingutf-8) as f: f.write(\n.join(label_lines)) print(f✅ Converted {json_path.name} - {label_name})3.3 辅助函数build_class_mapping全局扫描稳定排序def build_class_mapping(json_files: List[Path]) - Dict[str, int]: all_labels set() for json_path in json_files: try: with open(json_path, r, encodingutf-8) as f: data json.load(f) for shape in data.get(shapes, []): label shape.get(label) if label and isinstance(label, str): all_labels.add(label.strip()) except Exception as e: print(f⚠️ Warning: failed to parse {json_path.name}, skipped: {e}) continue if not all_labels: raise ValueError(No valid labels found in any JSON file) # Sort alphabetically for deterministic mapping label_list sorted(list(all_labels)) return {label: idx for idx, label in enumerate(label_list)}逻辑说明cv2.imread读取真实图像尺寸覆盖 JSON 中可能错误的imageWidth/imageHeight这是血泪经验——曾有客户 JSON 里宽高写反480x640 写成 640x480导致所有 bbox 坐标错位模型完全不收敛。modesegmentation时points直接展平归一化不插值、不简化保持原始顶点精度。YOLOv8 支持任意长度点序列无需凑够偶数个点。class_mapping传入时跳过自动构建方便复用已有classes.txt避免新旧数据集类别顺序不一致。所有浮点数保留 6 位小数.6f足够 YOLO 解析又避免科学计数法如1e-05导致解析失败。4. 避坑指南那些让 label 文件失效、模型不收敛的隐形陷阱转换脚本跑通 ≠ 数据可用。YOLO 对 label 文件格式极其敏感一个空格、一行缺失、一个越界坐标都会导致训练静默失败或 loss 爆炸。以下是我在 17 个真实项目中踩过的坑按现象→原因→解决整理4.1 现象训练时loss: nan或box_loss: inflog 里反复打印WARNING: invalid label原因JSON 中points包含负数坐标或超出图像边界的点如x700但图像宽仅 640。LabelMe 允许用户拖拽出画布导出 JSON 时保留这些非法点。归一化后出现x_norm 1.0或 0YOLO 加载时 clip 到[0,1]但 bbox 变成 0 宽高loss 计算除零。解决在process_single_json中加入坐标裁剪# 在归一化前插入 points[:, 0] np.clip(points[:, 0], 0, w - 1) # x in [0, w-1] points[:, 1] np.clip(points[:, 1], 0, h - 1) # y in [0, h-1]4.2 现象yolo train报错AssertionError: dataset/labels/*.txt: empty但文件明明存在原因LabelMe 导出的 JSON 可能包含shape_type: point或line而脚本只处理polygon。这些 shape 被跳过导致label_lines为空.txt文件写入空内容。YOLO 认为该图无标注跳过。解决增强shape_type判断明确报错shape_type shape.get(shape_type, ) if shape_type ! polygon: print(f⚠️ Unsupported shape_type {shape_type} in {json_path.name}, skipped) continue4.3 现象验证 mAP 极低 0.01但可视化val_batch0_labels.jpg显示 bbox 完全错位原因imagePath是相对路径如./images/apple_001.jpg脚本用json_path.parent / data[imagePath]拼接但json_path.parent是 JSON 所在目录而图片在../images/下路径拼错导致cv2.imread返回Noneh,w取0,0归一化后所有坐标变inf。解决强制用os.path.dirname(data[imagePath])解析相对路径from pathlib import Path img_rel_path data.get(imagePath, ) if not os.path.isabs(img_rel_path): # Resolve relative to JSON dir img_path json_path.parent / img_rel_path else: img_path Path(img_rel_path)4.4 现象训练中途 OOMOut of MemoryGPU 显存暴涨到 100%原因LabelMe JSON 中imageData字段极大一张图 Base64 编码可达 2MB脚本若json.load(f)后未及时释放内存累积。尤其批量处理时几百个 JSON 同时驻留内存。解决禁用imageData解析用json.loads的object_hook过滤def drop_image_data(d): if imageData in d: d[imageData] None # or del d[imageData] return d with open(json_path, r, encodingutf-8) as f: data json.load(f, object_hookdrop_image_data)4.5 现象classes.txt里类别顺序和labelme2yolo.py输出的 class_id 不一致训练报class 2 out of bounds原因build_class_mapping用sorted(list(all_labels))但中文 label 如苹果、香蕉在 ASCII 排序中排在英文后apple 香蕉为False导致classes.txt和映射字典顺序不一致。解决统一用 locale 排序或强制转拼音推荐轻量方案import locale try: locale.setlocale(locale.LC_COLLATE, zh_CN.UTF-8) label_list sorted(list(all_labels), keylocale.strxfrm) except: # fallback to ascii label_list sorted(list(all_labels))5. 验证与调试三步确认你的 YOLO label 100% 合规生成.txt文件只是第一步。YOLO 训练前必须验证 label 合法性否则 debug 成本远高于预防。我坚持的三步验证法已在 3 个千图级项目中拦截 92% 的数据问题。5.1 第一步用yolo check命令做静态检查YOLOv8YOLO 官方提供yolo check工具专为 label 格式诊断设计。它不训练只读取并校验# 安装最新 ultralytics确保 8.2.0 pip install --upgrade ultralytics # 检查 dataset.yaml 指向的 labels 目录 yolo check datadataset.yamldataset.yaml示例train: ../dataset/train/images val: ../dataset/val/images nc: 2 names: [apple, leaf]yolo check输出关键项Labels: 1200 OK, 3 WARNING, 0 ERROR→ OK 数是有效 label 文件数WARNING 可能是空文件或坐标越界ERROR 是致命格式错误如非数字字符。Label stats: class 0: 850, class 1: 350→ 确认类别分布合理无class 2等越界 id。Bounding box statistics: min_wh0.002, max_wh0.85→ 宽高在(0,1)内且不趋近于 0防 degenerate bbox。注意yolo check会自动尝试读取classes.txt若不存在或内容与dataset.yaml的names不一致会报Class names mismatch错误。5.2 第二步可视化 label肉眼确认对齐精度生成labels_viz/目录把每张图的.txt标注画回原图导出 PNG。这是发现 polygon 错位、bbox 偏移的最快方式# viz_labels.py import cv2 import numpy as np from pathlib import Path def draw_labels(image_path: str, label_path: str, output_path: str, class_names: List[str]): img cv2.imread(image_path) h, w img.shape[:2] with open(label_path, r) as f: for line in f: parts line.strip().split() if not parts: continue class_id int(parts[0]) if class_id len(class_names): continue if len(parts) 5: # detection _, cx, cy, bw, bh map(float, parts) x1 int((cx - bw/2) * w) y1 int((cy - bh/2) * h) x2 int((cx bw/2) * w) y2 int((cy bh/2) * h) cv2.rectangle(img, (x1,y1), (x2,y2), (0,255,0), 2) cv2.putText(img, class_names[class_id], (x1,y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,255,0), 1) else: # segmentation: points count must be even points list(map(float, parts[1:])) if len(points) % 2 ! 0: continue pts np.array([[int(p[0]*w), int(p[1]*h)] for p in zip(points[::2], points[1::2])], np.int32) cv2.polylines(img, [pts], True, (255,0,0), 2) cv2.putText(img, class_names[class_id], tuple(pts[0]), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255,0,0), 1) cv2.imwrite(output_path, img) # 批量执行 for img_path in Path(dataset/train/images).glob(*.jpg): label_path Path(dataset/train/labels) / f{img_path.stem}.txt if label_path.exists(): draw_labels(str(img_path), str(label_path), flabels_viz/{img_path.name}, [apple, leaf])运行后打开labels_viz/重点看三个位置图像四角是否有 bbox/polygon 被裁切说明坐标未 clip小目标边缘polygon 是否贴合物体轮廓验证归一化精度多边形顶点数是否与 LabelMe 中点击的点数一致防 points 丢失5.3 第三步用ultralytics.data.utils.verify_images做完整性扫描YOLO 内置工具可检查 image-label 一一对应、文件可读、尺寸匹配from ultralytics.data.utils import verify_images, verify_labels # 验证 images 目录 verify_images(dataset/train/images/, prefixtrain_images: ) # 验证 labels 目录自动关联同名 .txt verify_labels(dataset/train/labels/, dataset/train/images/, prefixtrain_labels: )输出示例train_labels: 1200 images, 1200 labels, 0 corrupt, 0 missing, 0 empty, 0 unverifiedcorrupt: label 文件无法解析如含中文逗号missing: 有xxx.jpg但无xxx.txtempty:xxx.txt存在但为空unverified: image 读取失败路径错、损坏这一步必须 0 error否则训练必 fail。6. 进阶技巧把转换流程嵌入 CI/CD让数据更新自动触发训练单次转换解决不了长期协作问题。当团队多人同时标注、每日新增 JSON、需要快速 retrain 模型时手动跑脚本是灾难。我的方案是用 GitHub Actions Docker 封装转换流水线PR 提交 JSON 后自动验证、转换、触发训练。这里不展开 DevOps只给最简可行的本地自动化模板。6.1 用inotifywait监控 JSON 目录Linux/macOS实时监听labelme_annotations/目录一旦有新 JSON立即转换# install inotify-tools sudo apt-get install inotify-tools # Ubuntu # or brew install inotify-tools # macOS # watch.sh #!/bin/bash JSON_DIR./labelme_annotations OUTPUT_DIR./yolo_dataset while inotifywait -e create,modify $JSON_DIR; do echo Detected change in $JSON_DIR, running conversion... python labelme2yolo.py \ --json_dir $JSON_DIR \ --output_dir $OUTPUT_DIR \ --mode segmentation \ --overwrite # 验证 yolo check datadataset.yaml # 可选触发训练 # yolo train datadataset.yaml modelyolov8n-seg.pt epochs100 done赋予执行权限并后台运行chmod x watch.sh nohup ./watch.sh watch.log 21 6.2 参数化脚本支持不同项目快速切换labelme2yolo.py加入配置文件支持避免硬编码路径# config.yaml input: json_dir: ./data/labelme image_subdir: images # JSON 中 imagePath 的相对子目录 output: root_dir: ./data/yolo mode: segmentation class_mapping: apple: 0 leaf: 1 stem: 2主函数加载配置import yaml def main(): with open(config.yaml) as f: cfg yaml.safe_load(f) convert_labelme_to_yolo( json_dircfg[input][json_dir], output_dircfg[output][root_dir], modecfg[output][mode], class_mappingcfg[output].get(class_mapping), )6.3 一键清理删除无效 label 和孤立 image转换后常有 orphaned files只有 image 无 label或反之。加一个清理脚本def cleanup_orphans(dataset_dir: str): dataset_dir Path(dataset_dir) img_dir dataset_dir / images lbl_dir dataset_dir / labels # Find images without labels img_stems {p.stem for p in img_dir.glob(*.*) if p.suffix.lower() in [.jpg,.jpeg,.png]} lbl_stems {p.stem for p in lbl_dir.glob(*.txt)} # Delete orphaned images for stem in img_stems - lbl_stems: for p in img_dir.glob(f{stem}.*): p.unlink() print(f️ Deleted orphaned image: {p.name}) # Delete orphaned labels for stem in lbl_stems - img_stems: (lbl_dir / f{stem}.txt).unlink() print(f️ Deleted orphaned label: {stem}.txt) cleanup_orphans(./yolo_dataset)我的习惯每次新项目启动先跑一遍cleanup_orphans再yolo check最后draw_labels抽样 50 张。这三步花 15 分钟省下后面 3 小时 debug 时间。LabelMe 到 YOLO 的转换本质不是格式搬运而是建立一套可审计、可回滚、可自动化的数据契约。当你看到labels_viz/apple_001.jpg上那个蓝色多边形严丝合缝包住苹果边缘时你就知道——数据链的最后一环稳了。希望帮到你。本文还有配套的精品资源点击获取
返回列表