跳到主要內容

CV Day 28 YOLO-Pose 實戰:訓練與推論

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 對照表。

留言

這個網誌中的熱門文章

Day 2 變數與資料型別

Day 2 變數與資料型別 引言 寫程式的過程中,變數與資料型別是處理資料的基礎。變數是存放資料的容器,資料型別則決定這筆資料有哪些特性、可以進行哪些操作。學會定義變數、認識各種資料型別,是學好 Python 的關鍵一步。 這篇文章會帶你了解 Python 中變數的觀念、如何定義變數,以及常見的資料型別,包括整數、浮點數、字串、布林值,還有串列、元組、字典與集合等容器型別。我們也會介紹變數的命名規則與撰寫風格建議,以及如何用 type() 檢查資料型別。 什麼是變數?如何在 Python 中定義變數 變數是在程式執行時用來存放資料的名稱。透過定義變數,我們可以給一筆資料一個名字,並在程式的其他地方用這個名字取用該筆資料。在 Python 中,變數不需要事先宣告型別,因為 Python 是動態型別語言,變數的型別由指定給它的值決定。 定義變數的基本語法 在 Python 中定義變數非常簡單,只要用賦值符號 = 把值指定給變數即可。例如: x = 5 # 定義變數 x,並把整數 5 賦值給它 name = "Alice" # 定義變數 name,並把字串 "Alice" 賦值給它 在這裡,x 是一個變數,被賦予整數 5;name 是另一個變數,被賦予字串 "Alice"。 變數的更新與覆寫 變數的值可以修改,也就是說,我們可以在程式的不同地方給同一個變數新的值。例如: x = 10 # x 最初被賦予 10 x = 15 # x 的值現在被更新為 15 這樣就能依照需求,在程式執行過程中靈活調整變數的值。 Python 的動態型別系統 Python 和某些靜態型別語言不同,定義變數時不需要宣告型別。賦值時,Python 會根據值自動判斷變數的型別。例如: x = 5 # x 是整數 x = 3.14 # x 變成浮點數 x = "Hi" # x 變成字串 同一個變數在程式執行過程中可以存放不同型別的值,這是 Python 的彈性之一。 常見資料型別 在 Python 中,資料型別決定我們可以對變數進行哪些操作...

Day 1 Python 簡介與環境設定

Day 1 Python 簡介與環境設定 引言 在現在的科技環境裡,程式設計已經是一項重要技能。無論你是對資料科學有興趣、想成為開發者,或是想踏入人工智慧(AI)領域,學會寫程式都能明顯提升你的競爭力。在眾多程式語言中,Python 因為語法簡單、功能強大、應用範圍廣泛,成為許多人進入程式世界的第一選擇。這篇文章會帶你認識 Python 的背景與優勢,並一步步教你在不同系統上安裝與設定 Python 開發環境,最後寫出第一支 Python 程式。 為什麼選擇 Python? Python 是一種高階程式語言,由 Guido van Rossum 在 1991 年發布。Python 的設計哲學強調程式碼的可讀性,並用縮排來定義程式區塊,這點和許多使用大括號的語言不同。簡潔的語法讓它成為初學者的理想選擇;就算是經驗豐富的開發者,也能用它完成複雜的專案。 Python 的優勢如下: 簡單易學 :Python 的語法清楚、結構簡潔,初學者很快就能上手。和其他語言相比,學習曲線相對平緩,不需要先弄懂一堆複雜觀念,就能開始寫程式。 應用範圍廣泛 :從資料科學、網頁開發、人工智慧、機器學習、自動化測試到網路爬蟲,Python 都有大量開源函式庫與工具支援,而且在這些領域都扮演關鍵角色。 豐富的函式庫與框架 :Python 的函式庫生態系非常龐大。做資料分析有 NumPy、Pandas;開發網站有 Django、Flask;做深度學習有 TensorFlow、PyTorch。各種需求幾乎都能找到對應的套件,讓開發更有效率。 跨平台支援 :Python 支援 Windows、macOS、Linux 等作業系統,程式通常不需要太多修改就能跨平台執行,讓開發與部署更有彈性。 活躍的社群 :Python 擁有龐大的開發者社群。學習或開發上遇到問題,幾乎都能在社群與論壇(例如 Stack Overflow)找到答案,對初學者來說是很強的後盾,也能減少卡關時的挫折感。 Python 的應用領域 Python 的流行與強大功能,讓許多領域都開始大量使用它。以下是幾個常見的應用方向: 資料科學 :隨著大數據與人工智慧興起,資料科學大量使用 Python。NumPy、Pandas 與 Matplotlib 等工具能處理和分析龐...

Python 從入門到 PyTorch 深度學習:開啟 AI 世界的大門

Python 從入門到 PyTorch 深度學習:開啟 AI 世界的大門 隨著人工智慧(AI)與深度學習(Deep Learning)快速發展,越來越多人對這些技術產生興趣。不論你是想踏入 AI 領域的初學者,還是已經有程式基礎的開發者,學好 Python 與深度學習框架(例如 PyTorch),都能為你打開更多可能。 為什麼選擇 Python? Python 已經是資料科學與人工智慧領域的首選語言。它的語法簡潔、容易上手,而且擁有龐大的生態系與大量開源函式庫。無論是資料處理、資料視覺化,還是建立機器學習與深度學習模型,Python 都能勝任。對想進入 AI 或資料科學領域的人來說,它幾乎是必備工具。 PyTorch 是什麼? PyTorch 是由 Meta(原 Facebook)AI 研究團隊開發的開源深度學習框架,以易用、靈活和動態計算圖著稱,是許多 AI 研究人員與開發者的首選。相較於其他框架,PyTorch 的寫法更貼近原生 Python,對初學者相對友善。無論是簡單的實驗,還是複雜的深度學習模型,PyTorch 都能提供強大的支援。 這個系列能帶給你什麼? 這個系列會從 Python 的基礎開始,帶你一步一步學習,最後能自己用 PyTorch 建立深度學習模型。即使你完全沒有寫過程式,也能跟著文章的節奏累積技能,理解 AI 與深度學習的核心觀念。 本系列涵蓋的主題 Python 基礎:從變數、條件判斷到函式與模組。 資料處理工具:用 NumPy 與 Pandas 有效率地操作資料。 資料視覺化:用 Matplotlib 與 Seaborn 把資料畫成圖表。 深度學習的數學基礎:線性代數、微積分與機率。 PyTorch 入門:理解張量、模型建構與 GPU 加速。 基礎深度學習模型:CNN 與 RNN 的實作應用。 深度學習專案實戰:從資料前處理到模型部署的端到端流程。 誰適合這個系列? 程式初學者 :如果你對 AI 充滿好奇,卻還沒寫過程式,系列的第一部分會帶你快速上手 Python,並幫助你理解深度學習的基本觀念。 資料科學愛好者 :如果你已經熟悉一些資料處理方法,進階部分會教你如何用 PyTorch 建構深度學習模型。 開發者與研究人員 :想更深入了...