ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

labelme转YOLO关键点格式:避坑指南与脚本实现

labelme转YOLO关键点格式:避坑指南与脚本实现 关键点检测这个方向做的人不少但真正踩过数据格式转换坑的人才知道从标注工具到训练框架之间那条“最后一公里”有多磨人。我最近在做一个自用的姿态估计小项目标注环节用的是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_widthy_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.99994.4 独家避坑心得第一个心得是关于标注规范的。转换脚本写得再好也救不了标注不规范的数据。我在项目开始前就定了一份标注约定文档明确关键点名字、后缀含义、目标前缀格式所有参与标注的人按同一份文档来。这份文档看起来是额外工作但它让转换脚本的映射表一次写对后面零返工。第二个心得是关于增量转换的。项目进行中会不断有新标注进来我的脚本设计成可以重复跑每次全量覆盖输出。这样不用担心增量同步的问题反正转换很快几百张图几秒钟的事。但要注意覆盖前先备份上一次的输出万一新标注有问题还能回退。第三个心得是关于小样本验证的。每次改完脚本不要直接跑全量先挑三类样本跑关键点齐全的、有遮挡的、多目标的。这三类过了基本就没大问题了。我吃过直接跑全量、训练跑了两天才发现遮挡点全错的亏小样本验证花五分钟能省两天。最后再分享一个脚本里的小设计输出TXT的同时生成一个转换日志记录每张图的关键点数量、可见点数量、是否有多目标。这个日志在排查问题时特别有用比如某个关键点标注次数异常翻日志就能定位到具体是哪几张图出的问题不用再去翻原始JSON。这个日志我一开始没加后来排查问题排烦了才补上补上之后排查效率提升明显。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表