☰
labelme转YOLO关键点格式:避坑指南与脚本实现
2026/10/26 20:51:56 网站建设 项目流程

关键点检测这个方向,做的人不少,但真正踩过数据格式转换坑的人才知道,从标注工具到训练框架之间那条“最后一公里”有多磨人。我最近在做一个自用的姿态估计小项目,标注环节用的是labelme,训练端用的是YOLO系列的关键点检测分支,中间卡在格式转换上整整折腾了两天。标题里说的“labelme2yolo格式改变”,说白了就是把labelme那套JSON标注,转成YOLO关键点检测能直接吃的TXT格式,同时把关键点顺序、可见性标志、归一化坐标这些细节全部对齐。这件事看起来只是写个脚本的事,但实际动手你会发现,坑集中在坐标系差异、关键点索引映射、以及YOLO对关键点可见性的特殊约定上。这篇内容适合正在做关键点检测、手里已经有一批labelme标注、又不想重新标一遍的同行参考,也适合刚接触YOLO关键点任务、想搞清楚标注格式底层逻辑的朋友。下面我把自己从踩坑到跑通的全过程拆开讲,包括脚本怎么写、参数怎么定、哪里最容易出错。

1. 整体思路与方案选型拆解

1.1 为什么不用现成转换工具而选择自己写脚本

市面上确实有一些开源的labelme转YOLO工具,我一开始也试了两个,但很快就放弃了。原因很直接:绝大多数现成工具是针对目标检测写的,输出的是类别加边界框,根本不处理关键点。少数支持关键点的脚本,关键点顺序是写死的,而我的项目里关键点定义有自己的顺序要求,比如先鼻尖再左右眼再左右耳,跟脚本内置的顺序对不上,转出来之后训练时关键点全串位了。还有一个更隐蔽的问题,现成工具对“可见性”的处理往往是二值的,要么可见要么不可见,但YOLO关键点格式实际上支持三种状态:可见、遮挡、未标注。这个差异在训练时影响很大,遮挡点如果被当成可见点参与损失计算,模型会学到错误的定位。

自己写脚本的好处是每个环节都可控。坐标系怎么转、关键点怎么排序、可见性怎么映射、归一化用哪张图的宽高,全部由自己决定。而且脚本一旦跑通,后续换数据集、加关键点、改类别,只需要动几行配置,不用再去读别人的源码猜逻辑。我最终的方案是用Python写一个单文件脚本,读labelme的JSON目录,输出YOLO需要的images和labels两个目录,关键点顺序和可见性映射通过一个配置字典管理。

1.2 labelme与YOLO关键点格式的核心差异

要把这件事讲清楚,得先把两边的格式摆出来对比。labelme标注关键点时,每个点是一个shape,类型是point,坐标是图像像素坐标,附带一个label名字。一张图的所有点放在同一个JSON的shapes数组里,顺序就是你点击的顺序。而YOLO关键点检测的标注格式,一行代表一个目标,结构是:类别索引、边界框中心x、边界框中心y、边界框宽、边界框高,后面紧跟每个关键点的x、y、可见性,全部归一化到0到1之间。

这里第一个大坑就出现了:labelme只给你点,不给你边界框。YOLO格式却强制要求前面有框。这个框怎么来?有两种做法,一种是用所有关键点的外接矩形当框,另一种是人工再标一遍框。我选的是外接矩形,因为关键点检测任务里框本身不是重点,它更多是给模型一个目标级的定位先验,用关键点外接矩形完全够用。但外接矩形要留一点余量,否则框贴得太紧,训练时数据增强裁剪容易把边缘关键点裁掉。

第二个差异是归一化基准。labelme的坐标是绝对像素,YOLO要的是相对图像宽高的比例。这里必须用每张图自己的宽高去归一化,不能用数据集统一的尺寸,否则不同分辨率的图会全部错位。我在脚本里是逐图读取JSON里的imageWidth和imageHeight字段来做归一化的。

