估計(jì)與關(guān)鍵點(diǎn)檢測工程實(shí)戰(zhàn))
簡介面向人體姿態(tài)關(guān)鍵點(diǎn)檢測學(xué)習(xí)與實(shí)踐的完整項(xiàng)目包圍繞YOLOV8-pose模型展開適合計(jì)算機(jī)視覺初學(xué)者、課程設(shè)計(jì)與畢業(yè)設(shè)計(jì)開發(fā)者以及需要快速復(fù)現(xiàn)姿態(tài)估計(jì)實(shí)驗(yàn)的研究者。包內(nèi)提供可直接跑通的訓(xùn)練、推理與驗(yàn)證源碼配套人體關(guān)鍵點(diǎn)標(biāo)注數(shù)據(jù)集、預(yù)訓(xùn)練權(quán)重和依賴配置省去從零搭建網(wǎng)絡(luò)、整理標(biāo)注文件與調(diào)試運(yùn)行環(huán)境的重復(fù)勞動。壓縮包共1369個(gè)文件約89.04MB以jpg圖像、md說明文檔、txt標(biāo)注與配置、py腳本為主體輔以yaml訓(xùn)練參數(shù)、pt權(quán)重、json記錄及ipynb、png可視化結(jié)果覆蓋數(shù)據(jù)準(zhǔn)備、模型訓(xùn)練、日志記錄與使用文檔等環(huán)節(jié)。已有6223人學(xué)習(xí)反映出姿態(tài)識別與關(guān)鍵點(diǎn)檢測方向具有較高關(guān)注度。讀者可據(jù)此完成關(guān)鍵點(diǎn)檢測模型的訓(xùn)練、評估與推理理解數(shù)據(jù)增強(qiáng)、損失函數(shù)設(shè)計(jì)和預(yù)訓(xùn)練遷移等流程README與腳本也有助于快速復(fù)現(xiàn)結(jié)果、排查環(huán)境配置和運(yùn)行問題。1. 姿態(tài)關(guān)鍵點(diǎn)檢測落地為什么我選 YOLOV8-pose 而不是先啃 HRNet上個(gè)月幫朋友做一個(gè)健身動作糾偏的小工具需求很樸素?cái)z像頭對著人能把肩、肘、腕、髖、膝、踝這些關(guān)鍵點(diǎn)標(biāo)出來判斷深蹲有沒有塌腰、手肘有沒有外翻。第一反應(yīng)是去找姿態(tài)識別里口碑好的 HRNet 系列結(jié)果光是把預(yù)測熱圖轉(zhuǎn)回原圖坐標(biāo)、再對齊翻轉(zhuǎn)和縮放這一套后處理就調(diào)了大半天玄學(xué)得很。后來換成 YOLOV8-pose一個(gè)模型文件丟進(jìn)去predict出來直接是歸一化的關(guān)鍵點(diǎn)坐標(biāo)十七個(gè)點(diǎn)帶置信度畫骨架、算角度、判姿態(tài)全都能接上。這份源碼包做的就是這件事——一套帶數(shù)據(jù)集、能直接跑通的姿態(tài)關(guān)鍵點(diǎn)檢測工程適合想快速把姿態(tài)識別接進(jìn)自己業(yè)務(wù)、又不想在模型復(fù)現(xiàn)上耗掉一周的開發(fā)者。下面按“它能干什么、目錄長什么樣、怎么跑、怎么改”講透。2. 項(xiàng)目定位與技術(shù)選型單階段多人姿態(tài)估計(jì)的取舍2.1 它到底解決什么問題這個(gè)項(xiàng)目的核心任務(wù)是多人 2D 姿態(tài)估計(jì)Multi-Person Pose Estimation給一張圖或一幀視頻輸出畫面里每個(gè)人的 17 個(gè) COCO 標(biāo)準(zhǔn)關(guān)鍵點(diǎn)鼻、左右眼、左右耳、左右肩、左右肘、左右腕、左右髖、左右膝、左右踝每個(gè)點(diǎn)帶 x、y 坐標(biāo)和置信度。注意“多人”兩個(gè)字很關(guān)鍵——早期很多姿態(tài)方案是“先檢測人框、再對每個(gè)人單獨(dú)跑一遍關(guān)鍵點(diǎn)”兩階段串行人一多幀率就崩。YOLOV8-pose 走的是單階段路線一次前向同時(shí)回歸人框和該框內(nèi)的關(guān)鍵點(diǎn)N 個(gè)人就是 N 組輸出不額外多跑一遍網(wǎng)絡(luò)。從工程角度看這個(gè)取舍非常劃算。姿態(tài)估計(jì)在業(yè)務(wù)里很少是終點(diǎn)通常是上游特征提取健身糾偏要算關(guān)節(jié)角行為識別要喂骨架序列動畫驅(qū)動要做重定向。這些下游任務(wù)真正在意的是“關(guān)鍵點(diǎn)穩(wěn)不穩(wěn)、延遲夠不夠低”而不是“學(xué)術(shù)榜單上 PCKh 高 0.3 個(gè)點(diǎn)”。單階段模型在 640×640 輸入下中端顯卡能跑到實(shí)時(shí)甚至超實(shí)時(shí)CPU 上降分辨率也能湊合用這是它落地的最大底氣。2.2 為什么是 YOLOV8-pose 這套技術(shù)棧把技術(shù)棧拆開看其實(shí)是三層層次組成作用骨干與頸部CSPDarknet 系列 backbone PAN-FPN多尺度特征提取與融合檢測頭解耦頭分類/框回歸/關(guān)鍵點(diǎn)回歸分支輸出人框與 17 點(diǎn)熱圖式回歸訓(xùn)練與推理框架Ultralytics 封裝PyTorch 后端數(shù)據(jù)加載、增強(qiáng)、損失、導(dǎo)出、預(yù)測一條龍關(guān)鍵點(diǎn)在檢測頭里是這么處理的對每個(gè)人框模型回歸 17 組(x, y, visibility)其中 x、y 是相對于該人框的歸一化偏移visibility 是該點(diǎn)是否可見的置信度。這種“框內(nèi)相對坐標(biāo)”的設(shè)計(jì)讓模型不必在全圖尺度上直接回歸絕對坐標(biāo)收斂更穩(wěn)也讓同一個(gè)模型天然支持多人——每個(gè) anchor/query 只負(fù)責(zé)自己那個(gè)人。提示visibility不是“這個(gè)點(diǎn)在畫面里被遮擋的概率”這么簡單它同時(shí)承擔(dān)了“該點(diǎn)是否在圖像邊界內(nèi)”的語義。做下游角度計(jì)算時(shí)通常要把 visibility 低于閾值比如 0.5的點(diǎn)直接丟掉否則會出現(xiàn)用低置信點(diǎn)算出來的詭異角度。選型上還有兩個(gè)現(xiàn)實(shí)考量。一是生態(tài)Ultralytics 這套接口的訓(xùn)練、驗(yàn)證、導(dǎo)出ONNX、TensorRT、OpenVINO幾乎是模板化的換個(gè)部署環(huán)境不用重寫推理邏輯。二是數(shù)據(jù)集門檻低YOLO 格式的 pose 標(biāo)注就是一個(gè) txt 文件每行一個(gè)人類別 cx cy w h kx1 ky1 kv1 ... kx17 ky17 kv17全部歸一化。自己標(biāo)幾十張圖就能微調(diào)比 COCO-WholeBody 那種 133 點(diǎn)的重型標(biāo)注友好太多。2.3 源碼包的目錄骨架拿到這種工程我一般先tree一遍看結(jié)構(gòu)。這個(gè)包的組織方式基本是“訓(xùn)練腳本 配置 數(shù)據(jù)集 推理示例”四塊典型布局如下yolov8-pose-project/ ├── ultralytics/ # 框架源碼含 cfg 與 nn 模塊 │ ├── cfg/models/v8/ # yolov8-pose.yaml 等模型結(jié)構(gòu)定義 │ └── nn/ # 骨干、頸部、檢測頭、損失實(shí)現(xiàn) ├── datasets/ │ └── pose-demo/ │ ├── images/{train,val}/ │ ├── labels/{train,val}/ │ └── data.yaml # 數(shù)據(jù)配置路徑、關(guān)鍵點(diǎn)形狀、類別名 ├── weights/ │ └── yolov8n-pose.pt # 預(yù)訓(xùn)練權(quán)重用來做遷移學(xué)習(xí)起點(diǎn) ├── train.py # 訓(xùn)練入口 ├── val.py # 驗(yàn)證入口 ├── predict.py # 圖片/視頻推理演示 └── requirements.txt這個(gè)結(jié)構(gòu)里最容易被忽視的是data.yaml它決定了模型頭部的輸出維度。文件大致長這樣# datasets/pose-demo/data.yaml path: ./datasets/pose-demo train: images/train val: images/val kpt_shape: [17, 3] # 17 個(gè)關(guān)鍵點(diǎn)每個(gè)點(diǎn) 3 個(gè)值 (x, y, visibility) flip_idx: [0, 2, 1, 4, 3, 6, 5, 8, 7, 10, 9, 12, 11, 14, 13, 16, 15] # 水平翻轉(zhuǎn)時(shí)左右點(diǎn)的索引交換 names: 0: personflip_idx是很多人第一次改數(shù)據(jù)集時(shí)會栽的地方——它用于數(shù)據(jù)增強(qiáng)里的水平翻轉(zhuǎn)。如果你自己定義了 5 個(gè)關(guān)鍵點(diǎn)卻忘了同步改這個(gè)數(shù)組訓(xùn)練時(shí)翻轉(zhuǎn)增強(qiáng)會把“左肩”的標(biāo)簽貼到“右肩”位置上模型學(xué)出來的骨架會左右亂竄loss 看著在降實(shí)際預(yù)測全是錯的。這個(gè)坑后面還會細(xì)說。3. 跑通流程從環(huán)境到第一張骨架圖3.1 環(huán)境準(zhǔn)備與依賴安裝這套工程對環(huán)境的要求不算苛刻Python 3.8 以上、PyTorch 1.8 以上就能起步。GPU 訓(xùn)練建議 CUDA 11.x 配對應(yīng)的 torch 版本CPU 也能跑但訓(xùn)練會慢到讓人懷疑人生。我一般的做法是單獨(dú)開一個(gè) conda 環(huán)境避免和機(jī)器上其他項(xiàng)目的 torch 版本打架# 創(chuàng)建并激活環(huán)境 conda create -n pose python3.10 -y conda activate pose # 先裝 torch按自己的 CUDA 版本選對應(yīng)命令這里以 CUDA 11.8 為例 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 再裝工程依賴 pip install -r requirements.txt # requirements 里通常包含 ultralytics、opencv-python、numpy、pyyaml、tqdm安裝完先做一次體檢確認(rèn) torch 能吃上 GPUpython -c import torch; print(torch.__version__, torch.cuda.is_available())輸出里True才算 GPU 就緒。如果顯示False多半是裝成了 CPU 版 torch或者驅(qū)動版本和 CUDA runtime 不匹配這時(shí)候別急著往下跑訓(xùn)練先在nvidia-smi里核對驅(qū)動支持的 CUDA 上限。這個(gè)檢查看著多余但我見過太多次訓(xùn)練跑了兩小時(shí)才發(fā)現(xiàn)一直在 CPU 上爬的情況。3.2 數(shù)據(jù)集組織與標(biāo)注格式核對數(shù)據(jù)集這塊工程里自帶了一份演示數(shù)據(jù)目錄結(jié)構(gòu)在data.yaml里已經(jīng)聲明好了。跑通前必須核對兩件事圖片和標(biāo)簽同文件名、同相對路徑標(biāo)簽是 YOLO pose 格式的 txt。標(biāo)注文件里每一行的格式是這樣的# labels/train/0001.txt # class cx cy w h x1 y1 v1 x2 y2 v2 ... x17 y17 v17 0 0.512 0.603 0.284 0.791 0.501 0.312 2 0.487 0.305 2 ... 0.402 0.881 1所有數(shù)值都是歸一化到 [0,1] 的相對于圖片寬高關(guān)鍵點(diǎn)坐標(biāo)是相對于整張圖而不是人框——這點(diǎn)和推理階段的輸出不同推理輸出是相對框的后處理時(shí)框架會自動轉(zhuǎn)回去但如果自己寫標(biāo)注轉(zhuǎn)換腳本這是最容易搞混的一處。v的取值一般是 0/1/20 表示未標(biāo)注1 表示標(biāo)注但被遮擋2 表示可見且標(biāo)注。如果你的標(biāo)注工具只輸出 0/1也問題不大但flip_idx和kpt_shape必須和實(shí)際點(diǎn)數(shù)嚴(yán)格一致。正確性校驗(yàn)有個(gè)很實(shí)用的小腳本把標(biāo)簽還原成骨架畫到圖上肉眼過一遍import cv2 import numpy as np IMG datasets/pose-demo/images/train/0001.jpg LBL datasets/pose-demo/labels/train/0001.txt # COCO 17 點(diǎn)的骨架連接對用來連線可視化 SKELETON [(5,7),(7,9),(6,8),(8,10),(5,6),(5,11),(6,12),(11,12), (11,13),(13,15),(12,14),(14,16),(0,1),(0,2),(1,3),(2,4)] img cv2.imread(IMG) h, w img.shape[:2] for line in open(LBL): parts np.array(line.split(), dtypefloat) kpts parts[5:].reshape(-1, 3) # 從第 6 個(gè)值起是 17*3 的關(guān)鍵點(diǎn) pts (kpts[:, :2] * [w, h]).astype(int) # 反歸一化回像素坐標(biāo) for a, b in SKELETON: if kpts[a, 2] 0 and kpts[b, 2] 0: # 兩端都有效才連線 cv2.line(img, tuple(pts[a]), tuple(pts[b]), (0, 255, 0), 2) for (x, y), v in zip(pts, kpts[:, 2]): if v 0: cv2.circle(img, (x, y), 3, (0, 0, 255), -1) cv2.imwrite(check.jpg, img)這段代碼做了三件事按行解析每個(gè)目標(biāo)的標(biāo)注、把關(guān)鍵點(diǎn)反歸一化、按骨架連接對畫線畫點(diǎn)。參數(shù)上SKELETON是 COCO 標(biāo)準(zhǔn)連接關(guān)系kpts[a, 2] 0這個(gè)判斷是為了跳過未標(biāo)注的點(diǎn)否則會在圖上的 (0,0) 附近畫出一堆亂線。跑完打開check.jpg如果骨架是貼在人體上的說明標(biāo)注沒問題如果整個(gè)人是歪的或者骨架飄在別處那基本是歸一化基準(zhǔn)錯了比如按人框而不是整圖歸一化。3.3 訓(xùn)練命令與關(guān)鍵參數(shù)環(huán)境、數(shù)據(jù)都確認(rèn)后訓(xùn)練就是一條命令的事python train.py \ --model weights/yolov8n-pose.pt \ --data datasets/pose-demo/data.yaml \ --epochs 100 \ --imgsz 640 \ --batch 16 \ --device 0 \ --workers 4 \ --project runs/pose --name demo幾個(gè)參數(shù)我按重要性排一下--model預(yù)訓(xùn)練權(quán)重起點(diǎn)。即使你的關(guān)鍵點(diǎn)數(shù)和 COCO 不同比如只標(biāo) 5 個(gè)點(diǎn)也可以加載框架會自動裁掉/重建頭部但骨干權(quán)重仍然有用收斂比從零訓(xùn)快得多。--imgsz輸入分辨率。640 是性能和精度的平衡點(diǎn)遠(yuǎn)距離小目標(biāo)多就上 960但顯存和延遲一起漲。--batch按顯存調(diào)OOM 就先減半別硬撐。--workersDataLoader 進(jìn)程數(shù)Windows 上建議設(shè)小一點(diǎn)甚至設(shè) 0否則容易卡死在多進(jìn)程上。--project/--name輸出目錄權(quán)重存runs/pose/demo/weights/best.pt訓(xùn)練曲線、混淆矩陣、驗(yàn)證集預(yù)測圖都在同目錄下。訓(xùn)練腳本里真正干活的部分一般是對框架 API 的封裝早期版本寫法大致是這樣from ultralytics import YOLO # 加載帶 pose 頭的預(yù)訓(xùn)練權(quán)重 model YOLO(weights/yolov8n-pose.pt) # 開始訓(xùn)練內(nèi)部會讀取 data.yaml 里的 kpt_shape 構(gòu)建頭部 results model.train( datadatasets/pose-demo/data.yaml, epochs100, imgsz640, batch16, device0, workers4, projectruns/pose, namedemo, # 姿態(tài)任務(wù)常用的增強(qiáng)開關(guān) fliplr0.5, # 水平翻轉(zhuǎn)依賴 data.yaml 的 flip_idx degrees10.0, # 小幅旋轉(zhuǎn)應(yīng)對拍攝角度 scale0.5, # 隨機(jī)縮放提升尺度魯棒性 mosaic1.0, # 馬賽克增強(qiáng)前期提升明顯 )這里要點(diǎn)一下fliplr和flip_idx的聯(lián)動關(guān)系fliplr0.5意味著每張圖有一半概率被水平翻轉(zhuǎn)翻轉(zhuǎn)時(shí)標(biāo)簽坐標(biāo)要鏡像x - 1-x同時(shí)左右關(guān)鍵點(diǎn)的語義要對調(diào)左肩變右肩??蚣芸縟ata.yaml里的flip_idx完成后者。所以flip_idx不是可選裝飾它是增強(qiáng)正確性的前提。同理mosaic1.0把四張圖拼成一張對小目標(biāo)友好但訓(xùn)練末段通常要關(guān)掉框架默認(rèn)最后 10 個(gè) epoch 自動關(guān)閉否則真實(shí)場景的構(gòu)圖分布和訓(xùn)練分布偏差會拉低最終指標(biāo)。3.4 推理與骨架可視化訓(xùn)練完最爽的一步是看效果。工程里的predict.py一般會做圖片、視頻、攝像頭三種輸入的分支核心調(diào)用只有幾行from ultralytics import YOLO model YOLO(runs/pose/demo/weights/best.pt) # 對單張圖片推理saveTrue 會把帶骨架的結(jié)果圖寫到 runs/pose/predict 下 results model.predict(test.jpg, imgsz640, conf0.4, saveTrue) for r in results: if r.keypoints is None: continue # keypoints.xy: (人數(shù), 17, 2) 像素坐標(biāo); keypoints.conf: (人數(shù), 17) 置信度 xy r.keypoints.xy.cpu().numpy() conf r.keypoints.conf.cpu().numpy() for person_xy, person_conf in zip(xy, conf): # 只保留置信度夠高的點(diǎn)避免下游算角度時(shí)用到垃圾數(shù)據(jù) valid person_conf 0.5 print(有效關(guān)鍵點(diǎn)數(shù):, valid.sum())conf0.4是人框的置信度閾值不是關(guān)鍵點(diǎn)的關(guān)鍵點(diǎn)置信度要單獨(dú)在keypoints.conf里過濾。這兩個(gè)閾值混用是新手高頻翻車點(diǎn)把人框閾值調(diào)得很高結(jié)果遠(yuǎn)處的人整個(gè)被丟掉或者只看人框置信度就直接拿所有關(guān)鍵點(diǎn)去算角度被遮擋點(diǎn)的坐標(biāo)其實(shí)是一團(tuán)噪聲。推理速度上yolov8n-pose在 640 輸入、單張中端卡上通常是毫秒級到十幾毫秒換yolov8s/m精度漲一點(diǎn)、延遲也漲。做實(shí)時(shí)視頻建議先用 n 版跑通鏈路再按精度需求往上換型號別一上來就上大模型然后抱怨卡。4. 核心模塊拆解一個(gè)關(guān)鍵點(diǎn)是怎么被回歸出來的4.1 檢測頭里的關(guān)鍵點(diǎn)分支框架的檢測頭在ultralytics/nn/modules/head.py里pose 版本的頭繼承自檢測頭多出一條關(guān)鍵點(diǎn)回歸分支。結(jié)構(gòu)上的關(guān)鍵點(diǎn)是關(guān)鍵點(diǎn)不是獨(dú)立預(yù)測的而是綁定在每個(gè)人框上的。簡化后的前向邏輯大致是# 偽代碼保留關(guān)鍵結(jié)構(gòu)省略了大量廣播與形狀處理 class PoseHead(nn.Module): def __init__(self, nc, kpt_shape, ch()): super().__init__() self.nc nc # 類別數(shù)姿態(tài)任務(wù)通常就是 1人 self.nk kpt_shape[0] # 關(guān)鍵點(diǎn)數(shù)量COCO 是 17 self.no nc 4 self.nk * 3 # 每框輸出: 類別 框4 關(guān)鍵點(diǎn)(17*3) # 三條并行分支分類、框回歸、關(guān)鍵點(diǎn)回歸 self.cv2 nn.ModuleList(...) # 分類 框 self.cv3 nn.ModuleList(...) # 關(guān)鍵點(diǎn) self.cv4 nn.ModuleList(...) # 關(guān)鍵點(diǎn)可見性/精修 def forward(self, x): for i in range(self.nl): # 遍歷 P3/P4/P5 三個(gè)尺度 cls_box self.cv2[i](x[i]) # (B, 4nc, H, W) kpt self.cv3[i](x[i]) # (B, nk*3, H, W) # 關(guān)鍵點(diǎn)坐標(biāo)用 sigmoid 約束到 [0,1]表示相對網(wǎng)格/框的偏移 kpt[..., 0::3] kpt[..., 0::3].sigmoid() kpt[..., 1::3] kpt[..., 1::3].sigmoid() # 第三維visibility不套 sigmoid直接作為 logit 參與 BCE 損失這段里最值得記住的是self.no nc 4 self.nk * 3它決定了每個(gè)預(yù)測位置輸出多少維。你改關(guān)鍵點(diǎn)數(shù)量時(shí)kpt_shape一變這個(gè)維度自動跟著變所以永遠(yuǎn)改data.yaml和模型 yaml 里的kpt_shape不要去手改no。另外x、y走 sigmoid 是有意的——把坐標(biāo)壓進(jìn) [0,1]語義上是“相對該人框左上角的歸一化偏移”這讓不同大小的目標(biāo)共享同一套回歸目標(biāo)尺度訓(xùn)練穩(wěn)定得多。后處理階段框架會把這些相對偏移乘上對應(yīng)人框的寬高、再加上框左上角坐標(biāo)還原成原圖像素坐標(biāo)。這條鏈路藏在ops.py的kpt_decode一類函數(shù)里平時(shí)不用碰但排查“預(yù)測點(diǎn)整體偏移”類問題時(shí)就得順著這條鏈一路看下來。4.2 姿態(tài)損失函數(shù)的三部分構(gòu)成訓(xùn)練姿態(tài)模型loss 由三塊加起來框回歸損失、分類損失、關(guān)鍵點(diǎn)損失。前兩塊和普通檢測一致CIoU BCE姿態(tài)特有的第三塊是重點(diǎn)。關(guān)鍵點(diǎn)損失一般是OKS-based 的加權(quán) BCE或者直接的坐標(biāo)回歸損失實(shí)現(xiàn)里大致是# 簡化示意關(guān)鍵點(diǎn)損失的核心思想 # 1) 對每個(gè) GT 關(guān)鍵點(diǎn)找到負(fù)責(zé)它的預(yù)測點(diǎn) # 2) 坐標(biāo)用 L1/L2 回歸visibility 用 BCE # 3) 按 OKS 的 sigmoid 權(quán)重對不同關(guān)鍵點(diǎn)加權(quán) loss_kpt 0.0 for gt_kpt, pred_kpt, vis in zip(gt_kpts, pred_kpts, gt_vis): if vis 0: # 未標(biāo)注的點(diǎn)不參與坐標(biāo)損失 continue loss_kpt (pred_kpt - gt_kpt).abs().mean() loss_kpt loss_kpt / max(vis.sum(), 1) # 按有效點(diǎn)數(shù)歸一化 loss_vis BCEWithLogits(pred_vis, (gt_vis 0).float())兩個(gè)細(xì)節(jié)決定成敗。第一vis 0的點(diǎn)必須 mask 掉不然模型會被迫去擬合“未標(biāo)注”的位置等價(jià)于教它胡說。第二歸一化要按有效點(diǎn)數(shù)而不是總點(diǎn)數(shù)否則一個(gè)大部分人只有半身可見的 batchloss 尺度會忽大忽小學(xué)習(xí)率都不好調(diào)。OKS 那套權(quán)重的作用是讓眼睛、手腕這類小且難的點(diǎn)在總 loss 里占比合理避免被肩髖這些大關(guān)節(jié)淹沒。4.3 數(shù)據(jù)增強(qiáng)對姿態(tài)任務(wù)的特殊處理檢測任務(wù)的增強(qiáng)搬到姿態(tài)上核心矛盾是任何幾何變換都必須同步作用到關(guān)鍵點(diǎn)上且左右翻轉(zhuǎn)要處理語義交換。馬賽克增強(qiáng)在姿態(tài)里尤其危險(xiǎn)——四張圖拼接后原來的人可能被裁掉一半關(guān)鍵點(diǎn)坐標(biāo)隨之越界框架內(nèi)部會做裁剪和過濾但如果你自己改增強(qiáng)流程一定要保證越界點(diǎn)被正確標(biāo)記為不可見而不是硬留在 [0,1] 之外。常見的正確做法是先算好圖片的仿射矩陣同時(shí)對圖像做 warpAffine、對關(guān)鍵點(diǎn)做同一矩陣的坐標(biāo)變換再根據(jù)變換后坐標(biāo)是否落在圖內(nèi)更新 visibility。翻轉(zhuǎn)時(shí)按flip_idx交換點(diǎn)序并x - 1 - x。這兩步看似機(jī)械但只要漏一步訓(xùn)練集會悄悄變臟loss 曲線照樣好看mAP 就是上不去——這類“黑匣子”式的指標(biāo)異常十有八九要回到增強(qiáng)代碼里找。4.4 驗(yàn)證指標(biāo)怎么看姿態(tài)任務(wù)的驗(yàn)證不能只看 mAP。工程val.py跑完會輸出幾類指標(biāo)框的 mAP50-95、以及基于 OKS 的關(guān)鍵點(diǎn) AP。OKSObject Keypoint Similarity是姿態(tài)版的 IoU按每個(gè)關(guān)鍵點(diǎn)的標(biāo)注方差加權(quán)算相似度再取平均。實(shí)際看的時(shí)候我會重點(diǎn)關(guān)注三件事一是AP 隨 OKS 閾值的變化如果 AP50 很高但 AP75 掉得厲害說明點(diǎn)位普遍差幾個(gè)像素多半是輸入分辨率不夠或者小目標(biāo)太多。二是分關(guān)鍵點(diǎn)看誤差手腕、腳踝通常是最差的如果差得離譜檢查這幾類點(diǎn)在訓(xùn)練集里的可見比例。三是看驗(yàn)證集的預(yù)測可視化圖框架會自動存一批肉眼看骨架有沒有系統(tǒng)性偏移比如整體偏右下有的話基本是坐標(biāo)解碼環(huán)節(jié)或標(biāo)注歸一化基準(zhǔn)出了問題。5. 常見問題排查我踩過的五個(gè)坑5.1 訓(xùn)練 loss 正常但預(yù)測骨架全亂現(xiàn)象訓(xùn)練 loss 平穩(wěn)下降驗(yàn)證指標(biāo)看著也還行但拿best.pt預(yù)測骨架連到了錯誤的人身上或者所有點(diǎn)擠在一小塊區(qū)域。原因data.yaml里kpt_shape和模型 yaml 里的kpt_shape不一致或者flip_idx長度與點(diǎn)數(shù)不匹配。框架在某些版本下不會直接報(bào)錯而是按較小維度截?cái)鄬?dǎo)致關(guān)鍵點(diǎn)順序錯位。解決訓(xùn)練前打印一次模型頭的實(shí)際輸出維度和kpt_shape[0] * 3對一遍flip_idx用腳本自動生成COCO 17 點(diǎn)就用標(biāo)準(zhǔn)數(shù)組自定義點(diǎn)數(shù)時(shí)手寫并校驗(yàn)長度。5.2 水平翻轉(zhuǎn)增強(qiáng)后左右肢體識別反了現(xiàn)象不翻轉(zhuǎn)的圖預(yù)測正常一遇到鏡像自拍或前置攝像頭畫面左右手腕、左右腳踝全部對調(diào)。原因fliplr開了但flip_idx是默認(rèn)的 COCO 順序而你的數(shù)據(jù)集點(diǎn)數(shù)不同或者干脆沒配。翻轉(zhuǎn)時(shí)坐標(biāo)鏡像了語義沒對調(diào)。解決把flip_idx改成和數(shù)據(jù)點(diǎn)定義嚴(yán)格對應(yīng)的交換數(shù)組如果暫時(shí)不想處理直接把fliplr設(shè) 0先保證正確性再談增強(qiáng)收益。5.3 顯存 OOM 或訓(xùn)練中途進(jìn)程被殺現(xiàn)象訓(xùn)練跑幾十個(gè) iteration 后報(bào) CUDA out of memory或者 Linux 下進(jìn)程被 OOM killer 干掉。原因batch與imgsz組合超出顯存或者workers開太大每個(gè) worker 都緩存了一份數(shù)據(jù)內(nèi)存被吃光。解決先把batch減半、imgsz降到 512 試一輪確認(rèn)能穩(wěn)定跑再往上加workers在內(nèi)存緊張的機(jī)器上設(shè) 2 到 4Windows 上設(shè) 0。真需要在有限顯存上跑大分辨率用梯度累積模擬大 batch。5.4 關(guān)鍵點(diǎn)置信度普遍偏低、下游角度算不準(zhǔn)現(xiàn)象人明明站得端正預(yù)測出來的手腕、腳踝置信度只有 0.3 左右按 0.5 過濾后點(diǎn)數(shù)不夠算關(guān)節(jié)角直接崩。原因訓(xùn)練集里這類點(diǎn)標(biāo)注稀疏或被大量遮擋模型學(xué)不到高置信輸出也可能是驗(yàn)證/推理時(shí)的imgsz與訓(xùn)練不一致尺度變化讓本來就不穩(wěn)的小關(guān)鍵點(diǎn)更飄。解決補(bǔ)標(biāo)一批包含完整四肢的樣本訓(xùn)練時(shí)適當(dāng)降低degrees和scale的強(qiáng)度讓模型先學(xué)好基本點(diǎn)位推理側(cè)把關(guān)鍵點(diǎn)閾值和“可見性”分開處理用 visibility 做軟加權(quán)而不是硬閾值角度計(jì)算時(shí)對低置信點(diǎn)做鄰近幀平滑。5.5 導(dǎo)出 ONNX 后推理結(jié)果和 PyTorch 對不上現(xiàn)象PyTorch 下骨架正常導(dǎo)出 ONNX 用 onnxruntime 跑關(guān)鍵點(diǎn)整體偏移或數(shù)量不對。原因?qū)С鰰r(shí)沒指定opset或者輸入尺寸動態(tài)化導(dǎo)致后處理里的坐標(biāo)還原邏輯假設(shè)失效也有版本是導(dǎo)出時(shí)把 NMS 一起打包后處理重復(fù)做了一次。解決導(dǎo)出時(shí)固定imgsz、顯式指定opset12以上導(dǎo)出后先用同一張圖對比兩邊輸出的最大絕對誤差正常應(yīng)在 1e-3 量級NMS 是否內(nèi)置要確認(rèn)清楚避免二次過濾。真對不上就退回導(dǎo)出去掉 NMS 的版本自己在外部做后處理鏈路反而清晰。6. 二次開發(fā)把關(guān)鍵點(diǎn)接進(jìn)自己的業(yè)務(wù)邏輯跑通只是起點(diǎn)真正花時(shí)間的是把 17 個(gè)點(diǎn)變成業(yè)務(wù)結(jié)論。我一般會在工程里加一個(gè)獨(dú)立的后處理模塊和模型推理解耦這樣換模型、調(diào)閾值都不用動業(yè)務(wù)代碼。最常用的是關(guān)節(jié)角計(jì)算比如判斷手肘彎曲程度import numpy as np def angle_between(a, b, c): 計(jì)算 b 為頂點(diǎn)時(shí)a-b-c 的夾角角度制 ba np.array(a) - np.array(b) bc np.array(c) - np.array(b) # 加 eps 防止零向量導(dǎo)致除零 cos np.dot(ba, bc) / (np.linalg.norm(ba) * np.linalg.norm(bc) 1e-8) return np.degrees(np.arccos(np.clip(cos, -1.0, 1.0))) # COCO 索引: 5左肩 7左肘 9左腕 L_SHOULDER, L_ELBOW, L_WRIST 5, 7, 9 elbow_angle angle_between(kpts[L_SHOULDER], kpts[L_ELBOW], kpts[L_WRIST])參數(shù)上要守住兩條一是傳入前先確認(rèn)這三個(gè)點(diǎn)的置信度都過閾值否則算出來的角度毫無意義二是np.clip不能省浮點(diǎn)誤差會讓arccos的輸入冒出 1.0000001 直接變 NaN。這就是典型的“算法沒問題、工程細(xì)節(jié)翻車”。改造思路上有三條路值得展開。第一條是換數(shù)據(jù)集適配自己的場景比如只關(guān)心上半身可以把kpt_shape改成[5, 3]甚至更少同步改flip_idx模型更輕、小樣本下更穩(wěn)。注意改點(diǎn)數(shù)等于換任務(wù)預(yù)訓(xùn)練頭要重建但骨干權(quán)重照樣能加載這一點(diǎn)前面提過。第二條是做時(shí)序平滑單幀姿態(tài)抖動是常態(tài)視頻場景里對每個(gè)關(guān)鍵點(diǎn)維護(hù)一個(gè)滑動窗口做中值濾波或 One-Euro 濾波能顯著降低骨架“抽搐”代價(jià)是引入幾幀延遲實(shí)時(shí)糾偏類應(yīng)用要權(quán)衡。第三條是模型蒸餾或換小模型先用yolov8m-pose訓(xùn)一個(gè)高精度版本再用它的輸出當(dāng)軟標(biāo)簽訓(xùn)yolov8n-pose部署是端側(cè)性能不夠時(shí)最省事的路線。驗(yàn)證改造是否有效我的習(xí)慣是固定一套“回歸樣本”挑 30 到 50 張覆蓋不同姿態(tài)、遮擋、光照的圖每次改完代碼都跑一遍比對關(guān)鍵點(diǎn)坐標(biāo)的平均像素誤差和下游業(yè)務(wù)指標(biāo)比如動作判定準(zhǔn)確率。這套樣本不進(jìn)訓(xùn)練集專門當(dāng)后悔藥用。從那以后我每次動kpt_shape、flip_idx或者增強(qiáng)參數(shù)都強(qiáng)制先跑一遍這套回歸樣本再提交省下來的返工時(shí)間遠(yuǎn)多于維護(hù)它的成本。希望這些能幫你在接姿態(tài)識別時(shí)少繞幾個(gè)彎。本文還有配套的精品資源點(diǎn)擊獲取