鍵點(diǎn)檢測(cè)與視頻流部署實(shí)戰(zhàn))
簡(jiǎn)介這是一份面向計(jì)算機(jī)視覺(jué)學(xué)習(xí)者與姿態(tài)識(shí)別開(kāi)發(fā)者的YOLOv8-pose人體關(guān)鍵點(diǎn)檢測(cè)實(shí)戰(zhàn)項(xiàng)目可直接跑通源碼并配套數(shù)據(jù)集適用于運(yùn)動(dòng)分析、人機(jī)交互、視頻監(jiān)控等場(chǎng)景的實(shí)驗(yàn)與二次開(kāi)發(fā)。壓縮包共1369個(gè)文件約89.04MB包含jpg圖像與標(biāo)注數(shù)據(jù)、py訓(xùn)練推理腳本、md說(shuō)明文檔、yaml配置文件以及pt預(yù)訓(xùn)練權(quán)重和日志、緩存等訓(xùn)練產(chǎn)物覆蓋從數(shù)據(jù)準(zhǔn)備到模型評(píng)估的主要環(huán)節(jié)。已有6223人學(xué)習(xí)說(shuō)明其可復(fù)現(xiàn)性和參考價(jià)值較高。項(xiàng)目圍繞頭部、肩肘腕、髖膝踝等關(guān)鍵點(diǎn)展開(kāi)提供完整目錄與運(yùn)行說(shuō)明讀者可據(jù)此快速搭建訓(xùn)練與推理流程理解多任務(wù)損失、數(shù)據(jù)增強(qiáng)和預(yù)訓(xùn)練遷移等實(shí)現(xiàn)細(xì)節(jié)并在此基礎(chǔ)上做精度調(diào)優(yōu)或場(chǎng)景遷移。1. 從一張多人合影到 17 個(gè)關(guān)鍵點(diǎn)這套 YOLOV8-pose 項(xiàng)目適合誰(shuí)姿態(tài)識(shí)別真正落地時(shí)最先卡住的通常不是網(wǎng)絡(luò)結(jié)構(gòu)而是關(guān)鍵點(diǎn)定義、標(biāo)簽格式和一套能跑通的訓(xùn)練腳本。這套基于 YOLOV8-pose 的姿態(tài)關(guān)鍵點(diǎn)檢測(cè)項(xiàng)目帶數(shù)據(jù)集和可直接跑通的源碼適合想快速驗(yàn)證人體 17 點(diǎn)、自定義關(guān)節(jié)布局或視頻流動(dòng)作分析的從業(yè)者。它不承諾解決所有遮擋和長(zhǎng)尾場(chǎng)景但能讓你在一個(gè)下午內(nèi)把訓(xùn)練、驗(yàn)證、推理、導(dǎo)出串起來(lái)先拿到一個(gè)能看的 baseline再?zèng)Q定往哪個(gè)方向補(bǔ)數(shù)據(jù)。如果你手頭已有一批關(guān)鍵點(diǎn)標(biāo)注卻總在kpt_shape、flip_idx和標(biāo)簽列數(shù)上翻車這套東西的價(jià)值會(huì)更明顯它把數(shù)據(jù)契約和訓(xùn)練命令放在同一個(gè)可對(duì)照的工程里省掉反復(fù)試錯(cuò)的時(shí)間。2. 數(shù)據(jù)契約先立住YOLO pose 標(biāo)簽、kpt_shape 與 flip_idx 三件套YOLOv8-pose 的訓(xùn)練能不能一次跑通七成取決于標(biāo)簽是否和data.yaml里的關(guān)鍵點(diǎn)定義對(duì)齊。模型結(jié)構(gòu)反而是后面的事。這一章先把一行標(biāo)簽拆開(kāi)再把kpt_shape、flip_idx和轉(zhuǎn)換腳本串起來(lái)最后給一個(gè)訓(xùn)練前必跑的自檢腳本。順序不要反先讓數(shù)據(jù)能被正確解析再談?wù){(diào)參。2.1 一行標(biāo)簽里到底存了什么YOLO pose 的標(biāo)簽文件是.txt文件名與圖片同名一行對(duì)應(yīng)一個(gè)人。單類人體 17 點(diǎn)的典型行如下0 0.512000 0.438000 0.276000 0.612000 0.501000 0.221000 2 0.523000 0.214000 2 ...前 5 個(gè)字段依次是類別 id、bbox 中心 x、bbox 中心 y、bbox 寬、bbox 高全部按圖像寬高歸一化到 0 到 1。后面每 3 個(gè)字段是一組關(guān)鍵點(diǎn)x y v。x、y同樣歸一化v是可見(jiàn)性標(biāo)記常見(jiàn)約定是0表示未標(biāo)注1表示標(biāo)注了但不可見(jiàn)2表示可見(jiàn)。17 點(diǎn)就是 51 個(gè)關(guān)鍵點(diǎn)字段加上前 5 個(gè)一行共 56 列。列數(shù)不對(duì)訓(xùn)練框架在解析階段就會(huì)報(bào)錯(cuò)或者把關(guān)鍵點(diǎn)錯(cuò)位讀進(jìn)網(wǎng)絡(luò)。bbox 不要隨手拿檢測(cè)框湊。關(guān)鍵點(diǎn)回歸依賴 bbox 提供的空間范圍框太緊會(huì)把手腕、腳踝裁到框外框太松又會(huì)讓模型學(xué)到多余背景。常見(jiàn)做法是讓 bbox 包住所有可見(jiàn)關(guān)鍵點(diǎn)并在四周留 5% 到 10% 的邊距。如果原始標(biāo)注里的 bbox 明顯不包含某個(gè)關(guān)鍵點(diǎn)轉(zhuǎn)換時(shí)先外擴(kuò)再歸一化。這個(gè)動(dòng)作很小但對(duì)早期收斂影響很直接。注意v0的點(diǎn)不要填一個(gè)看似合理的坐標(biāo)。未標(biāo)注就是未標(biāo)注坐標(biāo)置 0 更安全否則翻轉(zhuǎn)增強(qiáng)和損失計(jì)算會(huì)把一個(gè)假坐標(biāo)當(dāng)成真值。2.2 kpt_shape 和 flip_idx 為什么必須與關(guān)鍵點(diǎn)定義綁定data.yaml里的kpt_shape: [17, 3]告訴模型兩件事每個(gè)人有 17 個(gè)關(guān)鍵點(diǎn)每個(gè)點(diǎn)有 3 個(gè)值。如果你自定義了 5 個(gè)點(diǎn)做手勢(shì)或者 21 個(gè)點(diǎn)做手部這里必須同步改。只改標(biāo)簽不改kpt_shape訓(xùn)練會(huì)報(bào)關(guān)鍵點(diǎn)長(zhǎng)度不匹配只改kpt_shape不改標(biāo)簽?zāi)P蜁?huì)把多出來(lái)的列當(dāng)成下一行數(shù)據(jù)或者直接越界。flip_idx是水平翻轉(zhuǎn)增強(qiáng)的索引映射。開(kāi)啟fliplr0.5后圖像左右翻轉(zhuǎn)關(guān)鍵點(diǎn)坐標(biāo)也會(huì)翻轉(zhuǎn)但左右手腕、左右肩膀的語(yǔ)義不能靠坐標(biāo)自動(dòng)交換必須由flip_idx指定“第 i 個(gè)點(diǎn)翻轉(zhuǎn)后對(duì)應(yīng)第幾個(gè)點(diǎn)”。17 點(diǎn)人體常用順序是鼻、左眼、右眼、左耳、右耳、左肩、右肩、左肘、右肘、左腕、右腕、左髖、右髖、左膝、右膝、左踝、右踝對(duì)應(yīng)的flip_idx可以寫(xiě)成flip_idx: [0, 2, 1, 4, 3, 6, 5, 8, 7, 10, 9, 12, 11, 14, 13, 16, 15]這套映射的意思是鼻尖翻轉(zhuǎn)后還是鼻尖左眼和右眼互換左耳和右耳互換肩肘腕髖膝踝依次左右互換。自定義關(guān)鍵點(diǎn)布局時(shí)不要照抄這串?dāng)?shù)字拿一張正面圖手動(dòng)標(biāo)出左右點(diǎn)翻轉(zhuǎn)后再對(duì)一遍索引。這個(gè)檢查花十分鐘能省掉后面幾天的“玄學(xué)掉點(diǎn)”。2.3 把 COCO 關(guān)鍵點(diǎn)標(biāo)注轉(zhuǎn)成 YOLO pose 的腳本很多現(xiàn)成數(shù)據(jù)集是 COCO 關(guān)鍵點(diǎn) JSON 格式不能直接喂給 YOLO pose。下面這個(gè)腳本做三件事讀 JSON、把 bbox 轉(zhuǎn)成歸一化中心寬高、把關(guān)鍵點(diǎn)按x y v展開(kāi)寫(xiě).txt。腳本不復(fù)制圖片轉(zhuǎn)換完把圖片軟鏈接或復(fù)制到images/train、images/val即可。import json from pathlib import Path import numpy as np def coco_to_yolo_pose(json_path, out_dir, kpt_shape(17, 3), min_vis0): root Path(json_path).parent out_img Path(out_dir) / images out_lbl Path(out_dir) / labels out_img.mkdir(parentsTrue, exist_okTrue) out_lbl.mkdir(parentsTrue, exist_okTrue) data json.loads(Path(json_path).read_text(encodingutf-8)) images {im[id]: im for im in data[images]} k kpt_shape[0] for ann in data[annotations]: if ann.get(iscrowd, 0) 1: continue if ann.get(num_keypoints, 0) min_vis: continue im images[ann[image_id]] w, h im[width], im[height] x, y, bw, bh ann[bbox] if bw 1 or bh 1: continue # bbox 轉(zhuǎn)為中心點(diǎn) 寬高并歸一化 cx (x bw / 2) / w cy (y bh / 2) / h nw bw / w nh bh / h kps np.asarray(ann[keypoints], dtypenp.float32).reshape(k, 3) parts [] for px, py, v in kps: v int(v) if v 0: # 未標(biāo)注點(diǎn)不要偽造坐標(biāo)置 0 并保留 v0 parts.extend([0.0, 0.0, 0]) else: parts.extend([ float(np.clip(px / w, 0, 1)), float(np.clip(py / h, 0, 1)), v ]) cls_id 0 # 單類人體多類場(chǎng)景按 categories 建映射后替換 line .join( [str(cls_id), f{cx:.6f}, f{cy:.6f}, f{nw:.6f}, f{nh:.6f}] [str(v) for v in parts] ) stem Path(im[file_name]).stem (out_lbl / f{stem}.txt).write_text(line \n, encodingutf-8)kpt_shape必須和數(shù)據(jù)集關(guān)鍵點(diǎn)數(shù)量一致17 點(diǎn)就是(17, 3)。min_vis用來(lái)過(guò)濾關(guān)鍵點(diǎn)太少的樣本比如設(shè)為 5 可以丟掉幾乎全遮擋的標(biāo)注設(shè) 0 則保留全部。cls_id現(xiàn)在固定為 0多類場(chǎng)景要先從categories建一個(gè)category_id - 連續(xù)類別 id的映射不能直接把原始類別 id 寫(xiě)進(jìn)去否則類別號(hào)可能跳號(hào)。np.clip是防止個(gè)別標(biāo)注越界導(dǎo)致訓(xùn)練時(shí)坐標(biāo)異常但裁剪只能兜底轉(zhuǎn)換后仍要統(tǒng)計(jì)越界比例。關(guān)鍵點(diǎn)順序必須和flip_idx對(duì)應(yīng)。COCO 的 17 點(diǎn)順序與上一節(jié)列出的順序一致時(shí)flip_idx可以直接用如果你在轉(zhuǎn)換時(shí)重排了關(guān)鍵點(diǎn)flip_idx也要跟著重排。最穩(wěn)的驗(yàn)證方式是拿一張圖用腳本畫(huà)出 GT 骨架再水平翻轉(zhuǎn)圖像和關(guān)鍵點(diǎn)人工看左右手腕是否互換。這個(gè)步驟很土但比盯著 loss 猜有效。2.4 跑通前的數(shù)據(jù)自檢五個(gè)檢查項(xiàng)訓(xùn)練命令敲下去之前先跑一遍標(biāo)簽自檢。下面腳本檢查五件事標(biāo)簽是否有同名圖片、每行列數(shù)是否等于5 3*k、bbox 是否越界、可見(jiàn)關(guān)鍵點(diǎn)坐標(biāo)是否越界、可見(jiàn)性分布是否合理。from pathlib import Path from collections import Counter import cv2 def check_labels(img_dir, lbl_dir, k17): img_dir, lbl_dir Path(img_dir), Path(lbl_dir) bad, vis [], Counter() for p in sorted(lbl_dir.glob(*.txt)): img img_dir / (p.stem .jpg) if not img.exists(): img img_dir / (p.stem .png) if not img.exists(): bad.append((p.name, 缺圖片)) continue if cv2.imread(str(img)) is None: bad.append((p.name, 圖片不可讀)) continue for ln, line in enumerate(p.read_text().splitlines(), 1): if not line.strip(): continue cols line.split() if len(cols) ! 5 3 * k: bad.append((p.name, f第{ln}行列數(shù){len(cols)})) continue vals list(map(float, cols[1:])) box vals[:4] if not all(0 v 1 for v in box): bad.append((p.name, f第{ln}行bbox越界)) kps vals[4:] for i in range(k): x, y, v kps[3 * i:3 * i 3] vis[int(v)] 1 if v 0 and not (0 x 1 and 0 y 1): bad.append((p.name, f第{ln}行點(diǎn){i}越界)) print(異常數(shù):, len(bad)) for b in bad[:20]: print(b) print(可見(jiàn)性分布:, vis)k必須和data.yaml的kpt_shape[0]一致。圖片擴(kuò)展名按實(shí)際數(shù)據(jù)改.jpeg也要覆蓋。輸出里如果v0占比特別高說(shuō)明大量關(guān)鍵點(diǎn)未標(biāo)注訓(xùn)練時(shí)模型能學(xué)到的監(jiān)督信號(hào)有限先補(bǔ)標(biāo)或換數(shù)據(jù)比調(diào)模型更有效。異常數(shù)不為 0 時(shí)不要急著開(kāi)訓(xùn)練先按文件名定位到具體圖片和行號(hào)。自檢通過(guò)后目錄結(jié)構(gòu)保持images/train、labels/train、images/val、labels/val同級(jí)。訓(xùn)練框架按文件名找標(biāo)簽不按目錄遞歸猜。把同一段視頻的相鄰幀同時(shí)分進(jìn) train 和 val 會(huì)造成指標(biāo)虛高劃分時(shí)按視頻或按人物分組比隨機(jī)按幀切更可信。3. 把訓(xùn)練跑起來(lái)yolov8n-pose 選型、命令行參數(shù)與收斂判斷數(shù)據(jù)契約對(duì)齊后訓(xùn)練本身并不復(fù)雜復(fù)雜的是參數(shù)之間的牽制。這一章先講模型尺度怎么選再給一份能直接跑的命令和data.yaml然后說(shuō)清楚日志里哪些指標(biāo)值得盯最后把推理和可視化接上。訓(xùn)練不是把 epochs 拉滿就完事能在驗(yàn)證集上穩(wěn)定復(fù)現(xiàn)的配置才有意義。3.1 選 yolov8n-pose 還是 s/m顯存、幀率與精度的三角YOLOv8-pose 的 n/s/m 三個(gè)尺度參數(shù)量大約在 3M、11M、26M 量級(jí)。n 適合邊緣設(shè)備、單路視頻流和快速驗(yàn)證s 是精度和速度的折中多數(shù)業(yè)務(wù)先從這個(gè)尺度起步m 在遮擋嚴(yán)重、小目標(biāo)多、關(guān)鍵點(diǎn)定位要求高的場(chǎng)景更有優(yōu)勢(shì)但顯存和推理耗時(shí)也更高。不要一上來(lái)就上 m先把 n 跑通確認(rèn)標(biāo)簽和增強(qiáng)沒(méi)問(wèn)題再換 s/m 對(duì)比。模型尺度參數(shù)量級(jí)相對(duì)推理耗時(shí)適用場(chǎng)景主要代價(jià)yolov8n-pose約 3M最低邊緣端、預(yù)覽、快速 baseline小目標(biāo)關(guān)鍵點(diǎn)容易抖yolov8s-pose約 11M中等通用多人姿態(tài)、視頻流顯存占用上升yolov8m-pose約 26M較高遮擋多、遠(yuǎn)距離、精度優(yōu)先訓(xùn)練和部署成本高選型時(shí)不要只看 mAP。姿態(tài)關(guān)鍵點(diǎn)的業(yè)務(wù)指標(biāo)往往和 mAP 不完全一致比如手腕角度是否穩(wěn)定、腳踝在遮擋后能不能找回。先拿 n 跑一個(gè) baseline記錄驗(yàn)證集 mAP 和實(shí)際視頻抽檢結(jié)果再換 s 看提升是否值得多出來(lái)的耗時(shí)。如果 n 和 s 的指標(biāo)差距很小說(shuō)明數(shù)據(jù)質(zhì)量或標(biāo)簽一致性是瓶頸繼續(xù)加模型參數(shù)收益有限。3.2 一份能直接跑的訓(xùn)練命令與 data.yamldata.yaml放在數(shù)據(jù)集根目錄路徑寫(xiě)絕對(duì)路徑避免從不同工作目錄啟動(dòng)時(shí)找不到文件。下面這份配置對(duì)應(yīng)單類人體 17 點(diǎn)path: /data/pose_dataset train: images/train val: images/val test: images/test kpt_shape: [17, 3] flip_idx: [0, 2, 1, 4, 3, 6, 5, 8, 7, 10, 9, 12, 11, 14, 13, 16, 15] names: 0: person訓(xùn)練命令如下。先用yolov8n-pose.pt加載預(yù)訓(xùn)練權(quán)重讓骨干和檢測(cè)分支有一個(gè)不差的起點(diǎn)關(guān)鍵點(diǎn)分支如果和 COCO 17 點(diǎn)一致會(huì)直接復(fù)用不一致時(shí)會(huì)重建重建部分相當(dāng)于隨機(jī)初始化學(xué)習(xí)率不要開(kāi)得太大。yolo pose train \ modelyolov8n-pose.pt \ data/data/pose_dataset/data.yaml \ epochs120 \ imgsz640 \ batch16 \ device0 \ workers8 \ patience30 \ optimizerAdamW \ lr00.001 \ lrf0.01 \ warmup_epochs3 \ close_mosaic15 \ fliplr0.5 \ flipud0.0 \ degrees0.0 \ scale0.5 \ translate0.1 \ cacheram \ projectruns/pose \ nameexp01imgsz640是通用起點(diǎn)遠(yuǎn)距離小目標(biāo)可以試 960但顯存和耗時(shí)都會(huì)上去。batch16在 8GB 顯存上可能偏大報(bào) OOM 就減到 8 或 4不要同時(shí)改imgsz和batch否則排查變量太多。workers在 Windows 上建議設(shè) 0避免多進(jìn)程 DataLoader 卡死Linux 下按 CPU 核心數(shù)設(shè) 8 左右。cacheram只適合數(shù)據(jù)集較小、內(nèi)存充足的情況數(shù)據(jù)量大改成disk或False。close_mosaic15表示最后 15 個(gè) epoch 關(guān)閉 mosaic讓關(guān)鍵點(diǎn)定位在真實(shí)拼接分布上收一收通常比全程開(kāi) mosaic 更穩(wěn)。fliplr0.5必須配合正確的flip_idx否則左右點(diǎn)會(huì)學(xué)混flipud0.0是因?yàn)槿梭w上下翻轉(zhuǎn)不符合真實(shí)分布除非你的場(chǎng)景確實(shí)有倒立需求。注意modelyolov8n-pose.pt適合 17 點(diǎn)人體。自定義 5 點(diǎn)或 21 點(diǎn)時(shí)data.yaml的kpt_shape會(huì)觸發(fā)關(guān)鍵點(diǎn)頭重建預(yù)訓(xùn)練關(guān)鍵點(diǎn)分支不能直接復(fù)用訓(xùn)練初期 loss 波動(dòng)更大建議先把lr0降到 0.0005 觀察幾個(gè) epoch。3.3 訓(xùn)練日志怎么看loss 曲線、pose mAP 與過(guò)擬合信號(hào)訓(xùn)練啟動(dòng)后終端會(huì)持續(xù)打印 box loss、pose loss、分類 loss 和驗(yàn)證指標(biāo)。姿態(tài)任務(wù)重點(diǎn)看pose_loss是否穩(wěn)定下降以及驗(yàn)證集上的Pose mAP50、Pose mAP50-95是否同步改善。box loss 下降但 pose mAP 不動(dòng)常見(jiàn)原因是 bbox 學(xué)得不錯(cuò)但關(guān)鍵點(diǎn)監(jiān)督信號(hào)太弱檢查v0比例、關(guān)鍵點(diǎn)是否大量落在框外、flip_idx是否錯(cuò)位。每個(gè) epoch 結(jié)束后訓(xùn)練目錄下會(huì)生成results.csv和曲線圖。不要只看最后一行拉出最近 20 個(gè) epoch 看趨勢(shì)訓(xùn)練 loss 持續(xù)下降、驗(yàn)證 mAP 連續(xù)多個(gè) epoch 不升甚至下降就是過(guò)擬合信號(hào)早?;蚣訑?shù)據(jù)增強(qiáng)比繼續(xù)拉 epochs 更合理。驗(yàn)證集 mAP 抖動(dòng)很大時(shí)先確認(rèn) val 集樣本量是否太少幾百?gòu)垐D以內(nèi)的驗(yàn)證集本身方差就大不要根據(jù)單個(gè) epoch 的漲跌做決定。學(xué)習(xí)率方面AdamW 配lr00.001是常用起點(diǎn)SGD 可以試 0.01。如果前 5 個(gè) epoch loss 直接炸成 NaN先降學(xué)習(xí)率再檢查標(biāo)簽里是否有nan或極端越界坐標(biāo)。warmup_epochs3是給預(yù)訓(xùn)練權(quán)重和新建關(guān)鍵點(diǎn)頭一個(gè)緩沖自定義關(guān)鍵點(diǎn)數(shù)量時(shí)不要關(guān)掉 warmup。3.4 推理與可視化把 xy 坐標(biāo)還給原圖訓(xùn)練完拿best.pt做推理最容易被忽略的是坐標(biāo)還原。YOLOv8 的results.keypoints.xy已經(jīng)是原圖坐標(biāo)不需要再乘寬高如果你自己做了 letterbox 預(yù)處理才需要反向減 padding、除縮放比例。下面這段代碼把每個(gè)人可見(jiàn)關(guān)鍵點(diǎn)畫(huà)到原圖上并保留每個(gè)人的 bbox 對(duì)應(yīng)關(guān)系。from ultralytics import YOLO import cv2 model YOLO(runs/pose/exp01/weights/best.pt) results model.predict( sourcedemo.jpg, imgsz640, conf0.25, iou0.7, device0, saveFalse ) for r in results: if r.keypoints is None: continue xy r.keypoints.xy.cpu().numpy() # (N, 17, 2)原圖坐標(biāo) kconf r.keypoints.conf.cpu().numpy() # (N, 17) boxes r.boxes.xyxy.cpu().numpy() # (N, 4) for i in range(xy.shape[0]): x1, y1, x2, y2 boxes[i].astype(int) cv2.rectangle(r.orig_img, (x1, y1), (x2, y2), (0, 255, 0), 1) for j, (x, y) in enumerate(xy[i]): if kconf[i, j] 0.5: cv2.circle(r.orig_img, (int(x), int(y)), 3, (0, 0, 255), -1) cv2.imwrite(vis.jpg, r.orig_img)conf0.25是檢測(cè)框置信度閾值iou0.7控制 NMS 合并程度多人擁擠場(chǎng)景可以把iou降到 0.5 到 0.6減少框粘連。kconf是每個(gè)關(guān)鍵點(diǎn)的置信度畫(huà)圖時(shí)過(guò)濾 0.5 以下業(yè)務(wù)判定時(shí)閾值可以單獨(dú)調(diào)不要和檢測(cè)框閾值混用。r.orig_img是 BGR 格式直接交給 OpenCV 寫(xiě)文件。xy和boxes的順序一一對(duì)應(yīng)NMS 之后不要只取keypoints再自己排序否則人和骨架會(huì)對(duì)錯(cuò)。4. 避坑與排查關(guān)鍵點(diǎn)訓(xùn)練里最容易翻車的五類問(wèn)題姿態(tài)訓(xùn)練的報(bào)錯(cuò)往往不直觀表面看是 loss 不降根因可能在標(biāo)簽列數(shù)、翻轉(zhuǎn)映射或?qū)С鲱A(yù)處理。這一章按“現(xiàn)象 → 原因 → 解決”列五類高頻問(wèn)題每條都給可操作的排查動(dòng)作。遇到問(wèn)題先按順序排除不要一上來(lái)就改模型結(jié)構(gòu)。4.1 標(biāo)簽點(diǎn)數(shù)與 kpt_shape 對(duì)不上現(xiàn)象訓(xùn)練啟動(dòng)即報(bào)關(guān)鍵點(diǎn)長(zhǎng)度不匹配或者某個(gè) epoch 后突然出現(xiàn)索引越界驗(yàn)證時(shí)Pose mAP直接為 0。原因通常是data.yaml寫(xiě)了[17, 3]但標(biāo)簽實(shí)際是 5 點(diǎn)、21 點(diǎn)或者混入了一批只有 bbox 沒(méi)有關(guān)鍵點(diǎn)的檢測(cè)標(biāo)簽。解決先統(tǒng)計(jì)每個(gè)標(biāo)簽文件的列數(shù)換算關(guān)鍵點(diǎn)數(shù)量再統(tǒng)一kpt_shape。python - PY from pathlib import Path from collections import Counter c Counter() for p in Path(labels/train).glob(*.txt): for line in p.read_text().splitlines(): if line.strip(): c[(len(line.split()) - 5) // 3] 1 print(c) PY輸出里如果出現(xiàn)多個(gè)關(guān)鍵點(diǎn)數(shù)量說(shuō)明數(shù)據(jù)集混了不同定義必須按類別或按子集拆開(kāi)不能靠kpt_shape一個(gè)值硬吃。列數(shù)不能被 3 整除的標(biāo)簽也要單獨(dú)拎出來(lái)通常是手工編輯時(shí)多敲了一個(gè)字段。4.2 左右關(guān)鍵點(diǎn)翻轉(zhuǎn)后錯(cuò)位現(xiàn)象訓(xùn)練 loss 能降但驗(yàn)證集上左右手腕、左右腳踝經(jīng)?;Q動(dòng)作判定時(shí)角度忽正忽反。原因多半是fliplr開(kāi)了flip_idx沒(méi)配或配錯(cuò)。解決先關(guān)掉水平翻轉(zhuǎn)跑一個(gè)短 baseline確認(rèn)模型在無(wú)翻轉(zhuǎn)增強(qiáng)下左右點(diǎn)能分開(kāi)再打開(kāi)fliplr0.5用同一張圖做翻轉(zhuǎn)可視化逐點(diǎn)核對(duì)索引。COCO 17 點(diǎn)的flip_idx可以直接用上一章那串但自定義關(guān)鍵點(diǎn)必須自己寫(xiě)。檢查方法很直接取一張正面圖畫(huà)出 GT 骨架水平翻轉(zhuǎn)圖像和關(guān)鍵點(diǎn)看左肩是否落到右肩位置。如果翻轉(zhuǎn)后骨架交叉說(shuō)明映射錯(cuò)了。這個(gè)錯(cuò)誤不會(huì)讓訓(xùn)練報(bào)錯(cuò)只會(huì)讓指標(biāo)悄悄掉屬于典型的“黑匣子”問(wèn)題。4.3 損失下降但 mAP 不動(dòng)現(xiàn)象pose_loss一路下降訓(xùn)練集可視化看起來(lái)不錯(cuò)驗(yàn)證集Pose mAP50長(zhǎng)期停在 0.1 到 0.2。原因可能是驗(yàn)證集標(biāo)簽大量v0、bbox 與關(guān)鍵點(diǎn)不對(duì)應(yīng)、train/val 同源導(dǎo)致指標(biāo)虛高后回落或者學(xué)習(xí)率過(guò)大把關(guān)鍵點(diǎn)頭推崩。解決抽 20 張驗(yàn)證圖把 GT 骨架和預(yù)測(cè)骨架畫(huà)在同一張圖上對(duì)比統(tǒng)計(jì)v分布檢查 val 集是否和 train 來(lái)自同一段視頻把lr0減半再跑 30 個(gè) epoch 看趨勢(shì)。如果 GT 骨架本身就在框外優(yōu)先修 bbox不要調(diào)模型。如果v0占比超過(guò)一半模型能學(xué)到的點(diǎn)太少補(bǔ)標(biāo)或換數(shù)據(jù)比加 epochs 更有效。驗(yàn)證集 mAP 突然從很高掉到很低先懷疑數(shù)據(jù)泄漏或驗(yàn)證集里有損壞圖片不要先懷疑模型。4.4 顯存溢出與 DataLoader 卡死現(xiàn)象訓(xùn)練剛開(kāi)始就CUDA out of memory或者卡在第一個(gè) epoch 不動(dòng)Windows 下還會(huì)拋多進(jìn)程相關(guān)異常。原因通常是batch或imgsz太大、cacheram吃滿內(nèi)存、workers在 Windows 上開(kāi)太多。解決按順序減batch、降imgsz、關(guān)cache、把workers設(shè) 0。每次只改一個(gè)變量否則不知道是哪個(gè)參數(shù)觸發(fā)的。yolo pose train modelyolov8n-pose.pt data/data/pose_dataset/data.yaml \ epochs120 imgsz512 batch8 device0 workers0 cacheFalse ampTrueampTrue默認(rèn)開(kāi)啟能省顯存但如果出現(xiàn) loss NaN可以臨時(shí)關(guān)掉排除混合精度問(wèn)題。imgsz從 640 降到 512 會(huì)損失小目標(biāo)關(guān)鍵點(diǎn)精度只作為排查手段不要長(zhǎng)期用低分辨率硬扛。4.5 導(dǎo)出 ONNX 后關(guān)鍵點(diǎn)偏移現(xiàn)象PyTorch 推理關(guān)鍵點(diǎn)貼合導(dǎo)出 ONNX 后整體偏移幾個(gè)像素或者多人場(chǎng)景框和點(diǎn)對(duì)不上。原因通常是預(yù)處理不一致訓(xùn)練和 PyTorch 推理用了 letterboxONNX 側(cè)直接 resize 導(dǎo)致縮放比例和 padding 不同也可能是導(dǎo)出時(shí)沒(méi)開(kāi)simplify或者輸出解析時(shí)把歸一化坐標(biāo)當(dāng)成了像素坐標(biāo)。解決統(tǒng)一 letterbox 參數(shù)導(dǎo)出時(shí)加simplifyTrue用同一張圖分別跑 PyTorch 和 ONNX逐點(diǎn)對(duì)比。yolo export modelruns/pose/exp01/weights/best.pt formatonnx \ imgsz640 opset12 simplifyTrue導(dǎo)出后先用 Netron 看輸入輸出 shape確認(rèn)輸出字段順序。ONNX 側(cè)還原坐標(biāo)時(shí)先減 padding再除以縮放比例順序不能反。如果導(dǎo)出時(shí)加了nmsTrue輸出格式會(huì)變解析邏輯要跟著改不要拿舊腳本硬套。5. 從單幀到視頻流多人關(guān)鍵點(diǎn)歸屬、動(dòng)作判定與 ONNX 部署單張圖跑通只完成了一半。視頻流里要解決多人關(guān)鍵點(diǎn)歸屬、跨幀抖動(dòng)和部署預(yù)處理對(duì)齊三個(gè)問(wèn)題。這一章按“跟蹤 ID → 角度判定 → ONNX 預(yù)處理”推進(jìn)代碼盡量保持可抄參數(shù)按業(yè)務(wù)調(diào)。5.1 多人關(guān)鍵點(diǎn)歸屬與跟蹤 IDYOLO pose 的輸出天然把每個(gè) bbox 和一組關(guān)鍵點(diǎn)綁定NMS 后boxes、keypoints、conf同序。多人場(chǎng)景不要只取keypoints再按坐標(biāo)排序排序會(huì)打亂人的對(duì)應(yīng)關(guān)系。需要跨幀穩(wěn)定身份時(shí)用內(nèi)置跟蹤器把檢測(cè)框和 ID 關(guān)聯(lián)起來(lái)關(guān)鍵點(diǎn)跟著框走。from ultralytics import YOLO model YOLO(runs/pose/exp01/weights/best.pt) results model.track( sourcevideo.mp4, persistTrue, trackerbytetrack.yaml, conf0.3, iou0.6, imgsz640 ) for r in results: if r.boxes.id is None: continue ids r.boxes.id.int().cpu().tolist() xy r.keypoints.xy.cpu().numpy() kconf r.keypoints.conf.cpu().numpy() for i, tid in enumerate(ids): # xy[i] 就是該跟蹤 ID 當(dāng)前幀的 17 個(gè)關(guān)鍵點(diǎn) passpersistTrue讓跟蹤器在視頻幀之間保留狀態(tài)tracker文件控制匹配邏輯。boxes.id為空時(shí)說(shuō)明當(dāng)前幀沒(méi)有穩(wěn)定軌跡直接跳過(guò)不要給None做索引??鐜秳?dòng)可以用指數(shù)滑動(dòng)平均壓一下對(duì)同一個(gè)tid新關(guān)鍵點(diǎn)與上一幀做加權(quán)alpha取 0.5 到 0.7太低會(huì)拖影太高等于沒(méi)平滑。關(guān)鍵點(diǎn)置信度低于閾值的點(diǎn)用上一幀位置補(bǔ)不要用 0 坐標(biāo)補(bǔ)否則角度計(jì)算會(huì)瞬間跳變。5.2 動(dòng)作判定角度、滑動(dòng)窗口與閾值姿態(tài)關(guān)鍵點(diǎn)本身只是坐標(biāo)業(yè)務(wù)要的是動(dòng)作。以手臂彎曲為例用肩、肘、腕三點(diǎn)夾角判斷角度越小彎曲越明顯。下面函數(shù)負(fù)責(zé)算夾角輸入三個(gè)二維點(diǎn)輸出 0 到 180 度。import numpy as np def joint_angle(a, b, c): a, b, c map(np.asarray, (a, b, c)) p a hrefhttps://download.csdn.net/download/m0_51579041/88889941 stylecolor:#ec7500;font-size:14px; 本文還有配套的精品資源點(diǎn)擊獲取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p