第三个差异是可见性。labelme的point没有可见性字段,所有点默认都是标了的。但实际标注时,有些点是被遮挡的,标注者可能凭经验点了个大概位置。YOLO的可见性约定是0表示未标注、1表示遮挡、2表示可见。我的处理策略是:在labelme的label名字里加后缀来区分,比如nose_vis表示可见,nose_occ表示遮挡,脚本解析时根据后缀映射到1或2,没有后缀的默认按可见处理。这样既不用改标注工具,又能把可见性信息带过去。

1.3 关键点顺序映射的设计考量

关键点顺序是整件事里最容易被忽视、又最致命的一环。YOLO训练时,输出层的每个关键点通道是固定顺序的,你的标注顺序必须和模型定义的关键点顺序完全一致,错一个位置,整条数据就废了。labelme里点的顺序取决于标注者点击的先后,不同人标、甚至同一个人不同时间标,顺序都可能不一样。所以脚本里必须有一个显式的映射表,把label名字映射到固定的索引。

我的做法是维护一个有序列表,比如["nose", "left_eye", "right_eye", "left_ear", "right_ear", ...],脚本读到一个点,就去列表里查它的名字对应的索引,然后按索引把坐标填到对应位置。如果某个关键点在图上没标,就填0、0、0,可见性填0表示未标注。这个设计的好处是,无论标注者点击顺序如何,输出永远是固定顺序。这里有个细节要注意:映射表里的名字必须和labelme里写的label完全一致,大小写、下划线都不能差,我一开始就是因为left_eye写成了leftEye,排查了半天才发现是名字对不上导致整批点被当成未标注。

2. 核心细节解析与实操要点

2.1 坐标归一化的计算过程与边界处理

归一化的公式本身很简单,x_norm等于x_pixel除以image_width,y_norm等于y_pixel除以image_height。但实操中有几个边界情况必须处理。第一,坐标可能因为标注时的像素取整出现等于image_width的情况,归一化后变成1.0,而YOLO要求坐标严格在0到1之间,等于1在某些实现里会越界。我的处理是统一做一次截断,把归一化后的值限制在0.0001到0.9999之间,避免边界值引发训练时的索引异常。

第二,外接矩形的计算要基于归一化前的像素坐标,算完再归一化。框的中心点是(x_min + x_max) / 2,宽是x_max - x_min。这里要注意,如果某个目标只有一个关键点被标了,外接矩形会退化成一个点,宽高为0,这种样本在训练时会出问题。我的策略是给框加一个最小尺寸,比如宽高都不小于图像尺寸的百分之一,保证框有实际面积。这个最小尺寸不是拍脑袋定的,太小起不到作用,太大又会把框撑得离谱,百分之一是我在几百张图上试出来的经验值,既能兜住退化情况,又不会明显改变正常样本的框。

第三,关于框的余量。前面提到外接矩形要留余量,我的做法是把框的宽高各放大百分之十,中心点不变。这个比例的依据是,关键点检测常用的数据增强里,随机裁剪的幅度一般在百分之十以内,留百分之十的余量能保证裁剪后关键点仍在框内。放大太多会让框失去定位意义,放大太少又防不住裁剪,百分之十是个平衡点。

2.2 可见性映射的实操约定

可见性这块,我在标注阶段就做了约定,而不是等到转换时再猜。具体做法是在labelme的label命名上加后缀:正常可见的点直接写名字,比如nose;被遮挡但位置可推断的点写nose_occ;完全看不清、只是占位的点写nose_inv。脚本解析时,按后缀映射:无后缀映射为2,_occ映射为1,_inv映射为0。

这个约定的价值在于,它把可见性判断留在了标注现场,而不是转换时凭坐标猜。标注的人最清楚那个点当时能不能看见,转换脚本没有这个信息。我试过用坐标是否在图像内来推断可见性,结果完全不可靠,因为遮挡点的坐标也在图像内,只是被别的物体挡住了。所以可见性必须由标注环节带出来,这是我在返工一批数据之后得出的教训。

