
做關鍵點檢測項目的人大多繞不開一個尷尬的環(huán)節(jié)標注工具和訓練框架各說各話。我最近在整理一套自用的姿態(tài)關鍵點數(shù)據流程標注端一直用 labelme因為它畫點、拖框、改標簽都順手團隊里非技術背景的同事也能快速上手但訓練端用的是 YOLO 系列的關鍵點檢測模型它只認自己那套 txt 標注格式。于是每次數(shù)據更新都要在 labelme 的 JSON 和 YOLO 的 txt 之間來回倒騰一次。這個“l(fā)abelme2yolo 格式改變”的活兒看著不起眼真做起來坑不少坐標歸一化算錯、關鍵點順序對不上、可見性標志丟失、圖像尺寸讀錯任何一個環(huán)節(jié)出問題訓練時 loss 就是降不下去你還以為是模型結構的問題排查半天才發(fā)現(xiàn)是標注轉換錯了。這篇內容就是把我自己這套轉換流程完整拆開講清楚。它解決的核心問題很具體把 labelme 標注的關鍵點 JSON 文件穩(wěn)定、可復現(xiàn)地轉換成 YOLO 關鍵點檢測所需的 txt 格式同時保留關鍵點的順序語義和可見性信息。適合正在做單人姿態(tài)、手部關鍵點、工業(yè)部件定位點這類項目的同學參考無論你是剛接觸關鍵點檢測的新手還是已經跑通過一遍但想搞清楚細節(jié)的老手都能從里面找到能直接抄作業(yè)的步驟和踩坑經驗。下面我按設計思路、核心細節(jié)、實操實現(xiàn)、問題排查四個部分展開中間會穿插我自己實測下來的參數(shù)選擇和避坑技巧。1. 整體設計思路與格式差異拆解動手寫轉換腳本之前得先把兩邊的數(shù)據格式徹底搞明白。很多人一上來就搜現(xiàn)成腳本復制過來跑不通然后開始瞎改根本原因是對兩種格式的字段含義理解不到位。我習慣先把格式差異列成一張表對著表寫代碼邏輯會清晰很多。1.1 labelme 關鍵點標注的 JSON 結構labelme 保存的每個 JSON 文件對應一張圖里面最關鍵的幾個字段是imagePath、imageWidth、imageHeight、imageData可選和shapes。關鍵點標注在shapes里每個 shape 有一個label、一個shape_type關鍵點通常是point、以及points數(shù)組。對于點來說points里就一個坐標[x, y]單位是像素。這里有個容易被忽略的點labelme 的關鍵點本身不帶“可見性”字段。它只有坐標沒有“這個點被遮擋了”或者“這個點在圖外”的標記。而 YOLO 關鍵點格式是要求可見性標志的0 表示未標注、1 表示標注但不可見、2 表示可見。所以轉換時我們必須自己定一套規(guī)則來生成可見性常見做法是只要 labelme 里畫了這個點就認為可見性為 2如果某個關鍵點缺失就填 0 并把坐標補 0。這套規(guī)則必須在整個數(shù)據集上統(tǒng)一否則模型學到的可見性語義會亂。另外labelme 的shapes里可能同時存在檢測框rectangle和關鍵點point。YOLO 關鍵點格式要求每行先有一個目標框再跟若干關鍵點。所以轉換時要先把框和點配對起來。我的做法是約定每個目標一個框框的 label 和點的 label 用命名前綴關聯(lián)比如框叫person點叫person_nose、person_left_eye這樣靠前綴把點歸到對應的框下面。這個約定在標注階段就要跟標注人員講清楚不然后期配對邏輯會寫得很痛苦。1.2 YOLO 關鍵點 txt 格式的字段含義YOLO 關鍵點檢測的標注是一張圖一個 txt每行描述一個目標。一行的結構是類別索引 cx cy w h px1 py1 v1 px2 py2 v2 ...。其中cx cy w h是目標框的中心點和寬高全部按圖像寬高歸一化到 0 到 1后面每個關鍵點是三個值歸一化后的 x、歸一化后的 y、可見性 v。歸一化這一步是錯誤高發(fā)區(qū)。歸一化的分母是圖像寬度和高度不是框的寬高。我見過有人把關鍵點坐標減去框左上角再除以框寬高那是另一種相對框的表示法YOLO 關鍵點格式不認這個。正確做法就是px x / imageWidth、py y / imageHeight簡單直接但一定要確認imageWidth和imageHeight讀的是 JSON 里的字段而不是 PIL 重新打開圖片讀的尺寸兩者理論上一致但圖片被替換過而 JSON 沒更新時就會不一致這種臟數(shù)據要提前過濾。還有一個細節(jié)關鍵點的順序必須固定。YOLO 訓練時是按索引位置理解每個點的語義的第 0 個點永遠代表鼻子第 1 個點永遠代表左眼以此類推。所以轉換腳本里要維護一個全局的關鍵點名稱到索引的映射表所有樣本都按這張表輸出缺失的點補 0。這張表一旦定下來就不能隨便改改了就得重新訓練否則模型會把語義搞混。1.3 為什么選擇腳本轉換而不是手工處理有人會問數(shù)據量不大的話能不能手工改我的經驗是絕對不要。關鍵點數(shù)據動輒幾千個點手工改不僅慢而且歸一化的小數(shù)位、可見性標志這些細節(jié)極容易出錯出錯后還很難定位。寫一個一次性投入的腳本后面數(shù)據更新直接重跑才是可持續(xù)的做法。腳本轉換的另一個好處是可審計。轉換過程中可以加校驗檢查每個框是否有點、檢查坐標是否越界、檢查關鍵點數(shù)量是否和映射表一致。這些校驗在手工處理時幾乎不可能系統(tǒng)化地做。我在腳本里加了一個統(tǒng)計輸出每次轉換完打印總樣本數(shù)、缺失關鍵點的樣本數(shù)、坐標越界的樣本數(shù)一眼就能看出數(shù)據質量。這個習慣幫我提前發(fā)現(xiàn)過好幾次標注錯誤比如標注人員把點畫到了圖像外面。2. 核心細節(jié)解析與實操要點格式搞清楚了接下來是轉換過程中真正決定成敗的細節(jié)。這一部分我按“讀什么、配什么、算什么、寫什么”的順序講每個環(huán)節(jié)都附上我實際踩過的坑。2.1 圖像尺寸的讀取與校驗歸一化依賴圖像寬高所以第一步是把寬高拿準。labelme 的 JSON 里有imageWidth和imageHeight字段直接讀這兩個值是最穩(wěn)的因為它就是標注時用的尺寸。但有個前提JSON 和圖片必須是一一對應的且圖片沒有被替換過。我遇到過一種情況同事重新導出了一批圖尺寸變了但 JSON 還是舊的結果歸一化全錯。所以腳本里我加了一步校驗用 PIL 打開imagePath指向的圖片讀實際尺寸和 JSON 里的字段比對不一致就跳過并記錄到日志。這一步多花幾秒鐘能省掉后面幾小時的排查。如果圖片路徑在 JSON 里是相對路徑要注意腳本運行時的當前工作目錄。我的做法是統(tǒng)一以 JSON 文件所在目錄為基準拼接圖片路徑這樣不管從哪個目錄運行腳本都不會找不到圖。拼接后如果文件不存在也記錄到日志不中斷整個轉換流程因為批量數(shù)據里偶爾有一兩張圖丟失是常事不能因為一張圖讓整個任務失敗。2.2 框與關鍵點的配對邏輯配對是轉換里最需要設計的一環(huán)。前面說了用命名前綴關聯(lián)具體實現(xiàn)是先遍歷shapes把shape_type為rectangle的收集成框列表把shape_type為point的收集成點列表。然后對每個框按它的label作為前綴去點列表里找 label 以“前綴 分隔符”開頭的點。分隔符我習慣用下劃線比如框person對應點person_nose。這里有個邊界情況要處理一張圖里可能有多個同類目標比如兩個人。這時候兩個框的 label 都是person靠前綴就分不清哪個點屬于哪個人了。解決辦法是在標注階段就給每個目標加編號比如person_1、person_2點叫person_1_nose、person_2_nose。這個約定必須在標注規(guī)范里寫死轉換腳本按編號分組即可。如果標注階段沒做這個約定后期補救很麻煩只能靠點的空間位置去猜它屬于哪個框用點在框內的規(guī)則做歸屬但點落在框外或者兩框重疊時就不可靠了。所以我的建議是多目標場景標注時就把編號加上別偷懶。配對完成后還要檢查每個框是否配到了預期數(shù)量的關鍵點。如果某個框只配到一半的點說明標注漏了這時候可以選擇跳過該樣本或者補 0。我傾向于跳過并記錄因為關鍵點缺失太多的樣本對訓練是負貢獻。跳過閾值可以設成“缺失超過 30% 就跳過”這個比例可以根據項目容忍度調整。2.3 歸一化計算與數(shù)值精度歸一化的公式前面說了x / width、y / height但數(shù)值精度有講究。YOLO 的 txt 一般保留 6 位小數(shù)就夠了保留太多位沒必要還增大文件體積。我用的是格式化字符串%.6f實測下來訓練完全沒問題。要注意的是坐標可能因為標注誤差略微超出圖像邊界比如 x 等于 width 加 1歸一化后大于 1。這種值要不要截斷到 0 到 1 之間我的做法是截斷因為超出邊界的點對模型沒有意義截斷后至少不會引入異常梯度。截斷的同時記錄一條日志方便回頭檢查標注質量。框的歸一化同理cx (x1 x2) / 2 / width、w (x2 - x1) / width。這里要確保 x1 小于 x2labelme 畫框時如果從右下往左上拖存下來的點順序可能是反的所以計算前先對兩個角點做 min/max 排序。這個小細節(jié)不處理算出來的寬高會是負數(shù)訓練直接報錯。2.4 關鍵點順序映射表的維護映射表是整個轉換的“憲法”。我把它寫成一個 Python 字典鍵是關鍵點名稱后綴比如nose、left_eye值是對應的索引。所有樣本輸出時按索引 0 到 N-1 的順序依次取點取不到就填0 0 0。這張表要和訓練時的配置保持一致YOLO 的模型配置里有個kpt_shape參數(shù)比如[17, 3]表示 17 個關鍵點、每個點 3 個值這個 17 必須和映射表的長度一致。映射表最好單獨存成一個配置文件或者常量模塊轉換腳本和訓練配置都引用它避免兩處各寫一份導致不一致。我吃過這個虧轉換腳本里加了第 18 個點訓練配置忘了改結果訓練時維度對不上報錯信息還不直觀查了半天?,F(xiàn)在我的做法是映射表只在一個地方定義其他地方 import 引用改一處全生效。3. 實操過程與核心環(huán)節(jié)實現(xiàn)這一部分給出可以直接跑的完整流程。我用 Python 實現(xiàn)依賴只有標準庫加 Pillow不需要裝額外的深度學習框架輕量好部署。下面按步驟走關鍵代碼貼出來并解釋。3.1 環(huán)境準備與目錄結構約定先約定目錄結構這能讓腳本邏輯簡單很多。我習慣這樣組織dataset/ images/ train/ val/ labels_json/ train/ val/ labels_yolo/ train/ val/labels_json放 labelme 導出的 JSONlabels_yolo是腳本輸出的 txt目錄結構鏡像對應。這樣訓練時直接指向labels_yolo即可。環(huán)境上只需要 Python 3.8 以上和 Pillow安裝命令就一行pip install pillow不需要裝 labelme 本身因為腳本只讀 JSON不依賴 labelme 的運行時。這一點對部署到沒有圖形界面的服務器上很有用。3.2 關鍵點映射表與配置定義先定義映射表和類別表。假設做的是單人 5 關鍵點頭、左手、右手、左腳、右腳的簡化姿態(tài)任務映射表這樣寫# config.py KEYPOINT_NAMES [head, left_hand, right_hand, left_foot, right_foot] KEYPOINT_INDEX {name: i for i, name in enumerate(KEYPOINT_NAMES)} CLASS_NAMES [person] CLASS_INDEX {name: i for i, name in enumerate(CLASS_NAMES)} SEPARATOR _KEYPOINT_INDEX用推導式生成保證索引和名稱順序嚴格對應。類別同理。分隔符單獨定義方便以后改。這個文件被轉換腳本 import訓練配置里也引用len(KEYPOINT_NAMES)作為kpt_shape的第一個值做到單一數(shù)據源。3.3 單個 JSON 到 txt 的轉換函數(shù)核心轉換函數(shù)按前面講的邏輯實現(xiàn)我把它拆成幾個小函數(shù)便于測試。先寫讀取和校驗import json import os from PIL import Image def load_and_validate(json_path): with open(json_path, r, encodingutf-8) as f: data json.load(f) img_path os.path.join(os.path.dirname(json_path), data[imagePath]) if not os.path.exists(img_path): return None, image_missing with Image.open(img_path) as im: real_w, real_h im.size if real_w ! data[imageWidth] or real_h ! data[imageHeight]: return None, size_mismatch return data, None這個函數(shù)返回數(shù)據和錯誤碼調用方根據錯誤碼決定跳過還是繼續(xù)。size_mismatch和image_missing都記錄到日志不拋異常保證批量處理不中斷。接著寫配對和轉換def convert_one(data): w, h data[imageWidth], data[imageHeight] rects, points [], [] for shape in data[shapes]: if shape[shape_type] rectangle: rects.append(shape) elif shape[shape_type] point: points.append(shape) lines [] for rect in rects: prefix rect[label] (x1, y1), (x2, y2) rect[points] x1, x2 min(x1, x2), max(x1, x2) y1, y2 min(y1, y2), max(y1, y2) cx (x1 x2) / 2 / w cy (y1 y2) / 2 / h bw (x2 - x1) / w bh (y2 - y1) / h kpt_map {} for p in points: label p[label] if label.startswith(prefix SEPARATOR): suffix label[len(prefix) len(SEPARATOR):] if suffix in KEYPOINT_INDEX: kpt_map[KEYPOINT_INDEX[suffix]] p[points][0] cls_id CLASS_INDEX.get(prefix.split(SEPARATOR)[0], 0) parts [str(cls_id), f{cx:.6f}, f{cy:.6f}, f{bw:.6f}, f{bh:.6f}] for i in range(len(KEYPOINT_NAMES)): if i in kpt_map: px, py kpt_map[i] px min(max(px / w, 0.0), 1.0) py min(max(py / h, 0.0), 1.0) parts.extend([f{px:.6f}, f{py:.6f}, 2]) else: parts.extend([0.000000, 0.000000, 0]) lines.append( .join(parts)) return lines這段代碼里幾個關鍵點值得說明。prefix.split(SEPARATOR)[0]是為了處理帶編號的框 label比如person_1取person去查類別索引。關鍵點坐標做了截斷可見性畫了就填 2、沒畫填 0。缺失點補三個零值保證每行長度一致。這些處理都是前面設計思路的落地。3.4 批量處理與統(tǒng)計輸出批量處理就是遍歷目錄、調用轉換、寫文件、統(tǒng)計。統(tǒng)計部分我單獨寫因為它是數(shù)據質量的晴雨表def batch_convert(json_dir, out_dir): os.makedirs(out_dir, exist_okTrue) stats {total: 0, ok: 0, image_missing: 0, size_mismatch: 0, no_rect: 0} for name in os.listdir(json_dir): if not name.endswith(.json): continue stats[total] 1 data, err load_and_validate(os.path.join(json_dir, name)) if err: stats[err] 1 continue lines convert_one(data) if not lines: stats[no_rect] 1 continue out_path os.path.join(out_dir, name.replace(.json, .txt)) with open(out_path, w, encodingutf-8) as f: f.write(\n.join(lines)) stats[ok] 1 print(stats) return stats跑完打印的統(tǒng)計里ok是成功數(shù)其他幾個是各類異常數(shù)。我一般要求ok占比 95% 以上才繼續(xù)訓練低于這個值先回去查標注。no_rect表示圖里只有點沒有框這種樣本對關鍵點檢測沒用直接跳過是對的。3.5 轉換結果的抽樣驗證腳本跑完不能直接拿去訓練要抽樣驗證。我的驗證方法很土但有效隨機抽 5 個 txt把里面的歸一化坐標乘回圖像寬高用 Pillow 在原圖上畫點和框肉眼看位置對不對。這一步能發(fā)現(xiàn)歸一化方向搞反、坐標軸搞混這類低級但致命的錯誤。驗證代碼不復雜畫完存成圖片對比即可。我還會檢查 txt 的行數(shù)是否等于該圖的目標數(shù)以及每行的字段數(shù)是否等于5 3 * 關鍵點數(shù)字段數(shù)不對說明映射表或補零邏輯有問題。4. 常見問題與排查技巧實錄轉換腳本寫起來不難難的是數(shù)據出問題時快速定位。這一部分整理我實際遇到過的典型問題和排查路徑做成速查表遇到問題先對表。4.1 訓練 loss 不降反升的排查順序這是最讓人抓狂的癥狀。我的排查順序是先看標注轉換再看數(shù)據劃分最后才懷疑模型。標注轉換方面重點查三件事關鍵點順序是否和kpt_shape一致、可見性標志是否合理、歸一化是否用了正確的寬高。我遇到過一次 loss 震蕩最后發(fā)現(xiàn)是映射表里兩個點的索引寫反了模型學到的語義是亂的改回來立刻正常??梢娦苑矫嫒绻悬c都填 2模型學不到遮擋信息遇到遮擋樣本表現(xiàn)會差但一般不會導致 loss 不降所以優(yōu)先級放后面。數(shù)據劃分方面檢查訓練集和驗證集是否有重復圖片或者驗證集里全是難樣本。這個用文件名的哈希去重就能查。模型方面反而最后查因為關鍵點檢測的模型結構相對成熟配置對了很少出問題。4.2 坐標越界與負寬高的處理坐標越界前面提過截斷加日志。負寬高是框角點順序問題min/max 排序解決。還有一種情況是框的寬或高歸一化后等于 0說明標注時框畫成了一條線這種樣本要跳過因為寬高為 0 的框在訓練時會引發(fā)除零或者無效 anchor。我在轉換函數(shù)里加了一個判斷bw或bh小于一個極小值比如 1e-6就跳過該目標并記錄。4.3 多目標場景的歸屬混亂前面講過用編號解決這里補充一個排查技巧如果懷疑點歸屬錯了把每個框和它配到的點用不同顏色畫出來一眼就能看出哪個點跑到了別的框下面。歸屬錯誤的典型表現(xiàn)是某個框的關鍵點數(shù)量異常多另一個異常少。統(tǒng)計每個框配到的點數(shù)分布偏離預期值的就是可疑樣本。4.4 常見問題速查表問題現(xiàn)象可能原因排查方法解決方式訓練報維度錯誤映射表長度與 kpt_shape 不一致打印兩者長度對比統(tǒng)一引用同一配置loss 不下降關鍵點索引錯位抽樣畫點核對語義修正映射表歸一化后坐標大于 1標注點超出圖像邊界檢查原始坐標截斷并記錄日志寬高為負框角點順序反了打印原始角點min/max 排序找不到圖片相對路徑基準錯打印拼接后路徑以 JSON 目錄為基準尺寸不一致圖片被替換比對 JSON 與 PIL 尺寸跳過并記錄某框點數(shù)異常多目標歸屬錯按顏色畫點核對標注加編號這張表我貼在項目文檔里新人接手先看表能解決八成問題。4.5 我踩過的兩個印象深刻的坑第一個坑是編碼問題。labelme 的 JSON 里如果有中文標簽用默認編碼讀在某些環(huán)境下會報錯統(tǒng)一用utf-8讀寫就沒事。第二個坑是浮點數(shù)格式化早期我用str(round(x, 6))結果0.5會輸出成0.5而不是0.500000雖然數(shù)值一樣但某些嚴格的解析器對字段格式敏感后來統(tǒng)一用%.6f格式化字段寬度一致解析更穩(wěn)。這兩個坑都不大但排查起來費時間寫在這里給后來人省點事。4.6 轉換流程的復用與擴展建議這套流程我后來擴展到了手部關鍵點項目只改了映射表和關鍵點數(shù)量其他邏輯原樣復用說明設計上把可變部分收斂到配置里是對的。如果以后要支持左右手區(qū)分可以在映射表里加前綴或者拆成兩個類別。再進一步如果標注端換成別的工具只要寫一個適配層把它的輸出轉成 labelme 的 JSON 結構后面的轉換邏輯完全不用動。這種分層思路是我做數(shù)據管線時一直堅持的格式適配層和業(yè)務轉換層分開換工具不換邏輯。最后分享一個我個人的習慣每次轉換完把統(tǒng)計結果和抽樣驗證圖存到一個帶日期的文件夾里數(shù)據更新時對比前后統(tǒng)計能快速發(fā)現(xiàn)新數(shù)據引入的問題。這個習慣看起來繁瑣但在數(shù)據量上來之后是保證訓練穩(wěn)定的重要一環(huán)。關鍵點檢測的成敗很多時候不在模型而在這些看起來枯燥的數(shù)據細節(jié)里。