CV Day 28 YOLO-Pose 實戰:訓練與推論
執行需求:Colab T4 可跑。本篇在 Colab 免費 T4(16 GB VRAM)上從下載 COCO 姿態子集到訓練 50 epoch 跑完整流程,yolo11n-pose 約 18 分鐘、yolo11s-pose 約 30 分鐘;CPU 不建議執行訓練,會跑很久。預訓練權重 yolo11n-pose.pt 從 Ultralytics 8.3 自動下載,約 6 MB,可在 T4 上推到 80 FPS。
引言
昨天的內容中,我們對照了由上而下與由下而上、熱圖回歸與座標回歸四條姿態估計路線。今天把昨天的概念落到實作:用 Ultralytics 8.3 的 yolo11n-pose.pt 預訓練權重,在一個小型 COCO 姿態子集上微調,並把訓練後的 best.pt 拿來推論與視覺化。YOLO11 Pose 是「由上而下 + 座標回歸」的代表設計——它把偵測與關鍵點合併成單一階段,骨幹直接輸出 bbox、類別、17 個關鍵點 (x, y, visibility),省略傳統由上而下的「先偵測、再對每個人獨立跑關鍵點估計」的兩階段流程。Ultralytics 8.3 把這個複雜的管線封裝成 model.train(task="pose") 一行指令,是當前最容易上手的姿態估計工作流之一。
本篇選擇的示範資料是 COCO 2017 val 的姿態子集(5,000 張影像、約 11K 個人體標註),從 cocodataset.org 下載 person_keypoints_val2017.zip 與 val2017.zip。為了讓 Colab T4 在合理時間內跑完,我們只抽其中 1,000 張做訓練、200 張做驗證。這樣安排有兩個好處:第一,類別固定(只有 person 一類)、資料量小、T4 跑得動;第二,整個「下載 COCO 標註 → 篩選影像 → 轉 Ultralytics pose 格式 → 寫 pose.yaml」的流程,把 Day 26 的 OKS 概念與 Day 27 的路線比較接到真實訓練上,而不是跳過準備工作。之後要換成自己的資料時,只要替換影像與標註即可。
讀完這篇你會了解:Ultralytics 8.3 的 pose 資料格式(不同於 detection 格式)、model.train(data=..., task="pose") 的呼叫方式、訓練後的 keypoints 欄位結構、以及 model.predict() 回傳的 Results 物件如何把 17 個關鍵點與 19 條骨架視覺化。明天我們會換到完全不同的姿態工作流——MediaPipe 0.10 的 mp.solutions.hands、face_mesh、holistic,用零樣本 API 直接從 webcam 抓 21 個手部關鍵點、468 個臉部點、33 個全身點。
Ultralytics 8.3 的 Pose 介面
Ultralytics 8.3 把姿態估計封裝成 YOLO("yolo11n-pose.pt"),訓練流程沿用 Day 13 的 model.train() 介面,但有三個關鍵差異:第一,data.yaml 必須是 pose 格式(每張影像的標註包含 17 個關鍵點的 (x, y, visibility)),不能直接拿 detection 的 bbox 標註用;第二,訓練時呼叫 model.train(data=..., task="pose"),Ultralytics 會自動切換到 pose 專用的 loss(包含關鍵點 OKS 損失);第三,推論時 model.predict() 回傳的 Results 物件多了 keypoints 欄位,形狀 (N, 17, 3),最後一維是 (x, y, visibility)。
Ultralytics 8.3 提供的 pose 預訓練權重有 5 個尺寸:yolo11n-pose(nano,2.6M 參數、約 6 MB)、yolo11s-pose(small,9.4M、約 22 MB)、yolo11m-pose(medium、20.1M、約 50 MB)、yolo11l-pose(large、25.3M)、yolo11x-pose(xlarge、56.9M)。預訓練資料集都是 COCO 2017 train 的 person 類(約 64K 影像、262K 人體標註),在 COCO 2017 test-dev 上 yolo11n-pose 可達 mAP@pose50 約 0.80、yolo11s-pose 約 0.86。本篇預設用 yolo11n-pose,理由是它能在 Colab T4 上跑 batch=16、用於即時推論(T4 上 80 FPS)也足夠教學使用。
另一個值得注意的設計是 Ultralytics 對 COCO 17 點拓撲的標準化。Day 26 我們寫了 COCO 17 點與 19 條骨架的清單;Ultralytics 內建了完全相同的清單,並把它寫在 ultralytics/cfg/datasets/coco-pose.yaml 與 ultralytics/utils/plotting.py 的 skeleton 變數。這意味著你用 Ultralytics 推論後呼叫 result.plot() 視覺化,骨架連線與顏色都會跟 COCO 官方工具一致——不需要自己寫繪圖邏輯。如果你用的是自定義關鍵點拓撲(例如手部 21 點或自建資料集),則需要在訓練時用自定義 pose.yaml 設定關鍵點數與骨架,並在推論後自行繪製。
完整實作:從 COCO val2017 到 Ultralytics pose 格式再到訓練
以下範例在 Colab T4 上從下載 COCO 標註到訓練完成約 25 分鐘。流程分四步:先用 shell 指令下載 COCO 2017 val(已含 person_keypoints 標註),再用 Python 把 COCO JSON 轉成 Ultralytics pose 格式(每張影像一個 txt、每行包含 class + 17×3 個關鍵點座標),接著寫出 pose 專用的 pose.yaml,最後呼叫 Ultralytics 8.3 啟動訓練。執行前需要:pip install ultralytics==8.3.0(Colab 預裝版本可能不同,請鎖定 8.3.x)。
# 1. 下載 COCO 2017 val 的影像與姿態標註(約 250 MB + 200 MB,無需註冊)
mkdir -p /content/datasets/coco-pose
cd /content/datasets/coco-pose
if [ ! -d val2017 ]; then
wget -q http://images.cocodataset.org/zips/val2017.zip
unzip -q val2017.zip
fi
if [ ! -f annotations/person_keypoints_val2017.json ]; then
wget -q http://images.cocodataset.org/annotations/annotations_trainval2017.zip
unzip -q annotations_trainval2017.zip
fi
ls val2017 | head -3
ls annotations | head -5
# 輸出:
# 000000000139.jpg
# 000000000285.jpg
# 000000000632.jpg
# annotations/captions_val2017.json
# annotations/person_keypoints_val2017.json
# annotations/person_keypoints_train2017.json
# annotations/instances_val2017.json
# annotations/instances_train2017.json
這段下載 COCO 2017 val 的影像與 person_keypoints 標註。COCO 官方把姿態標註放在獨立的 person_keypoints_*.json 檔案,與 instances_*.json(物件偵測標註)平行存在。person_keypoints_val2017.json 內含約 11K 個人體標註(val2017 共 5,000 張影像),每個人包含 bbox、17 個關鍵點座標、visibility 旗標。我們只用 val2017 是因為下載量小、處理快;正式訓練建議用 train2017(含 64K 影像、262K 標註)。
# 2. 把 COCO JSON 轉成 Ultralytics pose 格式:每張影像一個 txt,每行 class + 17*3
import json
import os
import shutil
from pathlib import Path
from pycocotools.coco import COCO
COCO_ROOT = Path("/content/datasets/coco-pose")
ANN_FILE = COCO_ROOT / "annotations/person_keypoints_val2017.json"
IMG_DIR = COCO_ROOT / "val2017"
OUT_ROOT = Path("/content/datasets/coco-pose-subset")
NUM_KPTS = 17
# 建立輸出目錄
for split in ["train", "val"]:
(OUT_ROOT / "images" / split).mkdir(parents=True, exist_ok=True)
(OUT_ROOT / "labels" / split).mkdir(parents=True, exist_ok=True)
coco = COCO(str(ANN_FILE))
img_ids = coco.getImgIds()
# 篩選有 person 標註的影像,抽 1200 張(1000 train + 200 val)
person_cat_id = next(c["id"] for c in coco.loadCats(coco.getCatIds()) if c["name"] == "person")
imgs_with_person = [iid for iid in img_ids if any(
a["category_id"] == person_cat_id for a in coco.loadAnns(coco.getAnnIds(imgIds=iid))
)]
train_imgs = imgs_with_person[:1000]
val_imgs = imgs_with_person[1000:1200]
print(f"train: {len(train_imgs)} 張、val: {len(val_imgs)} 張")
# 輸出:train: 1000 張、val: 200 張
這段載入 COCO 標註檔、篩選有 person 標註的影像,並切成 1000 張訓練 + 200 張驗證。pycocotools 的 COCO 物件是 COCO 官方提供的 Python API(pip install pycocotools),用 getImgIds 取所有影像 ID、用 loadAnns 取每張影像的所有標註、用 getCatIds 取類別 ID 對應。person 在 COCO 中是類別 ID 1,這裡用 loadCats 反查避免硬編碼。注意我們用 val2017 而不是 train2017——train2017 較大(118K 影像、64K 含 person 的影像),val2017 較小、處理快。
# 3. 把 COCO 標註轉成 Ultralytics pose 格式(class cx cy w h + K*17*3)
def coco_to_ultralytics_pose(image_id, coco, img_dir, label_dir):
"""把一張 COCO 影像的所有 person 標註寫成 Ultralytics pose txt。"""
img_info = coco.loadImgs(image_id)[0]
W, H = img_info["width"], img_info["height"]
fname = img_info["file_name"]
# 複製影像
src = img_dir / fname
dst = label_dir.parent.parent / "images" / label_dir.parent.name / fname
if src.exists():
shutil.copy(src, dst)
ann_ids = coco.getAnnIds(imgIds=image_id, catIds=[person_cat_id])
anns = coco.loadAnns(ann_ids)
lines = []
for ann in anns:
if "keypoints" not in ann or ann.get("iscrowd", 0) == 1:
continue
# bbox 轉 YOLO cxcywh-normalized
x, y, w, h = ann["bbox"]
cx = (x + w / 2) / W
cy = (y + h / 2) / H
bw = w / W
bh = h / H
# keypoints:flat (x1, y1, v1, x2, y2, v2, ..., x17, y17, v17) 也 normalize
kpts = ann["keypoints"] # 長度 51
kpts_norm = []
for i in range(0, len(kpts), 3):
kx = kpts[i] / W
ky = kpts[i + 1] / H
kv = kpts[i + 2]
kpts_norm.extend([f"{kx:.6f}", f"{ky:.6f}", str(int(kv))])
line = f"0 {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f} " + " ".join(kpts_norm)
lines.append(line)
if not lines:
# 沒有 person 就移除複製的影像
if dst.exists():
dst.unlink()
return 0
label_path = label_dir / fname.replace(".jpg", ".txt")
label_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
return len(lines)
# 跑轉檔
for split, ids in [("train", train_imgs), ("val", val_imgs)]:
label_dir = OUT_ROOT / "labels" / split
img_dir_split = OUT_ROOT / "images" / split
total = 0
for iid in ids:
total += coco_to_ultralytics_pose(iid, coco, IMG_DIR, label_dir)
print(f"{split}: {total} 個人體標註、{len(list(img_dir_split.glob('*.jpg')))} 張影像")
# 輸出(實際數字會略有不同):
# train: 2,143 個人體標註、1000 張影像
# val: 412 個人體標註、200 張影像
這段把 COCO 的 keypoints 標註轉成 Ultralytics pose 格式。Ultralytics pose 的標註檔(.txt)每行格式:class_idx cx cy w h x1 y1 v1 x2 y2 v2 ... x17 y17 v17——前 5 欄是 bbox(class + cxcywh normalized)、後 51 欄是 17 個關鍵點(每個點 3 個值:x、y、visibility,x 與 y 已 normalize、visibility 是 0/1/2)。COCO 的原始 keypoints 已經是 flat 51 維串列(順序依 COCO 定義),我們只需要把 x 與 y 除以影像尺寸做 normalize、保留 visibility 整數值。沒有任何 person 標註的影像會被跳過(避免空標註檔)。這個「class + bbox + K×3」格式與 Day 13 的 detection 格式相比,多了 K×3 個關鍵點欄位,其他完全相同。
# 4. 寫出 Ultralytics 需要的 pose.yaml(用絕對路徑)
import yaml
pose_cfg = {
"path": str(OUT_ROOT),
"train": "images/train",
"val": "images/val",
"kpt_shape": [NUM_KPTS, 3], # 17 個關鍵點、每個點 (x, y, visibility)
"names": {0: "person"},
}
pose_yaml_path = OUT_ROOT / "pose.yaml"
with open(pose_yaml_path, "w", encoding="utf-8") as f:
yaml.safe_dump(pose_cfg, f, allow_unicode=True, sort_keys=False)
print(pose_yaml_path.read_text(encoding="utf-8"))
# 輸出:
# path: /content/datasets/coco-pose-subset
# train: images/train
# val: images/val
# kpt_shape:
# - 17
# - 3
# names:
# 0: person
這段寫出 Ultralytics 8.3 需要的 pose 設定檔。pose.yaml 與 detection 的 data.yaml 結構類似,但多了一個 kpt_shape 欄位:[17, 3] 表示 17 個關鍵點、每個點 3 個值 (x, y, visibility)。這個欄位告訴 Ultralytics 在訓練時如何解析 labels/*.txt 的關鍵點部分、在評估時如何計算 OKS。如果你的資料集是手部 21 點,把 kpt_shape 改成 [21, 3] 即可;如果是臉部 468 點,改成 [468, 3]。names 維持 {0: "person"} 因為我們只訓練單類別。
# 5. 載入 yolo11n-pose 預訓練權重(Ultralytics 會自動下載約 6 MB)
from ultralytics import YOLO
model = YOLO("yolo11n-pose.pt")
print(f"模型參數量:{sum(p.numel() for p in model.model.parameters()) / 1e6:.2f} M")
print(f"任務類型:{model.task}")
# 輸出:模型參數量:2.58 M
# 輸出:任務類型:pose
這行程式碼載入 YOLO11 nano 的姿態預訓練權重。YOLO("yolo11n-pose.pt") 與 Day 13 的 YOLO("yolo11n.pt") 類似,但加了 -pose 後綴表示 pose 任務版本。Ultralytics 8.3 會從官方 hub 下載約 6 MB 的權重(已經在 COCO 2017 train 上預訓練過 person 姿態),第一次執行會花 30–60 秒;之後會被快取在 ~/.config/Ultralytics/ 與工作目錄,第二次執行直接從磁碟讀取,整個載入過程不到 0.3 秒。model.task 顯示任務類型是 "pose",Ultralytics 內部會根據這個值切換訓練的 loss 計算與資料解析邏輯。
# 6. 啟動訓練:Ultralytics 8.3 pose 任務的標準 train 呼叫
results = model.train(
data=str(pose_yaml_path),
epochs=50,
imgsz=640,
batch=16,
device=0,
project="runs/pose",
name="coco_pose_yolo11n",
cache="disk",
patience=15,
workers=2,
task="pose", # 明確指定任務(YOLO("yolo11n-pose.pt") 已隱含設定)
)
print("訓練完成,最佳權重路徑:")
print(f" {results.save_dir}/weights/best.pt")
# 輸出:訓練完成,最佳權重路徑:
# 輸出: runs/pose/coco_pose_yolo11n/weights/best.pt
這段啟動訓練。model.train(data=..., task="pose") 是 Ultralytics 8.3 啟動姿態訓練的標準呼叫,與 Day 13 的 detection 訓練幾乎相同,差別只在 task="pose" 與 data 指向的 pose.yaml 含 kpt_shape。Ultralytics 會自動把訓練 loss 換成「pose loss」:bbox 回歸 loss + 類別分類 loss + 17 個關鍵點的 OKS loss(內部用 Day 26 學過的 OKS 公式)。其他參數(epochs=50、imgsz=640、batch=16、cache="disk")都沿用 Day 13 的設定。
# 7. 訓練 log:Ultralytics 8.3 pose 任務的關鍵指標
import pandas as pd
from pathlib import Path
log_path = Path("runs/pose/coco_pose_yolo11n/results.csv")
if log_path.exists():
df = pd.read_csv(log_path)
df.columns = [c.strip() for c in df.columns]
print(df[["epoch", "train/box_loss", "train/pose_loss", "metrics/mAP50(B)", "metrics/mAP50-95(B)"]].tail(5))
# 輸出(實際數字會略有不同):
# epoch train/box_loss train/pose_loss metrics/mAP50(B) metrics/mAP50-95(B)
# 0 46 0.652 0.873 0.821 0.512
# 1 47 0.641 0.861 0.825 0.518
# 2 48 0.635 0.852 0.828 0.522
# 3 49 0.628 0.844 0.832 0.527
# 4 50 0.621 0.836 0.835 0.531
這段讀取訓練 log CSV,印出最後 5 個 epoch 的關鍵指標。train/box_loss 是 bbox 回歸損失、train/pose_loss 是 17 個關鍵點的 OKS 損失(內部用 sigmoid 把誤差映射到 0–1 之間),都越低越好。metrics/mAP50(B) 是在 IoU=0.5 下的物件偵測 mAP、metrics/mAP50-95(B) 是 COCO 標準的 mAP。Ultralytics 8.3 還會另外寫出 metrics/mAP50(P) 與 metrics/mAP50-95(P),這是「pose mAP」——基於 OKS 的姿態估計精度。對於 50 epoch 的小資料集微調,pose mAP50 通常可達 0.85 以上、pose mAP50-95 約 0.50 以上(實際數字會略有不同)。
# 8. 載入 best.pt 做推論與視覺化
best = YOLO("runs/pose/coco_pose_yolo11n/weights/best.pt")
# 在 val 影像上做推論
results = best.predict(
source=str(OUT_ROOT / "images" / "val"),
imgsz=640,
conf=0.25, # 信心門檻
iou=0.7, # NMS IoU 門檻
save=True, # 把預測影像存到 runs/pose/predict/
project="runs/pose",
name="predict",
)
print(f"共推論 {len(results)} 張影像")
# 輸出(實際數字會略有不同):共推論 200 張影像
# 看一張影像的關鍵點形狀
r = results[0]
print(f"影像:{r.path}")
print(f"偵測到 {len(r.boxes)} 個人、{len(r.keypoints)} 組關鍵點")
print(f"keypoints 形狀:{r.keypoints.data.shape}")
print(f" - N={r.keypoints.data.shape[0]}(人數)")
print(f" - K={r.keypoints.data.shape[1]}(關鍵點數)")
print(f" - 3={r.keypoints.data.shape[2]}(x, y, visibility)")
# 輸出(實際數字會略有不同):
# 影像:/content/datasets/coco-pose-subset/images/val/000000000785.jpg
# 偵測到 3 個人、3 組關鍵點
# keypoints 形狀:torch.Size([3, 17, 3])
# - N=3(人數)
# - K=17(關鍵點數)
# - 3=3(x, y, visibility)
這段載入 best.pt 並在 val 影像上做推論。model.predict() 回傳 Results 物件的串列(每張影像一個),每個 Results 包含 boxes(N, 6:x1, y1, x2, y2, conf, cls)、keypoints(N, 17, 3:x, y, visibility)、masks(如果有)。注意 keypoints.data 的形狀是 (N, 17, 3),其中第三維的 visibility 來自 YOLO11 預訓練權重的內部 sigmoid 輸出(連續值 0–1,可用 > 0.5 當可見閾值)。save=True 會把帶有 bbox 與骨架視覺化的預測影像存到 runs/pose/predict/。
# 9. 手動繪製骨架:使用 Day 26 的 COCO 骨架定義
import matplotlib.pyplot as plt
from PIL import Image
import torch
COCO_SKELETON = [
(15, 13), (13, 11), (16, 14), (14, 12),
(11, 12),
(5, 11), (6, 12),
(5, 6),
(5, 7), (7, 9),
(6, 8), (8, 10),
(1, 3), (2, 4),
(0, 1), (0, 2),
(1, 2), (3, 5), (4, 6),
]
EDGE_COLORS = plt.cm.tab20.colors[:len(COCO_SKELETON)]
KPT_COLOR = (0.0, 1.0, 0.0)
# 取第一張 val 影像的預測
img_path = Path(r.path)
img = Image.open(img_path).convert("RGB")
W, H = img.size
fig, ax = plt.subplots(1, 1, figsize=(8, 8))
ax.imshow(img)
ax.axis("off")
kpts_xy = r.keypoints.data[..., :2].cpu().numpy() # (N, 17, 2)
kpts_v = r.keypoints.data[..., 2].cpu().numpy() # (N, 17)
boxes_xyxy = r.boxes.xyxy.cpu().numpy() # (N, 4)
scores = r.boxes.conf.cpu().numpy()
for n in range(len(kpts_xy)):
if scores[n] < 0.5:
continue
# 畫 bbox
x1, y1, x2, y2 = boxes_xyxy[n]
rect = plt.Rectangle((x1, y1), x2 - x1, y2 - y1, linewidth=2,
edgecolor="lime", facecolor="none")
ax.add_patch(rect)
ax.text(x1, y1 - 4, f"person {scores[n]:.2f}", color="lime", fontsize=10)
# 畫骨架
for ei, (i, j) in enumerate(COCO_SKELETON):
if kpts_v[n, i] < 0.5 or kpts_v[n, j] < 0.5:
continue
xi, yi = kpts_xy[n, i]
xj, yj = kpts_xy[n, j]
ax.plot([xi, xj], [yi, yj], color=EDGE_COLORS[ei], linewidth=2)
# 畫關節點
for k in range(NUM_KPTS):
if kpts_v[n, k] < 0.5:
continue
x, y = kpts_xy[n, k]
ax.plot(x, y, "o", color=KPT_COLOR, markersize=5)
ax.set_title(f"YOLO11 Pose:{len(kpts_xy)} 個人")
plt.tight_layout()
plt.savefig("/content/pose_pred.png", dpi=120, bbox_inches="tight")
print("已存 /content/pose_pred.png")
# 輸出:已存 /content/pose_pred.png
這段把預測結果手動繪製到原圖上:綠色框是 bbox、彩色線是骨架(19 條邊用不同顏色)、綠色圓點是可見關節點。我們用 Day 26 學過的 COCO 骨架定義(19 條邊)來決定哪些點要連線,並用 visibility > 0.5 過濾掉「模型認為不可見」的點(例如被遮擋的手腕)。scores[n] < 0.5 的整個人也會被跳過,避免低信心誤判。實務上 Ultralytics 的 result.plot() 內建類似的繪圖邏輯,但自己寫可以完全控制骨架顏色、粗細、要顯示哪些關節點——對於發表論文或產品 UI 很實用。
常見錯誤與踩雷
錯誤一:labels/*.txt 的關鍵點數不是 17 × 3 = 51 個欄位。Ultralytics pose 的標註檔每行必須是「class + bbox + 17 × 3 = 56 個欄位」(如果關鍵點數不是 17,要對應修改 pose.yaml 的 kpt_shape)。如果你漏寫幾個關鍵點、或 visibility 用浮點數而不是 0/1/2 整數,Ultralytics 會在訓練時拋出 ValueError: not enough values to unpack。對應排查方向:用 awk '{print NF}' labels/train/*.txt | sort -u 確認所有標註檔的欄位數都是 56。
錯誤二:visibility 用 0/1 標記而不是 COCO 的 0/1/2。Ultralytics 沿用 COCO 的 visibility 三值定義:v=0 未標註、v=1 標註但被遮擋、v=2 標註且可見。如果你把 visibility 都標成 0/1(只標可見/不可見),模型仍能訓練,但 OKS 評估會把「被遮擋的關節」也當成可見點計入,這對密集人群資料會有偏差。對應排查方向:標註階段就把 visibility 三個等級分清楚,特別注意「被遮擋但可推論位置」的關節要標 v=1。
錯誤三:pose.yaml 的 kpt_shape 與實際資料不一致。如果你下載的是 COCO 2017 train 的 17 點,kpt_shape 寫 [17, 3];但如果你想用自己的 12 點子集(去掉耳朵、加上腳尖),kpt_shape 也要改成 [12, 3],否則訓練會在解析標註時直接報欄位數不對的錯誤。對應排查方向:在標註檔生成的 Python 腳本裡寫一個 assert kpt_shape == [NUM_KPTS, 3],確保兩個地方一致。
錯誤四:把 detection 的 data.yaml 直接改名為 pose.yaml 用。detection 的 data.yaml 沒有 kpt_shape 欄位,Ultralytics 訓練會把它當成 0 個關鍵點的「特殊 detection」,model.task 仍會顯示 "detect" 而不是 "pose"。對應排查方向:寫獨立的 pose.yaml,並在 model.train() 呼叫時加 task="pose" 強制覆寫(即使你用 YOLO("yolo11n-pose.pt") 載入 pose 權重,Ultralytics 也會先讀 yaml 內容推斷任務)。
錯誤五:推論時 keypoints.data 的 visibility 是連續值。Ultralytics pose 的預訓練權重輸出的是「經過 sigmoid 的 visibility 信心」,不是 0/1/2 的整數。如果你直接把連續值當布林用,會把所有點都視為可見,視覺化會出現一堆不該連的線。對應排查方向:用 kpts_v > 0.5 過濾,或用 (kpts_v > 0.3) | (kpts_v > 0.7) 之類的分層門檻(更細緻的視覺化)。
效能與實務提醒
在 Colab T4 上跑 yolo11n-pose + COCO 姿態子集(1000 張訓練影像、640×640、batch=16),50 epoch 約 18 分鐘,平均每 epoch 約 22 秒。換成 yolo11s-pose 約 30 分鐘、yolo11m-pose 約 60 分鐘。訓練完成後 best.pt 約 6 MB,可直接部署。推論速度在 T4 上單張 640×640 影像約 12 ms(80 FPS),足以處理即時 webcam 串流(30 FPS 還有 2.5 倍餘裕)。CPU 推論用 ONNX Runtime 約 60 FPS(Apple M1)或 30 FPS(Intel i7),仍可用於即時應用。
另一個工程上的小建議:model.predict() 的 conf 參數預設是 0.25,對於大多數場景是合適的門檻;如果你推論的場景背景簡單(例如運動場景、人形佈景),可以調低到 0.15 保留更多候選;如果是密集場景(例如演唱會觀眾),可以調高到 0.4 減少誤判。iou 參數(用於 NMS)預設是 0.7,密集人群建議調到 0.5(讓 NMS 更積極合併重疊框)。
資料增強方面,Ultralytics 8.3 pose 任務沿用 detection 的增強策略:mosaic、mixup、HSV 抖動、random flip。flip 對姿態估計有個小細節——左右關節的索引也要跟著翻(鏡像後原本的左手變成右手),Ultralytics 內部已經處理好。如果你訓練自己的資料集,記得在資料增強階段也要把關鍵點的可見度、bbox 同步翻轉;自寫 dataset 時要特別小心。下一篇文章 Day 29 我們會換到完全不同的姿態工作流——MediaPipe 0.10 的 mp.solutions.hands、face_mesh、holistic,用零樣本 API 直接從 webcam 抓 21 個手部關鍵點、468 個臉部點、33 個全身點。
小結
今天把 Day 26 的 OKS 與 Day 27 的路線比較落到實作:用 Ultralytics 8.3 的 yolo11n-pose.pt 在 COCO 2017 val 的 person 子集(1000 張訓練 + 200 張驗證)上微調,整個流程從下載 COCO 標註、轉成 Ultralytics pose 格式、寫 pose.yaml、訓練 50 epoch、到推論與骨架視覺化,約 25 分鐘。重點回顧:第一,Ultralytics pose 的標註格式是「class + bbox + K × 3」共 56 欄(17 點設定);第二,pose.yaml 必須含 kpt_shape 欄位;第三,訓練 loss 包含 bbox loss + 類別 loss + OKS-based pose loss;第四,推論時 Results.keypoints 形狀是 (N, 17, 3),其中 visibility 是連續信心值;第五,用 Day 26 的 COCO 骨架定義可以自己畫出完整的關節與連線。明天我們換到 MediaPipe 0.10——它不需要訓練、只要呼叫 API 就能從 webcam 抓關鍵點,是零樣本姿態估計的代表工具。
結語
今天的重點是「把 Ultralytics 8.3 的 pose 訓練管線完整跑一遍」。我們從 COCO 2017 val 的下載開始、用 Python 把 person_keypoints_val2017.json 轉成 Ultralytics 標準 pose 格式(56 欄)、寫出 pose.yaml、載入 yolo11n-pose.pt、啟動 50 epoch 訓練;接著讀取 results.csv 看 loss 與 mAP 收斂情況、載入 best.pt 做推論、解析 Results.keypoints 結構、用手寫的 matplotlib 程式碼把 bbox、19 條骨架、17 個關節點全部視覺化到原圖上。讀完這篇你應該能回答:Ultralytics pose 的標註檔每行有幾個欄位?pose.yaml 比 data.yaml 多哪個欄位?Results.keypoints.data 的形狀是什麼?visibility 是離散還是連續值?
Ultralytics pose 與 MediaPipe 是兩條平行的姿態工作流:前者要訓練、需要自備資料集、但可以完全控制拓撲與類別;後者零樣本、不需訓練、但只能用 MediaPipe 預定義的拓撲(手部 21 點、臉部 468 點、全身 33 點)。實務上「需要自定義關節或領域微調」用 Ultralytics,「快速驗證概念或邊緣裝置部署」用 MediaPipe。兩條路線都會繼續發展,跨年後(2025 年起)也有各自的更新,但 2024 年底的版本已經足夠涵蓋本系列所有的姿態實戰。明天,我們會用 MediaPipe 0.10 在本地 CPU 上從 webcam 抓 21 個手部關鍵點、468 個臉部點、33 個全身點,零訓練、零 GPU、即時執行。
延伸資源
- Ultralytics Pose 官方文件(8.3.x,2024):
https://docs.ultralytics.com/tasks/pose/,YOLO("yolo11n-pose.pt")、model.train(task="pose")、model.predict()的完整 API 與 pose 資料格式說明。 - MS COCO Keypoints 官方文件(2017):
https://cocodataset.org/#keypoints-eval,17 點拓撲、visibility 三值、OKS 公式的原始來源。 - Ultralytics 姿態資料格式文件(8.3.x,2024):
https://docs.ultralytics.com/datasets/pose/,pose.yaml的kpt_shape、labels/*.txt 的欄位定義與自定義關鍵點範例。 - pycocotools 原始碼(2024):
https://github.com/cocodataset/cocoapi,COCO物件的loadAnns、getImgIds等 API 與 COCO JSON 格式說明。 - Ultralytics GitHub Releases(2024 年 9 月):
https://github.com/ultralytics/ultralytics/releases,YOLO11 pose 權重釋出說明與 COCO test-dev 上的 mAP 對照表。
留言
張貼留言