注意:可见性为0的点,YOLO在计算关键点损失时会跳过,但坐标仍然要填。我一开始图省事,未标注的点直接不写,导致那一行的关键点数量对不上,训练直接报维度错误。正确做法是每个关键点位置都要占位,坐标填0、0,可见性填0。

2.3 类别索引与多目标场景的处理

labelme的JSON里,一张图可能有多个目标,每个目标的点混在同一个shapes数组里。YOLO格式要求每个目标单独一行,所以脚本必须能把点按目标分组。labelme本身没有目标分组的概念,我的做法是在label命名里再加一层前缀来标识目标,比如person1_nose、person1_left_eye、person2_nose。脚本按前缀分组,每组生成一行。

这个设计对单目标场景是冗余的,但对多目标场景是必需的。我做的项目里大部分图是单人,但有一批是双人交互的,如果不分组,两个人的点会被混成一行,外接矩形也会把两个人框在一起,完全没法用。加前缀的代价是标注时多打几个字,但换来的是转换逻辑的清晰。如果你的项目确定只有单目标,可以省掉这层前缀,脚本里把整张图的点当成一个目标处理即可。

类别索引方面,我的项目只有一类,就是人,索引填0。如果有多类,需要在配置里维护一个类别名到索引的映射,脚本按前缀里的类别名去查。这里要注意,YOLO的类别索引是从0开始的连续整数,不能跳号,否则训练时会报类别数不匹配。

3. 实操过程与核心环节实现

3.1 环境准备与目录结构约定

脚本运行环境很简单,Python 3.8以上,只依赖标准库的json、os、glob,不需要装额外的包。这一点是我刻意保持的,转换脚本越轻越好,不引入依赖就少一层环境问题。目录结构上,我约定labelme的JSON文件全部放在raw_json目录下,每张图对应一个同名JSON。脚本输出到dataset目录,下面分images和labels两个子目录,图片从JSON同目录复制过去,标注写成同名TXT。

这里有个细节:labelme保存的JSON里,图片路径字段可能是绝对路径也可能是相对路径,取决于标注时的打开方式。脚本不要依赖这个字段去读图,而是直接用JSON文件名去掉后缀,去raw_json同级或指定的图片目录里找同名图片。我一开始用JSON里的imagePath字段拼路径,结果换了一台机器路径全失效,后来改成按文件名匹配就稳了。

3.2 转换脚本的核心逻辑拆解

脚本主体分四步。第一步,遍历JSON目录,逐个加载。第二步,解析shapes,按目标前缀分组,每组内按label名字查关键点索引,填坐标和可见性。第三步,计算每组的外接矩形,加余量,归一化。第四步,拼成YOLO行格式写入TXT,同时复制图片。

关键代码逻辑我用伪代码说明,方便你对照自己的实现。解析部分的核心是维护一个keypoints数组,长度等于关键点总数,初始全填[0, 0, 0],然后遍历shapes,对每个point类型的shape,解析出目标前缀和关键点名,查索引,把归一化坐标和可见性填到对应位置。这里归一化需要imageWidth和imageHeight,从JSON顶层字段读。

外接矩形计算时,只统计可见性大于0的点,因为未标注的点坐标是0、0,如果参与计算会把框拉到图像左上角。这是个很容易忽略的点,我第一版就是所有点都参与,结果有未标注点的样本框全部偏到左上角,训练时模型完全学不到东西。改成只统计可见点之后,框的位置就正常了。

写TXT时,每行格式是类别 框中心x 框中心y 框宽 框高 点1x 点1y 点1v 点2x 点2y 点2v ...,所有数值保留六位小数。保留六位是精度和文件大小的平衡,太少会损失定位精度,太多文件会变大且没必要。

3.3 参数配置与验证方法

