资讯详情

labelme转YOLO关键点格式:坐标归一化与可见性编码实战

📅 2026/10/26 20:53:18 | 华诺云谱 👁 阅读
labelme转YOLO关键点格式:坐标归一化与可见性编码实战
做关键点检测项目的人多半都经历过这个瞬间在 labelme 里辛辛苦苦点完几十张图的关键点导出 JSON 一看信心满满地丢给 YOLO 训练脚本结果第一行就报错——找不到标签文件或者找到了但格式对不上。我最初做自用关键点检测时也卡在这一步labelme 的 JSON 结构和 YOLO 关键点检测要求的 txt 标注之间隔着一整套坐标归一化、关键点可见性编码和数据集目录约定的转换逻辑。这篇就把我自己那套 labelme2yolo 格式转换的完整做法拆开讲清楚包括坐标怎么算、可见性标志怎么定、目录怎么摆、脚本怎么写以及我踩过的几个不写出来你大概率也会踩的坑。适合已经会用 labelme 打点、准备把数据喂给 YOLO 关键点检测模型的自用开发者也适合想搞清楚两种标注格式差异的读者。1. 先搞清楚两种格式到底差在哪动手写转换脚本之前必须先把 labelme 的输出和 YOLO 关键点检测的输入这两套格式逐字段对齐。很多人转换失败不是因为代码写错而是压根没弄清两边各存了什么、缺了什么。我见过最常见的误解是以为都是关键点坐标搬过去就行结果训练时 loss 一直不降回头查才发现关键点顺序和可见性全乱了。1.1 labelme 的 JSON 里究竟存了什么labelme 保存的是一份 JSON核心结构大致是这样顶层有imagePath、imageWidth、imageHeight、imageData可选取决于保存时是否内嵌图像然后是shapes数组。每个 shape 里有label标签名、points坐标点数组、shape_type点、矩形、多边形等、group_id等字段。关键点标注场景下shape_type通常是point每个 shape 的points里只有一个[x, y]。也就是说一张图上有几个关键点就有几个独立的 shape每个 shape 的label就是那个关键点的名字比如nose、left_shoulder、right_hip之类。这里有个容易被忽略的点labelme 的坐标是像素绝对坐标原点在图像左上角x 向右、y 向下和大多数图像处理库一致。还有一个细节值得单独说labelme 的 JSON 里imageWidth和imageHeight是判断坐标是否越界、归一化是否正确的重要依据。有些版本在未内嵌图像时这两个字段可能缺失或为 0转换脚本必须做兜底否则归一化时除以 0 直接崩。我一般会在脚本里优先读这两个字段读不到就用图像库打开原图取尺寸双保险。1.2 YOLO 关键点检测的 txt 长什么样YOLO 的关键点检测标注是每张图一个 txt每行描述一个目标实例。一行的结构是类别索引、目标框中心 x、目标框中心 y、目标框宽、目标框高这五个是归一化到 0~1 的浮点数紧接着是 K 组三元组每组是x, y, vK 是关键点总数x、y 同样归一化到 0~1v 是可见性标志0 表示未标注/不可见1 表示标注了但被遮挡2 表示可见且未遮挡。这里就是第一个大坑YOLO 关键点格式要求每个实例都带一个目标框而 labelme 打点往往只标了关键点没有框。自用场景下通常一张图只有一个目标比如一个人、一只手那这个框就得自己从关键点坐标里推出来——取所有关键点的最小外接矩形再适当外扩一点。如果一张图有多个目标实例那还得靠group_id或标签命名约定把关键点分组这部分复杂度陡增自用项目里我一般直接约定一图一实例省掉分组逻辑。第二个坑是关键点顺序必须全局固定。YOLO 训练时是按索引对齐关键点的第 0 个三元组永远对应你定义的第一个关键点。labelme 里 shape 的排列顺序是标注时的点击顺序完全不可靠。所以转换脚本里必须维护一份固定的关键点名称列表按这个列表的顺序去 JSON 里查找对应 label 的坐标找不到就填0 0 0。这份列表的顺序一旦定下就不能改改了就得重新转换全部数据。对比项labelme JSONYOLO 关键点 txt存储单位每图一个 JSON每图一个 txt坐标形式像素绝对坐标归一化 0~1关键点组织shapes 数组顺序为点击顺序固定索引顺序的三元组可见性无显式字段v 值 0/1/2目标框需另画矩形 shape每行必带归一化 cx cy w h类别信息label 字符串类别索引整数2. 转换脚本的核心逻辑拆解格式对齐之后脚本的逻辑其实不复杂但每一步都有讲究。我把自己那版脚本拆成四个环节读 JSON 取尺寸、按固定列表收集关键点、推算目标框、归一化并写行。下面逐个说顺带把每步为什么这么写讲透。2.1 关键点名称列表与索引映射脚本开头第一件事是定义关键点列表比如做一个手部关键点检测就定义 21 个点的名字顺序按你训练时要用的语义顺序来比如从手腕开始、按手指依次排。这个列表同时决定了输出 txt 里三元组的排列也决定了模型输出通道的语义。KEYPOINT_NAMES [ wrist, thumb_cmc, thumb_mcp, thumb_ip, thumb_tip, index_mcp, index_pip, index_dip, index_tip, # ... 其余手指 ] NAME_TO_IDX {name: i for i, name in enumerate(KEYPOINT_NAMES)}用字典做名称到索引的映射是为了后面在 shapes 里快速定位。这里有个实操建议labelme 里打点时标签名一定要和这份列表完全一致大小写、下划线都不能差。我吃过一次亏labelme 里手滑写成Thumb_Tip脚本里是thumb_tip结果那个点永远填 0训练出来的模型那个指尖位置一直飘查了大半天才发现是标签名不匹配。稳妥做法是在脚本里加一条告警JSON 里出现列表之外的 label 时打印出来方便及时发现拼写问题。2.2 从关键点反推目标框的几种策略前面说了 YOLO 每行必须带框而 labelme 只标点时没有框。反推框有几种策略各有适用场景。第一种是关键点最小外接矩形直接当框。取所有有效关键点的 min x、max x、min y、max y 组成矩形。优点是简单、和关键点严格对应缺点是如果关键点分布集中比如只标了脸部几个点框会偏小模型学到的目标范围偏窄。第二种是外接矩形按比例外扩。在第一种基础上宽高各放大一定比例比如 1.2 倍中心不变。这个比例需要根据你的目标类型调手部这种关键点基本覆盖整个目标的1.1~1.2 就够人体这种关键点集中在骨架、四肢末端离轮廓还有距离的可能要 1.3~1.5。我一般先取 1.2 跑一版看验证集上框的召回情况再微调。第三种是labelme 里同时画了矩形框。如果你打点时顺手画了包围框shape_type 为 rectangle那直接读那个框最准脚本里优先找 rectangle 类型的 shape找不到再用关键点反推。这是最推荐的做法多花几秒画个框省掉后面调外扩比例的麻烦。外扩之后还要做边界裁剪把框限制在图像范围内否则归一化后出现负数或大于 1 的值训练时可能报错或产生异常梯度。裁剪逻辑就是x1 max(0, x1)、x2 min(W, x2)这一套别嫌琐碎。2.3 归一化与可见性标志的确定归一化本身是一行除法x_norm x_pixel / image_widthy_norm y_pixel / image_height。但要注意两点一是必须用同一张图的宽高别拿错图二是浮点精度输出时保留 6 位小数足够YOLO 读取时对精度不敏感但保留太少比如 2 位在关键点密集时会有明显偏差。可见性 v 值的确定是自用场景里最容易被糊弄过去的一环。严格来说v2 表示可见未遮挡v1 表示标注了但被遮挡v0 表示未标注。labelme 打点时你其实无法直接表达遮挡这个语义除非用不同标签或额外字段。我的做法是只要这个关键点在 JSON 里存在就填 v2不存在就填0 0 0坐标和 v 全 0。这在自用项目里够用因为大部分数据关键点都是可见的。如果你的数据里有大量遮挡情况建议在 labelme 里用标签后缀区分比如nose_occ表示遮挡的鼻子脚本里识别后缀映射成 v1这样模型能学到遮挡状态效果会更好。提示v0 的三元组坐标也必须是 0不能填真实坐标配 v0。有些实现会忽略 v0 的坐标但为了兼容性统一填0 0 0最稳。2.4 写文件与目录组织YOLO 数据集的标准目录结构是images/train、images/val、labels/train、labels/valtxt 文件名必须和对应图片文件名不含扩展名一致。转换脚本输出时就要按这个结构落盘别先平铺再手动挪容易漏。import json, os from pathlib import Path def convert_one(json_path, out_label_dir, img_w, img_h): with open(json_path, r, encodingutf-8) as f: data json.load(f) w data.get(imageWidth) or img_w h data.get(imageHeight) or img_h kps [[0.0, 0.0, 0] for _ in KEYPOINT_NAMES] xs, ys [], [] for shape in data[shapes]: if shape[shape_type] ! point: continue name shape[label] if name not in NAME_TO_IDX: print(f[warn] unknown label {name} in {json_path}) continue x, y shape[points][0] idx NAME_TO_IDX[name] kps[idx] [x / w, y / h, 2] xs.append(x); ys.append(y) if not xs: return # 没有有效关键点跳过 x1, x2 min(xs), max(xs) y1, y2 min(ys), max(ys) cx, cy (x1 x2) / 2, (y1 y2) / 2 bw, bh (x2 - x1) * 1.2, (y2 - y1) * 1.2 # 裁剪到图像范围 cx min(max(cx, 0), w); cy min(max(cy, 0), h) bw min(bw, w); bh min(bh, h) line f0 {cx/w:.6f} {cy/h:.6f} {bw/w:.6f} {bh/h:.6f} for x, y, v in kps: line f {x:.6f} {y:.6f} {v} out_path Path(out_label_dir) / (Path(json_path).stem .txt) out_path.write_text(line \n, encodingutf-8)这段是核心骨架实际用的时候外面套一层遍历目录、按比例划分 train/val 的逻辑。划分时注意图片和标签要同步移动我习惯先按文件名列表 shuffle 再切分保证两边严格对应。3. 我踩过的几个坑和对应解法转换脚本跑通只是第一步真正让训练顺利的是那些格式之外的细节。下面这几个坑都是我实际踩过、并且发现身边做同类项目的人也在踩的写出来能帮你省不少调试时间。3.1 关键点顺序错乱导致 loss 不降最隐蔽的一个坑。脚本能跑、文件能生成、训练也不报错但 loss 就是降不下去验证时关键点位置整体偏移。原因往往是 labelme 里 shape 的点击顺序和脚本里的关键点列表顺序不一致而脚本又偷懒按 shapes 数组顺序直接写三元组没做名称映射。解法就是前面说的永远按固定名称列表映射绝不依赖 shapes 顺序。另外建议转换完后抽几张图做可视化验证——把 txt 里的关键点按索引画回原图和 labelme 里的点对比一眼就能看出顺序对不对。这个验证步骤花五分钟能省几小时排查。3.2 图像尺寸字段缺失或与实际不符有些 labelme 版本在保存时imageWidth、imageHeight可能为 0或者你后期用工具裁剪过图片但 JSON 没更新导致归一化基准错误所有坐标整体偏移。我的处理是以实际图像文件尺寸为准脚本里用图像库打开原图取sizeJSON 里的尺寸只作为交叉校验两者不一致时打印告警并以图像为准。from PIL import Image with Image.open(img_path) as im: img_w, img_h im.size这一步多一次 IO但换来的是数据可靠性值得。3.3 空标签文件和全零关键点的处理如果某张图的关键点全被过滤掉标签名不匹配、shape 类型不对脚本会生成一个只有框、关键点全零的行或者干脆空文件。空文件在训练时可能被当成无目标样本影响负样本比例全零关键点的行则会让模型学到这张图没有关键点污染数据。我的做法是没有有效关键点的图直接跳过不生成 txt同时把对应图片从数据集里剔除。转换脚本最后打印一份统计总 JSON 数、成功转换数、跳过数及跳过原因方便你回头补标。这个统计输出看似小事实际能帮你快速发现为什么训练集比预期少了几十张。3.4 多实例场景的分组难题前面提过自用项目我一般约定一图一实例。但如果你确实需要一图多目标比如多人姿态labelme 的group_id字段就是用来分组的打点时给同一个目标的点设相同 group_id。转换脚本里按 group_id 分组每组独立算框、独立写一行。注意 group_id 为 0 或空时通常表示未分组需要单独处理成一组或直接跳过。这块逻辑比单实例复杂不少建议先确认单实例流程跑通再上多实例。坑点现象根因解法顺序错乱loss 不降、关键点整体偏移依赖 shapes 点击顺序固定名称列表映射 可视化验证尺寸不符坐标整体偏移JSON 尺寸缺失或过期以实际图像尺寸为准空标签训练集数量对不上无效关键点未过滤跳过并统计原因多实例关键点串到别的目标上未按 group_id 分组按 group_id 分组独立成行4. 转换完成后的自检清单脚本跑完不等于数据就绪我习惯在开训前过一遍自检清单这几项检查能拦掉大部分训练异常但报错不明显的问题。4.1 数量与命名一致性核对先核对三组数量JSON 文件数、生成的 txt 数、对应图片数。三者应该满足txt 数 ≤ 图片数 JSON 数差值就是被跳过的样本对照脚本打印的跳过原因确认是否合理。再抽查文件名随机抽 10 个 txt确认每个都能在 images 目录找到同名图片扩展名不同反之亦然。命名不一致是训练时报找不到标签的头号原因提前核对能省事。4.2 坐标范围与关键点可视化抽检写个小脚本遍历所有 txt检查每个数值是否都在 0~1 之间v 值除外它是 0/1/2 整数框的宽高是否大于 0。任何越界值都说明归一化或裁剪有问题。然后抽 5~10 张图做可视化读 txt把框画成矩形、关键点按索引画成带编号的点叠在原图上保存。人眼扫一遍重点看关键点是否落在语义正确的位置、框是否合理包住目标。这一步是最后一道防线我每次转换新数据集都会做。4.3 数据集配置文件的关键点参数YOLO 训练还需要一个数据集配置文件通常是 yaml里面除了路径关键点检测还要声明kpt_shape: [K, 3]K 是关键点数量3 表示每个点三个值x、y、v。这个数字必须和 txt 里三元组数量严格一致写错了训练直接报维度不匹配。另外类别数nc和类别名也要对上自用单类别就写 1 和对应名字。配置文件里路径建议用绝对路径或相对于训练脚本的路径别用容易歧义的相对路径我见过因为路径解析问题导致训练读到空数据集的案例。path: /data/my_kpt_dataset train: images/train val: images/val nc: 1 names: [hand] kpt_shape: [21, 3]注意kpt_shape的 K 必须等于你 KEYPOINT_NAMES 的长度改关键点定义时这两处要同步改漏改一处就是维度报错。5. 让转换流程更省心的几个习惯这套流程我迭代了几版最后沉淀下来几个习惯算不上什么高深技巧但确实让每次换数据集时少折腾很多。第一个习惯是把关键点定义单独放一个配置文件脚本从配置读列表而不是硬编码在代码里。这样换项目时只改配置不改代码也方便把同一份定义共享给训练配置和可视化脚本避免多处维护导致不一致。第二个习惯是转换脚本支持 dry-run 模式只统计不写文件先看一遍有多少样本会被跳过、跳过原因分布如何确认没问题再真正落盘。数据量大时这个模式能避免反复生成又删除的麻烦。第三个习惯是保留原始 JSON 和转换后 txt 的映射日志记录每个 txt 来自哪个 JSON、用了哪张图的尺寸、跳过了哪些标签。出问题时顺着日志能快速定位是哪个环节出的岔子比盲目重跑高效得多。第四个习惯是版本化关键点定义。一旦用某版定义训出了可用模型就把那份定义连同转换脚本一起存档。后面想加关键点或调整顺序时新建一版而不是改旧的老数据用老定义、新数据用新定义避免改了定义导致旧模型无法复现的尴尬。自用项目最容易忽视这点等到想对比两版模型效果时才发现数据定义已经悄悄变了。这套 labelme 到 YOLO 关键点格式的转换说到底就是把人打点的自由格式翻译成模型要的严格格式难点不在代码量而在对两边格式细节的理解和对边界情况的处理。把名称映射、尺寸基准、框反推、可见性这几件事做扎实剩下的就是跑起来看效果、按需微调了。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