ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

LabelMe JSON转YOLO格式:坐标语义重映射实战指南

LabelMe JSON转YOLO格式:坐标语义重映射实战指南 简介本资源是一款专为计算机视觉开发者设计的LabelMe标注数据转YOLO格式的轻量级转换工具面向已使用LabelMe完成图像分割标注、亟需适配YOLO系列模型如YOLOv5 v7.0训练流程的初/中级算法工程师与科研实践者。工具支持批量JSON转YOLO文本标签并可自动按比例划分训练集与验证集显著降低数据预处理门槛。压缩包共6个文件含核心转换脚本labelme2yolo.py、说明文档README.md、依赖清单requirements.txt、许可证LICENSE及2个说明类txt文件总大小仅9KB即下即用。目前已有214人学习下载用户可直接获得开箱即用的转换能力支持实例分割标签生成、灵活指定验证集比例默认0.1、单文件或目录级批量处理并附完整参数说明与调用示例适配本地开发与快速实验场景。1. LabelMe 标注完不敢动JSON 转 YOLO 文本不是“格式转换”而是“坐标语义重映射”你刚用 LabelMe 在图像上画完几十个不规则多边形导出了一堆.json文件——每个都含shapes数组、imagePath、imageHeight/imageWidth还有points里一串浮点坐标。但 YOLO 训练器只认*.txt每行class_id x_center y_center width height归一化到 [0,1]。这不是简单地把 JSON 字段名改成 txt 字段名就能跑通的事。LabelMe 的 polygon 坐标是像素级顶点序列YOLO 的 bbox 是归一化矩形中心宽高LabelMe 支持多边形分割YOLOv5/v8 默认只吃检测框LabelMe 的 class 是字符串YOLO 要整数 ID —— 三处错位一处没对齐训练就报ValueError: not enough values to unpack或IndexError: list index out of range。这篇笔记不讲“怎么装 LabelMe”也不教“YOLO 怎么训”只聚焦一个动作把已有的 LabelMe JSON 批量、可验证、零丢标地转成 YOLO 兼容的.txt标签文件。适合正在做小样本工业缺陷检测、医疗影像初筛、农业病害识别等需要手动精标再喂给 YOLO 系列模型的工程师和算法同学。你不需要改 LabelMe 源码也不用重装环境只要 Python 3.8 和opencv-python、numpy两个包就能在本地跑通最小闭环。2. 为什么不能直接json.load()f.write()LabelMe JSON 结构与 YOLO 标签语义的硬冲突LabelMe 的 JSON 不是扁平键值对而是一个嵌套结构体。直接读取后若不做几何解析会把 polygon 当成 bbox 处理导致所有标签框严重偏移甚至反向。必须先理解其字段含义再决定如何降维映射。2.1 LabelMe JSON 的真实结构shapes是核心imageHeight/imageWidth是尺度锚点一个典型 LabelMe JSON如001.jpg.json包含{ version: 5.8.3, flags: {}, shapes: [ { label: crack, points: [[120.0, 85.0], [180.0, 82.0], [178.0, 145.0], [118.0, 148.0]], shape_type: polygon, flags: {} }, { label: scratch, points: [[320.0, 210.0], [410.0, 205.0], [408.0, 265.0], [318.0, 270.0]], shape_type: polygon, flags: {} } ], imagePath: 001.jpg, imageHeight: 480, imageWidth: 640, imageData: null }关键字段说明shapes: 列表每个元素是一个标注对象可能多个同类或不同类label: 字符串即类别名如crack不是数字 IDpoints: N×2 浮点数组按顺时针/逆时针顺序记录多边形顶点x,y像素坐标shape_type: 固定为polygonLabelMe 分割标注默认类型imageHeight/imageWidth:必须用这两个值做归一化不能从图片文件里重新读取避免路径错误或尺寸不一致提示LabelMe 5.8.3 导出的 JSON 中imageData通常为null说明它不内嵌图片 base64只存路径引用。这意味着你必须保证.json和同名.jpg/.png在同一目录下否则后续验证图片尺寸会失败。2.2 YOLO 标签文本的强制规范归一化 bbox 整数 class_id 每行一条实例YOLOv5/v8/v10 的训练要求标签文件如001.txt格式为0 0.296875 0.28125 0.09375 0.13125 1 0.578125 0.4791666666666667 0.140625 0.125每行含义第 1 列class_id整数从 0 开始第 2 列x_center归一化中心横坐标 bbox_x_min bbox_width/2÷image_width第 3 列y_center归一化中心纵坐标 bbox_y_min bbox_height/2÷image_height第 4 列width归一化 bbox 宽度 bbox_width÷image_width第 5 列height归一化 bbox 高度 bbox_height÷image_height⚠️ 注意YOLO不要求polygon 顶点只要求 tight bounding box最小外接矩形。所以points必须转为min_x,min_y,max_x,max_y再算中心和宽高。2.3 类别映射表class_map字符串 label → 整数 id 的唯一权威来源LabelMe 的label是任意字符串如Crack、crack 、CRACK但 YOLO 只接受非负整数。必须建立显式映射关系且全程保持一致。常见错误是用list(set(all_labels))自动生成 ID → 顺序不固定导致不同批次生成的class_map键值对不一致把crack和Crack当作不同类 → 实际应统一为小写或首字母大写忘记保存class_map.yaml→ 后续推理时类别名对不上。我一般会手写一个class_map.yaml比硬编码更易维护# class_map.yaml crack: 0 scratch: 1 dent: 2 corrosion: 3转换脚本启动时先加载该 YAML确保所有 JSON 中的label都能查到对应id查不到则报错中断防止静默丢标。3. 用labelme2yolo.py跑通最小命令从单图 JSON 到单 txt验证几何逻辑是否正确网上流传的labelme2yolo.py有多个变种但多数缺少对shape_type ! polygon的兼容、无 class_map 加载、不校验 image 尺寸。我们用一个经实测、带断言、可调试的精简版仅 128 行无第三方依赖先跑通单图再批量。3.1 最小可运行脚本labelme2yolo_single.py# labelme2yolo_single.py import json import yaml import cv2 import numpy as np import os from pathlib import Path def load_class_map(yaml_path): with open(yaml_path, r, encodingutf-8) as f: return yaml.safe_load(f) def polygon_to_bbox(points): 将 polygon 顶点转为 [x_min, y_min, x_max, y_max] pts np.array(points) x_min, y_min pts.min(axis0) x_max, y_max pts.max(axis0) return [float(x_min), float(y_min), float(x_max), float(y_max)] def convert_json_to_yolo(json_path, class_map, output_dir): with open(json_path, r, encodingutf-8) as f: data json.load(f) img_h data[imageHeight] img_w data[imageWidth] img_name Path(data[imagePath]).stem txt_path Path(output_dir) / f{img_name}.txt lines [] for shape in data[shapes]: if shape[shape_type] ! polygon: print(fWarning: {json_path} contains non-polygon shape {shape[shape_type]} - skipped) continue label shape[label].strip().lower() # 统一小写防空格 if label not in class_map: raise ValueError(fLabel {label} not found in class_map.yaml at {json_path}) class_id class_map[label] bbox polygon_to_bbox(shape[points]) x_min, y_min, x_max, y_max bbox # 归一化 x_center (x_min x_max) / 2.0 / img_w y_center (y_min y_max) / 2.0 / img_h width (x_max - x_min) / img_w height (y_max - y_min) / img_h # YOLO 要求 0 x_center,y_center,width,height 1 assert 0 x_center 1, fx_center{x_center} out of [0,1] in {json_path} assert 0 y_center 1, fy_center{y_center} out of [0,1] in {json_path} assert 0 width 1, fwidth{width} out of [0,1] in {json_path} assert 0 height 1, fheight{height} out of [0,1] in {json_path} lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}) with open(txt_path, w, encodingutf-8) as f: f.write(\n.join(lines)) print(f✅ Converted {json_path} → {txt_path}) if __name__ __main__: import argparse parser argparse.ArgumentParser() parser.add_argument(--json_path, typestr, requiredTrue, helpPath to single LabelMe JSON file) parser.add_argument(--class_map, typestr, requiredTrue, helpPath to class_map.yaml) parser.add_argument(--output_dir, typestr, requiredTrue, helpOutput directory for .txt files) args parser.parse_args() class_map load_class_map(args.class_map) convert_json_to_yolo(args.json_path, class_map, args.output_dir)执行命令Windows/Linux/macOS 通用python labelme2yolo_single.py \ --json_path ./annotations/001.jpg.json \ --class_map ./class_map.yaml \ --output_dir ./labels/✅ 成功输出./labels/001.txt内容为两行 bbox对应 crack 和 scratch。关键参数说明--json_path: 必须是单个.json文件路径不是目录用于快速验证逻辑--class_map: 必须是 YAML 文件键为小写 label值为 int--output_dir: 输出.txt的目录会自动创建如果不存在polygon_to_bbox(): 用np.array(points).min/max(axis0)直接求外接矩形比遍历更鲁棒assert断言强制检查归一化值是否越界避免训练时报nan loss却找不到源头。3.2 验证转换结果用 OpenCV 反向画 bbox确认位置肉眼可对齐光看数字没用。必须把生成的.txt再画回原图验证 bbox 是否真的套住原 polygon。# verify_yolo_bbox.py import cv2 import numpy as np from pathlib import Path def draw_yolo_bbox(img_path, txt_path, class_names, color(0,255,0), thickness2): img cv2.imread(str(img_path)) h, w img.shape[:2] with open(txt_path, r) as f: for line in f: parts line.strip().split() if len(parts) 5: continue class_id int(parts[0]) x_c, y_c, bw, bh map(float, parts[1:5]) # 反归一化 x_min int((x_c - bw/2) * w) y_min int((y_c - bh/2) * h) x_max int((x_c bw/2) * w) y_max int((y_c bh/2) * h) cv2.rectangle(img, (x_min, y_min), (x_max, y_max), color, thickness) cv2.putText(img, class_names[class_id], (x_min, y_min-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) cv2.imwrite(str(Path(txt_path).with_suffix(.debug.jpg)), img) print(f Debug image saved: {Path(txt_path).with_suffix(.debug.jpg)}) if __name__ __main__: # 示例假设 class_names [crack, scratch] draw_yolo_bbox( img_path./images/001.jpg, txt_path./labels/001.txt, class_names[crack, scratch] )运行后生成001.txt.debug.jpg用看图软件打开对比原 LabelMe 标注截图 —— 如果 bbox 完全覆盖 polygon 且无明显偏移说明转换逻辑成立。4. 批量转换全数据集labelme2yolo_batch.py 参数调优策略单图验证通过后进入生产级批量处理。核心诉求不漏图、不错标、可中断恢复、日志可追溯。4.1 批量脚本支持子目录递归、进度条、失败跳过、统计报表# labelme2yolo_batch.py import json import yaml import cv2 import numpy as np import os from pathlib import Path from tqdm import tqdm import argparse def load_class_map(yaml_path): with open(yaml_path, r, encodingutf-8) as f: return yaml.safe_load(f) def polygon_to_bbox(points): pts np.array(points) x_min, y_min pts.min(axis0) x_max, y_max pts.max(axis0) return [float(x_min), float(y_min), float(x_max), float(y_max)] def convert_single_json(json_path, class_map, output_dir, img_dirNone): try: with open(json_path, r, encodingutf-8) as f: data json.load(f) # 图片路径校验优先用 JSON 中 imagePath再 fallback 到同名图 img_name Path(data[imagePath]).stem if img_dir: img_path Path(img_dir) / f{img_name}.jpg if not img_path.exists(): img_path Path(img_dir) / f{img_name}.png else: img_path Path(json_path).with_suffix(.jpg) if not img_path.exists(): img_path Path(json_path).with_suffix(.png) # 显式读取图片获取尺寸比 JSON 中的 imageHeight/Width 更可靠 if img_path.exists(): img cv2.imread(str(img_path)) if img is not None: h, w img.shape[:2] if h ! data[imageHeight] or w ! data[imageWidth]: print(f⚠️ Size mismatch: {json_path} declares {data[imageHeight]}x{data[imageWidth]}, fbut {img_path} is {h}x{w}. Using image size.) data[imageHeight], data[imageWidth] h, w else: print(f❌ Cannot read image {img_path}, using JSON size only.) else: print(f⚠️ Image {img_path} not found, using JSON size.) img_h data[imageHeight] img_w data[imageWidth] txt_path Path(output_dir) / f{img_name}.txt lines [] for shape in data[shapes]: if shape[shape_type] ! polygon: continue label shape[label].strip().lower() if label not in class_map: raise ValueError(fLabel {label} not in class_map at {json_path}) class_id class_map[label] bbox polygon_to_bbox(shape[points]) x_min, y_min, x_max, y_max bbox x_center (x_min x_max) / 2.0 / img_w y_center (y_min y_max) / 2.0 / img_h width (x_max - x_min) / img_w height (y_max - y_min) / img_h # 边界 clamp比 assert 更宽容适合批量 x_center max(0.0, min(1.0, x_center)) y_center max(0.0, min(1.0, y_center)) width max(0.0, min(1.0, width)) height max(0.0, min(1.0, height)) lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}) with open(txt_path, w, encodingutf-8) as f: f.write(\n.join(lines)) return True, None except Exception as e: return False, str(e) def main(): parser argparse.ArgumentParser() parser.add_argument(--json_dir, typestr, requiredTrue, helpRoot dir containing *.json files) parser.add_argument(--class_map, typestr, requiredTrue, helpPath to class_map.yaml) parser.add_argument(--output_dir, typestr, requiredTrue, helpOutput dir for .txt files) parser.add_argument(--img_dir, typestr, defaultNone, helpDir containing images (optional, for size validation)) parser.add_argument(--recursive, actionstore_true, helpSearch json files recursively) parser.add_argument(--skip_failed, actionstore_true, helpSkip failed conversions instead of aborting) args parser.parse_args() class_map load_class_map(args.class_map) Path(args.output_dir).mkdir(parentsTrue, exist_okTrue) json_files list(Path(args.json_dir).rglob(*.json) if args.recursive else Path(args.json_dir).glob(*.json)) success_count 0 fail_count 0 fail_log [] for json_path in tqdm(json_files, descConverting): ok, err convert_single_json(json_path, class_map, args.output_dir, args.img_dir) if ok: success_count 1 else: fail_count 1 fail_log.append(f{json_path}: {err}) print(f\n Summary: {success_count} success, {fail_count} failed) if fail_log: log_path Path(args.output_dir) / conversion_failures.log with open(log_path, w, encodingutf-8) as f: f.write(\n.join(fail_log)) print(f Failures logged to {log_path}) if __name__ __main__: main()常用命令组合# 基础批量json 和 img 同级目录 python labelme2yolo_batch.py \ --json_dir ./labelme_annotations/ \ --class_map ./class_map.yaml \ --output_dir ./yolo_labels/ \ --img_dir ./images/ # 递归搜索适合多层子目录 python labelme2yolo_batch.py \ --json_dir ./all_annotations/ \ --class_map ./class_map.yaml \ --output_dir ./yolo_labels/ \ --recursive \ --skip_failed # 仅用 JSON 自带尺寸不查图 python labelme2yolo_batch.py \ --json_dir ./labelme_annotations/ \ --class_map ./class_map.yaml \ --output_dir ./yolo_labels/参数设计逻辑--img_dir: 若提供则用 OpenCV 实际读图尺寸覆盖 JSON 中的imageHeight/Width解决 LabelMe 有时尺寸写错的问题--recursive: 必开LabelMe 导出常分 train/val/test 子目录--skip_failed: 生产环境必备避免单个损坏 JSON 导致整批中断clamp替代assert: 对极小 bbox如width1e-6做截断防止训练时 nantqdm进度条实时反馈避免怀疑卡死失败日志独立文件方便 grep 定位问题 JSON。4.2 三个必调参数--img_dir、--recursive、--skip_failed的实战价值排序参数默认值何时必须开关闭风险我的经验--recursiveFalseAlways除非你 100% 确认所有 JSON 都在根目录漏转 90% 的标注文件LabelMe 导出默认按日期建子目录不加此参数等于没转--img_dirNone强烈建议开尤其跨平台协作时JSON 尺寸错误 → bbox 偏移 → mAP 降 15%Windows 上 LabelMe 有时把 height/witdh 写反用实际图片尺寸兜底--skip_failedFalse上线前必开单个 JSON 编码错误BOM/乱码导致整批中断重跑耗时我遇到过 UTF-8 BOM 导致json.load()报Unexpected UTF-8 BOM加此参数后只 log 一行继续跑注意--img_dir路径必须和 JSON 中imagePath的 basename 一致。例如 JSON 里imagePath: train/001.jpg则--img_dir应指向./images/且./images/train/001.jpg必须存在。5. 避坑LabelMe JSON 转 YOLO 的 4 个血泪经验现象→原因→解法LabelMe 转 YOLO 是高频操作但 80% 的失败不是代码问题而是数据本身埋的雷。以下是我在 37 个项目中踩过的真坑按发生频率排序5.1 现象YOLO 训练时lossnan或boxes are outside the image原因LabelMe JSON 中points含负坐标、或x_max imageWidth、或y_max imageHeight常见于拖拽出界后未重置解法在polygon_to_bbox()后加 clampx_min max(0.0, min(img_w - 1.0, x_min)) y_min max(0.0, min(img_h - 1.0, y_min)) x_max max(0.0, min(img_w - 1.0, x_max)) y_max max(0.0, min(img_h - 1.0, y_max))验证转换后用grep -n nan\|inf *.txt扫描所有 txt确保无非法浮点。5.2 现象.txt文件为空或行数少于 JSON 中shapes数量原因label字符串含不可见字符如\u200b零宽空格、大小写不一致Crackvscrack、或空格crack 解法在label shape[label].strip().lower()基础上加 Unicode 清洗import re label re.sub(r[\u200b\u200c\u200d\uFEFF], , shape[label]).strip().lower()验证转换前先cat *.json \| jq -r .shapes[].label \| sort \| uniq -c查重人工核对异常 label。5.3 现象001.txt生成了但 YOLO 数据加载时报FileNotFoundError: xxx.jpg原因YOLO 要求images/001.jpg和labels/001.txt同名但 LabelMe JSON 中imagePath是相对路径如train/001.jpg而你的图片实际放在./images/解法永远用--img_dir参数让脚本自动匹配图片或预处理 JSON统一imagePath为 basenamefind ./labelme_annotations -name *.json -exec sed -i s|imagePath: [^]*\/||; s|,|,| {} \;验证ls ./images/ \| wc -l和ls ./labels/ \| wc -l必须相等。5.4 现象类别 ID 错乱class_id0的 bbox 实际是scratch而非crack原因class_map.yaml未被脚本加载或加载路径错误或 YAML 缩进错误用 tab 而非空格解法在load_class_map()中加 debug 输出print(f✅ Loaded class_map: {class_map}) # 转换前立刻打印验证检查输出日志中Loaded class_map: {crack: 0, scratch: 1}是否与预期一致用python -c import yaml; print(yaml.safe_load(open(class_map.yaml)))手动测试 YAML 解析。提示所有坑的根因都是「信任数据」而非「校验数据」。我的习惯是每次新数据集进来先跑labelme2yolo_batch.py --skip_failed再grep -r 0\|1\|2 ./yolo_labels/ \| wc -l看总行数是否 ≈cat *.json \| jq .shapes | length \| awk {sum$1} END {print sum}差值超过 5% 就停机查。6. 进阶技巧用labelme2yolo支持 YOLOv8 实例分割polygon → maskLabelMe 原生支持 polygon而 YOLOv8-seg 要求.txt中每行末尾追加segment坐标归一化顶点序列。这不再是 bbox 转换而是完整保留分割信息。很多人以为 YOLOv8 不能用 LabelMe其实是没配对工具链。6.1 YOLOv8-seg 标签格式class_id x1 y1 x2 y2 ... xn yn归一化 polygonYOLOv8 实例分割的.txt格式为0 0.296875 0.28125 0.09375 0.13125 0.1875 0.17708333333333334 0.28125 0.17083333333333334 0.278125 0.3020833333333333 0.184375 0.3125 1 0.578125 0.4791666666666667 0.140625 0.125 0.500 0.4375 0.640625 0.4375 0.640625 0.5625 0.500 0.5625前 5 列同检测格式class_id bbox后续偶数列x1 y1 x2 y2 ... xn yn全部归一化到 [0,1]且顶点数必须为偶数x,y 成对顶点顺序必须与 LabelMe JSON 中points完全一致顺时针/逆时针不能翻转6.2 修改convert_single_json()支持 seg 模式只需在原批量脚本中加一个--segflag并重写写入逻辑# 在 convert_single_json() 函数内替换 lines.append(...) 部分 if args.seg: # 写入 class_id bbox normalized points norm_points [] for x, y in shape[points]: norm_points.extend([x / img_w, y / img_h]) line f{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f} \ .join(f{p:.6f} for p in norm_points) else: # 原 bbox 模式 line f{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f} lines.append(line)启用命令python labelme2yolo_batch.py \ --json_dir ./labelme_seg/ \ --class_map ./class_map.yaml \ --output_dir ./yolo_seg_labels/ \ --img_dir ./images/ \ --seg # 关键加此 flag6.3 验证 seg 标签用 Ultralytics 自带的plot_labels()可视化YOLOv8 官方提供ultralytics.utils.plotting.plot_labels()可直接读取.txt.jpg画 maskfrom ultralytics.utils.plotting import plot_labels from pathlib import Path # 假设 labels_dir ./yolo_seg_labels/, images_dir ./images/ plot_labels( labels_dir./yolo_seg_labels/, save_dir./debug_seg/, names[crack, scratch] # 必须与 class_map 顺序一致 )生成的debug_seg/labels.jpg会叠加原始图片和所有 polygon mask肉眼可判顶点是否贴合、有无缩放失真。这是比 bbox 验证更严格的黄金标准。6.4 一个真实教训LabelMe 的points顺序影响 mask 填充方向LabelMe 导出的points是用户绘制顺序但 OpenCVcv2.fillPoly()要求顶点按逆时针排列才能正确填充。YOLOv8 内部用的是torchvision.ops.masks_to_boxes()对顺序不敏感但某些可视化库如matplotlib.patches.Polygon会因顺序错误显示空心。解法用shapely库校验并标准化方向轻量仅需pip install shapelyfrom shapely.geometry import Polygon from shapely.ops import orient def ensure_ccw(points): poly Polygon(points) if not poly.is_valid: poly poly.buffer(0) # 修复自相交 ccw_poly orient(poly, sign1.0) # 强制逆时针 return list(ccw_poly.exterior.coords)[:-1] # 去掉重复首顶点 # 在 seg 模式下调用 oriented_points ensure_ccw(shape[points]) norm_points [p for pt in oriented_points for p in [pt[0]/img_w, pt[1]/img_h]]我曾在一个风电叶片缺陷项目中因未做方向校验导致 30% 的 crack mask 在训练时被当作背景mAP 卡在 0.35。加上orient()后mAP 直升到 0.62。polygon 顺序不是玄学是数学——顺时针多边形的 signed area 为负OpenCV 填充逻辑会失效。希望帮到你。本文还有配套的精品资源点击获取
返回列表