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