脚本里我抽出了几个可配置参数,集中放在文件头部。关键点顺序列表、类别映射、框余量比例、最小框尺寸比例、可见性后缀映射,这五个是必须按项目改的。改完之后,先拿三五张图跑一遍,人工检查输出的TXT。检查方法是把TXT里的归一化坐标乘回图像宽高,看是否落在关键点的实际位置上。我一般会挑一张关键点齐全的、一张有遮挡的、一张多目标的,三种情况都对了,再跑全量。

全量跑完之后,还要做一次统计校验。统计每个关键点被标注的次数,如果某个关键点的标注次数明显低于其他点,说明映射表里可能有名字对不上的情况。我第一次跑全量就是靠这个统计发现right_ear的标注次数是0,回头查才发现labelme里写的是rightEar。这个校验步骤花不了几分钟,但能省掉训练跑一半才发现数据有问题的返工。

4. 常见问题与排查技巧实录

4.1 转换后训练报维度错误的排查路径

维度错误是转换后最常见的问题,表现是训练启动时报关键点数量不匹配。排查顺序我总结成三步。第一步,看TXT每行的字段数是否一致,用脚本统计每行的空格分隔字段数,正常应该是5加3倍关键点数。如果某行字段数不对,说明那个目标的关键点没填满,多半是某个点的label名字没在映射表里查到,被跳过了。第二步,看关键点总数配置和映射表长度是否一致,这两个数必须相等。第三步,看类别索引是否从0开始连续,跳号会导致类别数对不上。

4.2 关键点位置整体偏移的定位方法

如果训练能跑但关键点位置整体偏移,问题通常出在归一化基准上。最常见的是用了数据集统一的尺寸去归一化,而不是每张图自己的宽高。排查方法是挑一张图,把TXT里的坐标乘回该图的实际宽高,看是否和标注位置吻合。如果不吻合,检查脚本里读imageWidth和imageHeight的地方,确认读的是当前JSON的字段而不是全局变量。还有一种可能是坐标顺序写反了,x和y对调,这种偏移表现为关键点整体沿对角线翻转,比较容易识别。

4.3 常见问题速查表

问题现象可能原因排查与解决
训练报关键点维度错误某行字段数不足统计每行字段数,检查映射表名字是否与label完全一致
关键点整体偏移归一化用了统一尺寸改为逐图读取imageWidth和imageHeight
框偏到图像左上角未标注点参与了外接矩形计算只统计可见性大于0的点
多目标混成一行缺少目标前缀分组在label命名加目标前缀,脚本按前缀分组
遮挡点被当成可见点可见性未从标注带出在label加后缀约定可见性,脚本按后缀映射
坐标等于1越界像素取整导致归一化后截断到0.0001至0.9999

4.4 独家避坑心得

第一个心得是关于标注规范的。转换脚本写得再好,也救不了标注不规范的数据。我在项目开始前就定了一份标注约定文档,明确关键点名字、后缀含义、目标前缀格式,所有参与标注的人按同一份文档来。这份文档看起来是额外工作,但它让转换脚本的映射表一次写对,后面零返工。

第二个心得是关于增量转换的。项目进行中会不断有新标注进来,我的脚本设计成可以重复跑,每次全量覆盖输出。这样不用担心增量同步的问题,反正转换很快,几百张图几秒钟的事。但要注意,覆盖前先备份上一次的输出,万一新标注有问题,还能回退。

第三个心得是关于小样本验证的。每次改完脚本,不要直接跑全量,先挑三类样本跑:关键点齐全的、有遮挡的、多目标的。这三类过了,基本就没大问题了。我吃过直接跑全量、训练跑了两天才发现遮挡点全错的亏,小样本验证花五分钟,能省两天。

最后再分享一个脚本里的小设计:输出TXT的同时,生成一个转换日志,记录每张图的关键点数量、可见点数量、是否有多目标。这个日志在排查问题时特别有用,比如某个关键点标注次数异常,翻日志就能定位到具体是哪几张图出的问题,不用再去翻原始JSON。这个日志我一开始没加,后来排查问题排烦了才补上,补上之后排查效率提升明显。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询