鍵點(diǎn)索引映射:COCO 18、BODY_25與官方COCO 17全面對(duì)比)
在姿態(tài)估計(jì)這個(gè)方向混久了基本都會(huì)在同一個(gè)地方栽一次跟頭OpenPose跑通了結(jié)果一查關(guān)鍵點(diǎn)坐標(biāo)完全不知道第8個(gè)點(diǎn)到底是右胯還是左胯。更常見的是你把OpenPose的結(jié)果拿去訓(xùn)練YOLOv8-Pose或者和別人對(duì)接標(biāo)注數(shù)據(jù)兩邊對(duì)不上——明明都是COCO順序怎么就不一樣呢。這個(gè)問(wèn)題說(shuō)大不大說(shuō)小不小但一旦數(shù)據(jù)量大起來(lái)索引錯(cuò)一位整個(gè)訓(xùn)練集就廢了。OpenPose官方提供了兩套常用的關(guān)鍵點(diǎn)定義COCO 18點(diǎn)和BODY_25點(diǎn)。我先直接說(shuō)結(jié)論OpenPose的COCO 18點(diǎn)并不等于官方COCO數(shù)據(jù)集的17點(diǎn)BODY_25也不是簡(jiǎn)單地在18點(diǎn)后面加6個(gè)腳部點(diǎn)其中間還插了一個(gè)MidHip。這兩套模型的關(guān)鍵點(diǎn)排列順序、左右手/腳的先后習(xí)慣甚至和原始COCO標(biāo)注的定義都不一樣。這篇文章就把這些對(duì)應(yīng)關(guān)系徹底理清楚并給出可以直接抄走的索引映射代碼。1. 為什么OpenPose的COCO模型和官方COCO數(shù)據(jù)集總對(duì)不上1.1 三種COCO點(diǎn)集來(lái)源完全不同很多人第一次接觸這個(gè)問(wèn)題時(shí)最懵的就是我用的OpenPose模型文件明明叫pose_deploy_linevec.prototxt里面注釋寫的是COCO模型輸出也是18個(gè)通道為什么和網(wǎng)上COCO數(shù)據(jù)集的17個(gè)關(guān)鍵點(diǎn)對(duì)不上原因在于這里其實(shí)存在三種不同的關(guān)鍵點(diǎn)定義官方COCO數(shù)據(jù)集關(guān)鍵點(diǎn)嚴(yán)格來(lái)說(shuō)是17個(gè)點(diǎn)分別是鼻子、左右眼、左右耳、左右肩、左右肘、左右腕、左右胯、左右膝、左右踝沒(méi)有脖子沒(méi)有骨盆中心。OpenPose的COCO 18點(diǎn)模型在官方17點(diǎn)基礎(chǔ)上額外加了一個(gè)Neck脖子/頸部中心插入到Nose之后變成18點(diǎn)。它的左右排列習(xí)慣也沿用了OpenPose一貫的風(fēng)格先右后左。OpenPose的BODY_25模型在COCO 18點(diǎn)基礎(chǔ)上又在索引8的位置插入了MidHip骨盆中心并且在最后追加了6個(gè)腳部點(diǎn)最終形成25個(gè)點(diǎn)。稍等這里有個(gè)非常反直覺的地方BODY_25是25個(gè)點(diǎn)但它不是簡(jiǎn)單地把COCO 18點(diǎn)的索引整體往后挪而是在中間插隊(duì)把原來(lái)的RHip擠到了9號(hào)位。這就是很多轉(zhuǎn)換代碼出錯(cuò)的根本原因——你以為25點(diǎn)取前18個(gè)就是COCO 18實(shí)際上取出來(lái)的是Nose, Neck, RShoulder...到第8個(gè)MidHip那里就全部錯(cuò)位了。1.2 Neck和MidHip兩個(gè)官方數(shù)據(jù)集里沒(méi)有的點(diǎn)在官方COCO的標(biāo)注文件里你是找不到Neck這個(gè)點(diǎn)的。為什么因?yàn)镃OCO數(shù)據(jù)集的標(biāo)注邏輯是每個(gè)人實(shí)例標(biāo)注17個(gè)關(guān)鍵點(diǎn)缺少的點(diǎn)用visibility0表示。脖子這個(gè)位置在很多視角下會(huì)被遮擋標(biāo)注難度大所以官方干脆沒(méi)有定義。OpenPose為了后續(xù)做姿態(tài)分析比如計(jì)算軀干角度、判斷身體朝向必須有軀干中心于是強(qiáng)行把Neck加了進(jìn)來(lái)。它不是一個(gè)解剖學(xué)意義上的脖子更準(zhǔn)確地說(shuō)是軀干頂部的中心點(diǎn)算法根據(jù)肩部和骨盆的位置擬合出來(lái)的。MidHip的定位則更貼近骨盆中心在BODY_25的索引里被放在8號(hào)位處于RHip之前。這個(gè)點(diǎn)對(duì)于衡量髖部旋轉(zhuǎn)、區(qū)分正面背面、判斷走路姿態(tài)都非常有用因此OpenPose寧可破壞與COCO 18的索引連續(xù)性也要把它塞進(jìn)中間。1.3 索引錯(cuò)位會(huì)引發(fā)什么連鎖問(wèn)題如果你沒(méi)有意識(shí)到上面這套排列邏輯直接在代碼里寫kpts_25[:18]當(dāng)成18點(diǎn)數(shù)據(jù)用那么從索引8開始你拿到的點(diǎn)就全是錯(cuò)的kpts_25[8]其實(shí)是MidHip不是RHipkpts_25[9]才是RHip但你會(huì)把它當(dāng)成RKnee依次往后所有下半身的點(diǎn)都會(huì)錯(cuò)位。更隱蔽的是如果你反過(guò)來(lái)用把18點(diǎn)數(shù)據(jù)映射到25點(diǎn)只給前18個(gè)位置賦值那么RHip、RKnee、RAnkle、LHip、LKnee、LAnkle這些關(guān)鍵點(diǎn)就會(huì)被寫進(jìn)MidHip、RHip、RKnee、RAnkle的位置直接就全亂了。索引錯(cuò)位不會(huì)報(bào)錯(cuò)程序照樣跑但畫出來(lái)的骨架是扭曲的算出來(lái)的角度全部沒(méi)有意義。這種錯(cuò)誤在調(diào)試階段極難發(fā)現(xiàn)因?yàn)槟愕谝粫r(shí)間不會(huì)去懷疑索引居然不是順序?qū)?yīng)的。2. COCO 18點(diǎn)索引明細(xì)從Nose到LEar逐點(diǎn)對(duì)照2.1 18點(diǎn)完整索引與名稱對(duì)照表OpenPose的COCO 18點(diǎn)模型輸出18個(gè)關(guān)鍵點(diǎn)熱圖實(shí)際forward出來(lái)可能是19通道多出來(lái)的一個(gè)是背景后面第6章再細(xì)說(shuō)。0到17的索引排列如下索引關(guān)鍵點(diǎn)名稱說(shuō)明0Nose鼻子1Neck頸部中心OpenPose自造2RShoulder右肩3RElbow右肘4RWrist右手腕5LShoulder左肩6LElbow左肘7LWrist左手腕8RHip右胯9RKnee右膝10RAnkle右踝11LHip左胯12LKnee左膝13LAnkle左踝14REye右眼15LEye左眼16REar右耳17LEar左耳注意看這個(gè)排列邏輯上半身骨架從脖子開始先畫右邊整條手臂再畫左邊整條手臂然后回到軀干先右胯再左胯順著大腿小腿下來(lái)最后是臉部的眼睛和耳朵。在整個(gè)序列中右Right和左Left是分組的而且右在前、左在后。這一點(diǎn)和很多人的直覺不一樣因?yàn)樽匀徽Z(yǔ)言里常說(shuō)左肩右肩但OpenPose內(nèi)部自始至終堅(jiān)持右先左后后面第5章還原官方COCO 17點(diǎn)時(shí)還會(huì)因?yàn)檫@個(gè)左右順序再踩一次坑。2.2 從Caffe模型輸出中讀取18個(gè)熱圖實(shí)際操作中用OpenCV的DNN模塊讀取這套模型很常見。核心代碼如下import cv2 import numpy as np protoFile pose/coco/pose_deploy_linevec.prototxt weightsFile pose/coco/pose_iter_440000.caffemodel net cv2.dnn.readNetFromCaffe(protoFile, weightsFile) inWidth, inHeight 368, 368 blob cv2.dnn.blobFromImage( img, 1.0 / 255, (inWidth, inHeight), (0, 0, 0), swapRBFalse, cropFalse ) net.setInput(blob) out net.forward() print(out.shape) # 常見輸出 (1, 19, 46, 46) 或 (1, 18, 46, 46)out[0, i]就是第i個(gè)關(guān)鍵點(diǎn)的置信度熱圖。如果shape[1]是19說(shuō)明前18個(gè)通道是關(guān)節(jié)點(diǎn)熱圖最后一個(gè)是背景通道如果shape[1]恰好是18說(shuō)明導(dǎo)出模型時(shí)已經(jīng)把背景通道去掉了。拿到熱圖后用minMaxLoc找響應(yīng)最大的位置再映射回原圖尺寸就是關(guān)鍵點(diǎn)坐標(biāo)nPoints 18 keypoints [] for i in range(nPoints): probMap out[0, i, :, :] _, conf, _, point cv2.minMaxLoc(probMap) x point[0] * frameWidth / out.shape[3] y point[1] * frameHeight / out.shape[2] keypoints.append((int(x), int(y), conf))這里frameWidth和frameHeight是原圖尺寸out.shape[3]和out.shape[2]是熱圖的寬高。這種縮放方式比簡(jiǎn)單除以系數(shù)更穩(wěn)妥因?yàn)椴煌斎氤叽缦聼釄D大小會(huì)變。2.3 COCO 18的骨架連線limbs拿到點(diǎn)坐標(biāo)后要畫骨架連線關(guān)系同樣有固定順序POSE_PAIRS_COCO18 [ (0, 1), (1, 2), (1, 5), (2, 3), (3, 4), (5, 6), (6, 7), (1, 8), (8, 9), (9, 10), (1, 11), (11, 12), (12, 13), (0, 14), (0, 15), (14, 16), (15, 17) ]這些連線的含義是Nose連NeckNeck分叉到兩肩然后沿手臂往下Neck連兩胯沿腿往下Nose連兩眼眼睛連耳朵。注意軀干部分沒(méi)有把RHip和LHip直接連起來(lái)因?yàn)檫@兩個(gè)點(diǎn)之間沒(méi)有PAFPart Affinity Fields部件親和場(chǎng)分支直接建模。如果你畫出來(lái)的骨架中間少了一根腰帶不是代碼錯(cuò)了是模型本來(lái)就沒(méi)有這條邊。3. BODY_25索引明細(xì)腳部點(diǎn)與MidHip排布規(guī)律3.1 BODY_25完整索引對(duì)照表BODY_25是OpenPose性能最好、應(yīng)用最廣的模型。它的25個(gè)關(guān)鍵點(diǎn)排布如下索引關(guān)鍵點(diǎn)名稱說(shuō)明0Nose鼻子1Neck頸部中心2RShoulder右肩3RElbow右肘4RWrist右手腕5LShoulder左肩6LElbow左肘7LWrist左手腕8MidHip骨盆中心9RHip右胯10RKnee右膝11RAnkle右踝12LHip左胯13LKnee左膝14LAnkle左踝15REye右眼16LEye左眼17REar右耳18LEar左耳19LBigToe左腳大腳趾20LSmallToe左腳小腳趾21LHeel左腳跟22RBigToe右腳大腳趾23RSmallToe右腳小腳趾24RHeel右腳跟有沒(méi)有發(fā)現(xiàn)一個(gè)奇怪的地方腳部這6個(gè)點(diǎn)是先左腳后右腳和身體部分先右后左的順序正好相反。這確實(shí)容易讓人搞混官方就是這樣設(shè)計(jì)的沒(méi)有太多道理可講只能死記19-21是左腳22-24是右腳。3.2 連接關(guān)系表含腳部BODY_25的骨架連線比COCO 18多了軀干中線和腳部連接POSE_PAIRS_BODY25 [ (0, 1), (1, 2), (1, 5), (2, 3), (3, 4), (5, 6), (6, 7), (1, 8), (8, 9), (9, 10), (10, 11), (1, 8), (8, 12), (12, 13), (13, 14), (0, 15), (0, 16), (15, 17), (16, 18), (11, 24), (11, 22), (22, 23), (14, 21), (14, 19), (19, 20) ]注意這里軀干部位Neck(1)和MidHip(8)相連MidHip(8)分叉到RHip(9)和LHip(12)這樣軀干中心就被明確建模了。腳部則是從腳踝出發(fā)連到腳跟和腳趾。我用這個(gè)連線畫出來(lái)的骨架比COCO 18模型要穩(wěn)不少尤其在跑動(dòng)、跳躍這類動(dòng)作里腳部信息能明顯減少下半身的抖動(dòng)。3.3 為什么我推薦直接用BODY_25作為中間格式在OpenPose框架內(nèi)部如果條件允許建議優(yōu)先用BODY_25原因有三個(gè)信息量更大多了骨盆中心和6個(gè)腳部點(diǎn)做動(dòng)作分析時(shí)能計(jì)算的關(guān)節(jié)角度更多尤其是髖關(guān)節(jié)和踝關(guān)節(jié)的角度。模型更穩(wěn)官方在BODY_25上的訓(xùn)練數(shù)據(jù)更充分實(shí)測(cè)對(duì)遮擋的魯棒性普遍好于COCO 18模型。轉(zhuǎn)換路徑更短BODY_25可以無(wú)損截取成COCO 18也可以映射成官方COCO 17。反過(guò)來(lái)從COCO 18想得到BODY_25缺失的MidHip和腳部點(diǎn)只能補(bǔ)零信息就丟了。如果你在自己的項(xiàng)目里同時(shí)涉及老代碼用COCO 18和新代碼用BODY_25最省心的做法是內(nèi)部統(tǒng)一存BODY_25格式只在需要對(duì)接外部接口時(shí)做映射。4. 18與25互轉(zhuǎn)核心映射關(guān)系與Python代碼4.1 18→25注意RHip之后所有索引都要1先給出最核心的映射關(guān)系。從COCO 18轉(zhuǎn)到BODY_25前8個(gè)點(diǎn)索引0-7直接對(duì)應(yīng)從第8個(gè)點(diǎn)RHip開始全部要往后挪一位coco18_to_body25 [ 0, # 0 Nose - 0 Nose 1, # 1 Neck - 1 Neck 2, # 2 RShoulder - 2 RShoulder 3, # 3 RElbow - 3 RElbow 4, # 4 RWrist - 4 RWrist 5, # 5 LShoulder - 5 LShoulder 6, # 6 LElbow - 6 LElbow 7, # 7 LWrist - 7 LWrist 9, # 8 RHip - 9 RHip 10, # 9 RKnee - 10 RKnee 11, # 10 RAnkle - 11 RAnkle 12, # 11 LHip - 12 LHip 13, # 12 LKnee - 13 LKnee 14, # 13 LAnkle - 14 LAnkle 15, # 14 REye - 15 REye 16, # 15 LEye - 16 LEye 17, # 16 REar - 17 REar 18, # 17 LEar - 18 LEar ]轉(zhuǎn)換成25點(diǎn)數(shù)組時(shí)索引8的MidHip、19-24的腳部點(diǎn)全部置零。如果只是臨時(shí)使用直接補(bǔ)(0, 0, 0)即可如果還要繼續(xù)做跟蹤或角度計(jì)算建議給這些點(diǎn)一個(gè)confidence0方便后續(xù)過(guò)濾def convert_18_to_25(kpts_18): # kpts_18: shape (18, 3) 或 (18, 2) kpts_25 np.zeros((25, kpts_18.shape[1] if kpts_18.ndim 1 else 1)) for src, dst in enumerate(coco18_to_body25): kpts_25[dst] kpts_18[src] return kpts_254.2 25→18刪掉MidHip后收攏反過(guò)來(lái)從BODY_25還原成COCO 18就是把第8位MidHip刪掉后面19-24的腳部點(diǎn)直接丟棄body25_to_coco18 [0, 1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18] def convert_25_to_18(kpts_25): return kpts_25[body25_to_coco18]這里的body25_to_coco18列表里的數(shù)值是BODY_25的索引按這個(gè)順序取出來(lái)自然就是COCO 18的排列。用NumPy的花式索引一步就能完成非常簡(jiǎn)潔。4.3 批量轉(zhuǎn)換與坐標(biāo)保持實(shí)際項(xiàng)目里不可能只轉(zhuǎn)一個(gè)人大概率是批量轉(zhuǎn)換。批量處理時(shí)先確認(rèn)數(shù)組形狀是(N, 25, 3)還是(N, 3, 25)我建議統(tǒng)一成(N, 25, C)后面不管做可視化還是訓(xùn)練集轉(zhuǎn)換都少很多麻煩def batch_convert_25_to_18(kpts_batch): # kpts_batch: (N, 25, C) return kpts_batch[:, body25_to_coco18, :]坐標(biāo)本身就是數(shù)值拷貝轉(zhuǎn)換過(guò)程不需要做任何插值或縮放x、y、confidence直接原樣搬運(yùn)。唯一需要注意的是如果25點(diǎn)里有MidHip的坐標(biāo)而你要算的角度恰好和骨盆中心相關(guān)刪掉它就等于丟了這部分信息后面代碼要按實(shí)際點(diǎn)數(shù)重新組織不要越界訪問(wèn)索引。5. 還原官方COCO 17點(diǎn)刪點(diǎn)之后還要左右交換5.1 官方COCO 17點(diǎn)定義與順序官方COCO數(shù)據(jù)集的關(guān)鍵點(diǎn)順序和OpenPose的兩套模型都有明顯差異。官方COCO的排列是索引關(guān)鍵點(diǎn)名稱0nose1left_eye2right_eye3left_ear4right_ear5left_shoulder6right_shoulder7left_elbow8right_elbow9left_wrist10right_wrist11left_hip12right_hip13left_knee14right_knee15left_ankle16right_ankle注意兩個(gè)關(guān)鍵差異左右順序反了官方COCO是先左后右left_eye, right_eye; left_shoulder, right_shoulderOpenPose是先右后左。所以從OpenPose映射到COCO時(shí)眼睛、耳朵、肩、肘、腕、胯、膝、踝全部要做左右互換。沒(méi)有Neck官方COCO沒(méi)有脖子點(diǎn)也沒(méi)有MidHip這兩個(gè)點(diǎn)必須刪除。如果你只是把BODY_25按索引順序挑出來(lái)不交換左右那么轉(zhuǎn)出來(lái)的COCO關(guān)鍵點(diǎn)標(biāo)簽就是左右顛倒的。這個(gè)問(wèn)題非常隱蔽因?yàn)楫嬙趫D上看起來(lái)還是一個(gè)人形只有當(dāng)你單獨(dú)看某個(gè)關(guān)鍵點(diǎn)比如left_eye時(shí)才會(huì)發(fā)現(xiàn)它其實(shí)落在右眼上。5.2 25→17映射核心邏輯正確做法是既刪點(diǎn)又交換左右body25_to_coco17 { 0: 0, # Nose - nose 15: 2, # REye - right_eye 16: 1, # LEye - left_eye 17: 4, # REar - right_ear 18: 3, # LEar - left_ear 2: 6, # RShoulder - right_shoulder 5: 5, # LShoulder - left_shoulder 3: 8, # RElbow - right_elbow 6: 7, # LElbow - left_elbow 4: 10, # RWrist - right_wrist 7: 9, # LWrist - left_wrist 9: 12, # RHip - right_hip 12: 11, # LHip - left_hip 10: 14, # RKnee - right_knee 13: 13, # LKnee - left_knee 11: 16, # RAnkle - right_ankle 14: 15, # LAnkle - left_ankle } def convert_25_to_coco17(kpts_25): kpts_17 np.zeros((17, kpts_25.shape[1])) for src, dst in body25_to_coco17.items(): kpts_17[dst] kpts_25[src] return kpts_17這個(gè)字典的鍵是BODY_25索引值是COCO 17索引??辞宄讉€(gè)典型映射BODY_25的2 RShoulder映射到COCO的6 right_shoulder而不是5 left_shoulderBODY_25的15 REye映射到COCO的2 right_eye而不是1 left_eye。這種細(xì)節(jié)必須靠注釋寫清楚否則轉(zhuǎn)頭就忘。5.3 示例把OpenPose結(jié)果轉(zhuǎn)成YOLOv8-Pose訓(xùn)練標(biāo)簽YOLOv8-Pose的訓(xùn)練標(biāo)簽用的就是官方COCO 17點(diǎn)順序。從OpenPose的BODY_25跑完結(jié)果保存成YOLO格式時(shí)可以直接復(fù)用上面的轉(zhuǎn)換函數(shù)。假設(shè)你已經(jīng)用OpenPose對(duì)一張圖做了推理拿到(25, 3)的數(shù)組每個(gè)點(diǎn)是(x, y, confidence)。轉(zhuǎn)換過(guò)程如下kpts_17 convert_25_to_coco17(kpts_25) # shape (17, 3) # YOLO格式歸一化的 x, y, visibility x_norm kpts_17[:, 0] / img_width y_norm kpts_17[:, 1] / img_height vis (kpts_17[:, 2] 0.3).astype(int) yolo_line [0, center_x / img_width, center_y / img_height] for i in range(17): yolo_line.extend([x_norm[i], y_norm[i], vis[i]])注意這里關(guān)鍵點(diǎn)數(shù)組要拉平成一維才能和YOLO的標(biāo)簽格式對(duì)齊。visibility的計(jì)算閾值可以根據(jù)置信度分布調(diào)整建議先統(tǒng)計(jì)一下OpenPose輸出的置信度范圍再定別拍腦袋用0.5有時(shí)候OpenPose對(duì)腳部點(diǎn)的置信度整體偏低閾值設(shè)高了會(huì)把有效點(diǎn)全過(guò)濾掉。6. 踩坑實(shí)錄模型輸出、可視化、訓(xùn)練標(biāo)簽三處重災(zāi)區(qū)6.1 坑1把OpenPose COCO 18當(dāng)成官方COCO 17用這個(gè)坑我當(dāng)年就踩過(guò)。項(xiàng)目里要用官方COCO 17點(diǎn)格式的標(biāo)簽訓(xùn)練關(guān)鍵點(diǎn)檢測(cè)網(wǎng)絡(luò)我拿到OpenPose的18點(diǎn)輸出后想當(dāng)然地以為OpenPose的COCO就是官方COCO18比17多一個(gè)點(diǎn)去掉一個(gè)背景類就行結(jié)果直接取了前17個(gè)點(diǎn)。訓(xùn)練出來(lái)的模型測(cè)試效果很差精度始終上不去排查了很久才發(fā)現(xiàn)是把Neck當(dāng)成一個(gè)有效關(guān)鍵點(diǎn)喂進(jìn)去了導(dǎo)致網(wǎng)絡(luò)學(xué)習(xí)的時(shí)候第8個(gè)點(diǎn)RHip對(duì)應(yīng)的監(jiān)督信號(hào)是錯(cuò)的。正確的做法是必須先做一次索引重排把18點(diǎn)還原成官方COCO的17點(diǎn)順序刪除Neck然后按官方順序重新排列。類似的差一個(gè)點(diǎn)的問(wèn)題在MediaPipe、AlphaPose等不同框架里也會(huì)遇到每次換框架都要重新核對(duì)關(guān)鍵點(diǎn)順序不要相信框架名里的COCO三個(gè)字母。6.2 坑2畫骨架時(shí)把左右連反畫線這步相對(duì)隱蔽。比如你想把右肩RShoulder和右肘RElbow用一條線連起來(lái)用OpenPose的索引就是(2, 3)。但如果你參考的是官方COCO的連線習(xí)慣right_shoulder和right_elbow分別是6和8。兩邊索引對(duì)不上一旦混用畫出的人體就是右手肘接到了左肩上這種詭異形態(tài)。穩(wěn)妥的作圖思路是定義一套自己的語(yǔ)義連線列表比如(Nose, Neck)、(RShoulder, RElbow)這類帶名字的對(duì)然后每個(gè)模型定義自己的索引字典通過(guò)名字查索引再畫線。這樣比直接寫死數(shù)字對(duì)要安全得多keypoint_names_25 [ Nose, Neck, RShoulder, RElbow, RWrist, LShoulder, LElbow, LWrist, MidHip, RHip, RKnee, RAnkle, LHip, LKnee, LAnkle, REye, LEye, REar, LEar, LBigToe, LSmallToe, LHeel, RBigToe, RSmallToe, RHeel ] sematic_limbs [(Nose, Neck), (Neck, RShoulder), (Neck, LShoulder)] for a, b in sematic_limbs: idx_a keypoint_names_25.index(a) idx_b keypoint_names_25.index(b) if conf[idx_a] thr and conf[idx_b] thr: cv2.line(img, pts[idx_a], pts[idx_b], color, 2)用名字查索引雖然慢一點(diǎn)但代碼可讀性和可維護(hù)性提升一大截。預(yù)處理時(shí)直接把索引字典算好運(yùn)行時(shí)不會(huì)慢多少。6.3 坑3模型輸出通道數(shù)到底是18還是19用OpenCV DNN加載OpenPose時(shí)輸出張量形狀經(jīng)常讓人困惑。COCO 18模型的net.forward()結(jié)果可能是(1, 18, H, W)也可能是(1, 19, H, W)。這個(gè)差異取決于你用的prototxt文件是哪個(gè)版本。BODY_25模型同理可能輸出26個(gè)通道25個(gè)關(guān)鍵點(diǎn)加1個(gè)背景也可能只輸出25個(gè)通道。我見過(guò)不少人在out.shape[1]寫死數(shù)字換了個(gè)模型就崩了。正確處理方式是動(dòng)態(tài)判斷nPoints out.shape[1] if nPoints 19 and proto_contains_background: nPoints - 1更簡(jiǎn)單的判斷邏輯是如果你知道當(dāng)前加載的是哪個(gè)模型直接用對(duì)應(yīng)的點(diǎn)數(shù)如果不知道就檢查最后一個(gè)通道是否是全局背景——背景通道在整張圖上通常是較均勻的低響應(yīng)不像關(guān)鍵點(diǎn)熱圖那樣有清晰峰值可以用這個(gè)特征來(lái)區(qū)分。6.4 自查清單這幾個(gè)方法是我用來(lái)驗(yàn)證關(guān)鍵點(diǎn)順序是否正確用的每次接入新數(shù)據(jù)源或者訓(xùn)練完新模型后都會(huì)跑一遍打印索引和名稱加載模型后打印輸出通道數(shù)和已知的關(guān)鍵點(diǎn)名稱列表對(duì)比確認(rèn)通道順序。畫點(diǎn)不連線先把所有關(guān)鍵點(diǎn)以不同顏色畫在原圖上人工比對(duì)位置而不是直接畫骨架。點(diǎn)對(duì)了連線才可能對(duì)。左右對(duì)稱性檢查找一個(gè)正對(duì)鏡頭的標(biāo)定圖像驗(yàn)證左肩和右肩的x坐標(biāo)是否大致對(duì)稱如果左右顛倒立刻能發(fā)現(xiàn)。和官方Demo對(duì)比OpenPose官方Demo自帶可視化如果關(guān)鍵點(diǎn)位置和官方可視化中的標(biāo)簽對(duì)不上說(shuō)明你自己的解析或映射有問(wèn)題。最后再分享一個(gè)從實(shí)際項(xiàng)目中總結(jié)出來(lái)的小經(jīng)驗(yàn)關(guān)鍵點(diǎn)順序問(wèn)題越早驗(yàn)證越好。等跑到訓(xùn)練階段再發(fā)現(xiàn)標(biāo)簽索引錯(cuò)了浪費(fèi)的不只是時(shí)間還有重新標(biāo)注、重新預(yù)處理的大量成本。每次從外部拿到一組標(biāo)注好的關(guān)鍵點(diǎn)數(shù)據(jù)先畫出來(lái)人眼過(guò)一遍這種笨辦法反而是最高效的。