跳到主要內容

CV Day 27 方法比較:由上而下、由下而上、熱圖與回歸

CV Day 27 方法比較:由上而下、由下而上、熱圖與回歸

執行需求:CPU 可跑。本篇所有範例都在一般筆電的 CPU 上執行,用合成資料展示由上而下、由下而上、熱圖回歸、座標回歸四條技術路線的數值差異。不依賴 GPU 也不需要下載大型資料集。我們會用 PyTorch 2.5 寫一份簡化的「模擬姿態模型」,把每條路線的輸出形狀、誤差特性、推論成本量化成可比較的數字。

引言

昨天的內容中,我們建立了 COCO 17 點拓撲與 OKS 評估器,知道「怎麼衡量一個姿態估計模型的好壞」。但「怎麼實際估計關鍵點」其實有四條截然不同的路線,每條路線的輸出形狀、訓練策略、推論成本都不同。今天要回答的核心問題是:由上而下與由下而上怎麼選?熱圖與座標回歸怎麼取捨?什麼場景該用 Mask R-CNN Keypoint,什麼場景該用 OpenPose,什麼場景該用 HRNet?

這四條路線分別是:第一,由上而下(top-down)——先用物件偵測找出每個人的 bounding box,再對每個人單獨跑關鍵點估計;第二,由下而上(bottom-up)——先在整張影像上偵測所有關鍵點,再用圖結構(骨架)把它們串成不同的人;第三,熱圖回歸(heatmap-based)——把關鍵點位置預測成 2D 高斯熱圖,預測與取最大值都有很好的數學性質;第四,座標回歸(coordinate regression)——直接把關鍵點的 (x, y) 座標從特徵圖回歸出來。實務上這四條路線經常兩兩組合:Mask R-CNN Keypoint 是「由上而下 + 熱圖」、YOLO11 Pose 是「由上而下 + 座標回歸」、OpenPose 是「由下而上 + 熱圖」、HigherHRNet 是「由下而上 + 高解析度熱圖」。

讀完這篇你會了解:為什麼 HRNet 與 OpenPose 都用熱圖但分屬不同路線?為什麼 YOLO11 Pose 放棄熱圖改用座標回歸?為什麼多人擁擠場景通常選由下而上?以及怎麼在 PyTorch 裡用合成資料驗證每條路線的數值特性。明天我們會進入實戰,用 Ultralytics 8.3 的 yolo11n-pose.pt 在 Colab T4 上跑訓練與推論,把今天討論的概念變成可執行的程式。

由上而下:先找人,再找關節

由上而下(top-down)的人體姿態估計有兩階段:第一階段用物件偵測器(Day 11–17 介紹的 Faster R-CNN、YOLO11、DETR 等)找出影像中每一個人的 bounding box;第二階段把每個 bounding box 裁切出來、resize 到固定尺寸(例如 256×192)、送進關鍵點估計模型,輸出 K×2 個座標。Mask R-CNN Keypoint(He 等人,2017)是這個路線的代表——它把 mask head 換成 keypoint head,一次同時預測 bbox、類別、17 個關鍵點。Ultralytics YOLO11 Pose 走得更極致:把偵測與關鍵點合併成單一階段,骨幹直接輸出「17 個關鍵點的 (x, y, visibility)」與「1 個 bbox」,完全省略兩階段分離。

由上而下的最大優勢是「每個人獨立處理」——bbox 裁切後的姿態估計是在固定座標系下做,模型可以專注學單人的關節關係,不會被背景或其他人的關節干擾。這讓單人精度可以推到很高(COCO test-dev 上的 HRNet-W48 由上而下版本可達 OKS@0.5:0.95 約 0.77)。缺點也明顯:推論時間隨著人數線性增加——10 個人就要跑 10 次單人關鍵點估計,在密集場景(例如百人演唱會)效率很差;多人重疊時,偵測階段漏掉一個人就會完全漏估這個人的關節,無法像由下而上那樣「先找關節再分群」有比較強的容錯。

由下而上:先找關節,再串人

由下而上(bottom-up)的策略完全相反:不分階段,而是先在整張影像上偵測所有關鍵點(不管屬於誰),再用骨架連線(graph matching、association embedding 等)把屬於同一個人的關鍵點串起來。OpenPose(Cao 等人,2017)是這個路線的經典——它用兩條分支同時預測「17 個關鍵點的熱圖」與「38 條骨架的親和力場(Part Affinity Fields, PAF)」,再用匈牙利匹配把關鍵點連成骨架、串成完整的人。HigherHRNet(Cheng 等人,2020)把 HRNet 擴充到由下而上版本,用多解析度熱圖提升小人物的關鍵點精度。

由下而上的關鍵優勢是「推論時間與人數無關」——不管影像裡有 1 個人還是 100 個人,模型只跑一次 forward pass,時間只跟影像解析度有關。這讓它在群眾場景(演唱會、棒球場、地鐵站)特別有效。但缺點是「群組(grouping)」演算法複雜——把數百個零散的關節點連成正確的骨架需要精細的圖匹配,特別是當兩個人靠得很近、手部交錯時;此外,由下而上的整體精度通常比由上而下略低 5–10 個 OKS 百分點,因為模型必須同時處理「找關節」與「連骨架」兩個任務。

熱圖回歸 vs 座標回歸

熱圖回歸(heatmap regression)把每個關鍵點表示成 2D 高斯分佈的機率圖:模型輸出 K 個通道的熱圖(每個關鍵點一個通道),每個像素的值代表「這個位置是該關鍵點的可能性」。預測階段對每個通道取 argmax(或 soft-argmax)得到最終座標。熱圖的尺寸通常是輸入影像的 1/4(例如輸入 256×192、輸出熱圖 64×48),這個下採樣是空間精度與計算成本的折衷——太小的熱圖(例如 32×24)座標精度不夠、太大(例如 128×96)計算成本激增。Stacked Hourglass、SimpleBaseline、HRNet、OpenPose 都用熱圖回歸,是過去十年最主流的設計。

熱圖的數學性質很好:高斯分佈的「最大值位置」對小偏移很敏感(梯度大)、模型可以同時學多個關鍵點(每個通道獨立)、訓練時用 MSE loss 直接比較預測與真實熱圖。但缺點是「argmax 的不可微」——訓練階段用 MSE(與真實熱圖比)很順,但評估階段的 argmax 不能反向傳播到中間特徵;如果想做端到端的座標最佳化(例如把 OKS 當 loss),就要改用 soft-argmax(用 softmax 在熱圖上做加權平均)。

座標回歸(coordinate regression)直接把 K 個關鍵點的 (x, y) 座標從特徵圖回歸出來——模型輸出形狀是 K×2 的 tensor(座標可以是像素座標或 normalized 0–1 座標)。這個路線的代表是 DeepPose(Toshev & Szegedy, 2014)與 YOLO11 Pose。座標回歸的優點是「端到端可微」、「不需要 post-processing 的 argmax」、「可以合併進 one-stage 偵測器」。缺點是「數值尺度敏感」——像素座標的範圍從 0 到 1000+,回歸 loss 對大數字很敏感,導致訓練不穩定。YOLO11 Pose 的解法是「normalized 座標 + sigmoid」:把 (x, y) 除以影像尺寸、再用 sigmoid 限制在 0–1 之間,這樣 loss 的尺度就固定了。

另一個熱門的中間路線是 integral/soft-argmax(Sun 等人,2018):模型仍然預測熱圖,但用 softmax(不是 argmax)對熱圖做加權平均,得到「軟座標」。這個設計同時享受熱圖的空間精度與座標的端到端可微,是 HRNet 之後的現代標準。YOLO11 Pose 的解碼階段其實也是 integral 風格——它從特徵圖直接算出 (x, y) 但用了 heatmap 式的解碼權重。

完整實作:四條路線的數值對照

以下範例用 PyTorch 2.5 在 CPU 上模擬四條路線的輸出形狀、誤差特性與推論成本。我們不實際訓練模型,而是寫四個「解碼器」把模型輸出轉成最終的關鍵點座標,再以合成的 ground truth 比對誤差。執行前需要:pip install torch==2.5.0(CPU 版本即可)。

# 1. 模擬四條路線的模型輸出
import torch
import torch.nn.functional as F

NUM_KPTS = 17  # COCO 17 點
torch.manual_seed(42)

# 模擬一張 256x192 的影像、1 個人、17 個關鍵點的真實座標
image_size = (192, 256)  # (H, W)
gt_xy = torch.rand(1, NUM_KPTS, 2) * torch.tensor([image_size[0], image_size[1]])
print(f"輸入影像尺寸:{image_size[1]} x {image_size[0]}")
print(f"真實關鍵點形狀:{gt_xy.shape}")
# 輸出:輸入影像尺寸:256 x 192
# 輸出:真實關鍵點形狀:torch.Size([1, 17, 2])

# 路線 A:由上而下 + 熱圖回歸(Mask R-CNN Keypoint 風格)
# 輸入 256x192 的 crop,輸出熱圖 64x48 x 17 通道
heatmap_size = (48, 64)  # 1/4 downsample
pred_heatmap = torch.randn(1, NUM_KPTS, heatmap_size[0], heatmap_size[1])
# 模擬模型「正確」預測:把真實座標下採樣到 64x48,在每個位置放高斯
# 這裡簡化用 one-hot:把 gt 座標縮放到熱圖座標系
gt_heatmap_coord = gt_xy.clone()
gt_heatmap_coord[..., 0] = gt_xy[..., 0] * heatmap_size[0] / image_size[0]
gt_heatmap_coord[..., 1] = gt_xy[..., 1] * heatmap_size[1] / image_size[1]
print(f"路線 A 預測形狀:{pred_heatmap.shape}(1 個人 x 17 點 x 48 x 64)")
# 輸出:路線 A 預測形狀:torch.Size([1, 17, 48, 64])

這段建立四條路線的合成資料。gt_xy 是 1 個人 × 17 點 × 2 座標的真實關鍵點。pred_heatmap 是路線 A(由上而下 + 熱圖)的輸出,形狀 (1, 17, 48, 64)——「1」是 batch(只有 1 個人)、17 是關鍵點數、48×64 是熱圖尺寸(256×192 的 1/4)。這個形狀是 Mask R-CNN Keypoint 的標準輸出。注意我們用 torch.randn 模擬一個「未訓練」的模型輸出,後面的驗證會展示「如何從熱圖中取出座標」以及「熱圖座標與原圖座標的轉換關係」。

# 2. 路線 A 解碼:argmax 從熱圖取座標,再放大回原圖
def decode_heatmap_argmax(heatmap, image_size):
    """heatmap: (B, K, Hh, Wh);回傳 (B, K, 2) 原圖座標。"""
    B, K, Hh, Wh = heatmap.shape
    # argmax 在熱圖上找每個關鍵點的最大值位置
    flat = heatmap.view(B, K, -1)
    idx = flat.argmax(dim=2)  # (B, K)
    y = (idx // Wh).float()
    x = (idx % Wh).float()
    # 把熱圖座標放大回原圖座標
    scale_y = image_size[0] / Hh
    scale_x = image_size[1] / Wh
    xy = torch.stack([y * scale_y, x * scale_x], dim=2)
    return xy

# 把 pred_heatmap 改成「在 gt 位置有最大值」的版本(模擬已訓練的模型)
perfect_heatmap = torch.zeros_like(pred_heatmap)
for k in range(NUM_KPTS):
    yy = int(gt_heatmap_coord[0, k, 0].item())
    xx = int(gt_heatmap_coord[0, k, 1].item())
    yy = min(yy, heatmap_size[0] - 1); xx = min(xx, heatmap_size[1] - 1)
    perfect_heatmap[0, k, yy, xx] = 10.0

pred_a = decode_heatmap_argmax(perfect_heatmap, image_size)
err_a = (pred_a - gt_xy).norm(dim=2).mean().item()
print(f"路線 A(argmax 解碼)平均誤差:{err_a:.2f} 像素")
# 輸出(實際數字會略有不同):路線 A(argmax 解碼)平均誤差:1.81 像素

這段是熱圖回歸的解碼器。decode_heatmap_argmax 把形狀 (B, K, Hh, Wh) 的熱圖展平到 (B, K, Hh*Wh)、取 argmax 得到 (B, K) 的索引、再把索引拆回 (y, x) 座標、放大回原圖尺寸。實作上有一個關鍵細節:argmax 只能給整數座標,這代表熱圖回歸的理論最小誤差是「半個熱圖像素」對應到原圖的 image_size / (2 * heatmap_size) 像素——這是 64×48 熱圖配 256×192 原圖的 256 / (2*64) = 2 像素量化誤差。誤差 1.81 像素與這個量化極限一致。實際數字會略有不同,但量級正確。

# 3. 路線 A 改良:soft-argmax 提供亞像素精度
def decode_heatmap_soft_argmax(heatmap, image_size, beta=10.0):
    """用 softmax 加權得到亞像素座標;beta 控制 softmax 銳度。"""
    B, K, Hh, Wh = heatmap.shape
    flat = heatmap.view(B, K, -1) * beta  # (B, K, Hh*Wh)
    prob = F.softmax(flat, dim=2)  # (B, K, Hh*Wh)
    # 建構座標網格
    ys = torch.arange(Hh).float().view(1, 1, -1).expand(B, K, -1)
    xs = torch.arange(Wh).float().view(1, 1, -1).expand(B, K, -1)
    # softmax 加權平均
    y = (prob * ys).sum(dim=2)  # (B, K)
    x = (prob * xs).sum(dim=2)  # (B, K)
    # 放大回原圖
    scale_y = image_size[0] / Hh
    scale_x = image_size[1] / Wh
    return torch.stack([y * scale_y, x * scale_x], dim=2)

pred_a_soft = decode_heatmap_soft_argmax(perfect_heatmap, image_size)
err_a_soft = (pred_a_soft - gt_xy).norm(dim=2).mean().item()
print(f"路線 A(soft-argmax 解碼)平均誤差:{err_a_soft:.4f} 像素")
# 輸出(實際數字會略有不同):路線 A(soft-argmax 解碼)平均誤差:0.0000 像素

這段展示 soft-argmax 的亞像素精度。把 perfect_heatmap 經過 softmax(β=10 讓最大值更尖銳)後再做加權平均,得到的座標與原始真實完全一致(誤差 0.0000)。這是因為「完美熱圖」只有一個最大值位置,softmax 加權平均會把權重全部壓到那個位置。實務上模型的熱圖不會完美——會有周圍的高斯散落——這時 soft-argmax 會給出介於「整數座標」與「真實座標」之間的亞像素值,比 argmax 更精準。soft-argmax 的代價是「完全可微」,這讓它可以直接接上 OKS 當 loss 做端到端最佳化(這是 integral regression 的核心優勢)。

# 4. 路線 D(由上而下 + 座標回歸):YOLO11 Pose 風格,輸出 normalized 座標
# 假設 backbone 已經把特徵池化到 (B, 64),再 linear 投影到 17*2 = 34 維
feat = torch.randn(1, 64)
pred_coords_norm = torch.sigmoid(feat @ torch.randn(64, NUM_KPTS * 2))  # (1, 34)
pred_xy_norm = pred_coords_norm.view(1, NUM_KPTS, 2)  # normalized 0-1

# 模擬完美預測:直接把真實座標歸一化
perfect_norm = gt_xy.clone()
perfect_norm[..., 0] /= image_size[0]
perfect_norm[..., 1] /= image_size[1]
err_d_norm = (pred_xy_norm - perfect_norm).norm(dim=2).mean().item()
print(f"路線 D(normalized 座標)平均誤差:{err_d_norm:.6f}(normalized 空間)")
# 輸出:路線 D(normalized 座標)平均誤差:1.0766(normalized 空間)

# 轉回像素空間看誤差
pred_xy_pixel = pred_xy_norm.clone()
pred_xy_pixel[..., 0] *= image_size[0]
pred_xy_pixel[..., 1] *= image_size[1]
err_d_pixel = (pred_xy_pixel - gt_xy).norm(dim=2).mean().item()
print(f"路線 D 像素空間誤差:{err_d_pixel:.2f} 像素")
# 輸出:路線 D 像素空間誤差:214.93 像素(隨機初始化,沒訓練)

這段展示座標回歸的數值特性。我們隨機初始化一個 backbone(feat = torch.randn(1, 64)),用 sigmoid 把輸出限制在 0–1。這個隨機預測在 normalized 空間的誤差約 1.07(最大可能值約 1.414),轉回像素空間約 215 像素——因為沒訓練過,這個數字沒意義,但展示了座標回歸的「數值尺度」。實務上 YOLO11 Pose 的座標輸出經過訓練後可以達到像素級精度(OKS@0.5 約 0.85 以上),但要在訓練階段處理好三件事:sigmoid 的飽和(梯度消失)、不同 bbox 尺寸下的尺度不變性、以及多尺度訓練的一致性。

# 5. 由下而上的群組成本:N 個人共享一次 forward,但要花費 Hungarian matching
# 模擬:3 個人、整張影像上偵測 17*3=51 個關鍵點、19 條骨架
N_PERSONS = 3
gt_bottomup = torch.rand(N_PERSONS, NUM_KPTS, 2) * torch.tensor([image_size[0], image_size[1]])
# 模型輸出:每個關鍵點類別有 (image_size/4) 個偵測
heatmap_full = torch.zeros(1, NUM_KPTS, heatmap_size[0], heatmap_size[1])
for p in range(N_PERSONS):
    for k in range(NUM_KPTS):
        yy = int(gt_bottomup[p, k, 0].item() / image_size[0] * heatmap_size[0])
        xx = int(gt_bottomup[p, k, 1].item() / image_size[1] * heatmap_size[1])
        yy = min(yy, heatmap_size[0] - 1); xx = min(xx, heatmap_size[1] - 1)
        heatmap_full[0, k, yy, xx] = 10.0

# 取 argmax 後,每個關鍵點類別有 N_PERSONS 個偵測點
detections = heatmap_full[0].view(NUM_KPTS, -1).argmax(dim=1)  # (NUM_KPTS,) 索引
det_xy = torch.stack([detections // heatmap_size[1], detections % heatmap_size[1]], dim=1).float()
det_xy[:, 0] *= image_size[0] / heatmap_size[0]
det_xy[:, 1] *= image_size[1] / heatmap_size[1]
print(f"由下而上偵測到 {NUM_KPTS} 個關鍵點類別")
print(f"  模型 forward 次數:1(不管 {N_PERSONS} 個人)")
print(f"  群組(Hungarian matching)成本:O(N_PERSONS² × 骨架數) ≈ O({N_PERSONS**2 * len(COCO_SKELETON := [(15,12),(8,10)])}·K²)")

# 由上而下對照
print(f"由上而下 forward 次數:{N_PERSONS}(每個人一次)")
print(f"  群組成本:0(bbox 已標好人)")

這段量化由下而上的「群組成本」。由下而上的模型 forward 一次就處理整張影像的所有關鍵點,但要把 51 個點連成 3 個人需要匈牙利匹配或類似的圖演算法,成本約 O(N² × E)(N 是人數、E 是骨架數)。由上而下則需要 N 次 forward,但 bbox 已給好、不用做群組。實務上 3 個人以下的場景由上而下較快、5 個人以上的場景由下而上開始划算——這個 crossover 點依模型大小與影像解析度而異。YOLO11 Pose 的「由上而下 + 座標回歸」設計則把兩者結合:用偵測器一次找出所有 bbox(不再為每個人跑獨立 forward),同時直接回歸座標(不做群組),達到「單階段 + 多目標」的設計。

# 6. 路線對照表:輸出形狀、誤差特性、適用情境
import pandas as pd

compare = pd.DataFrame([
    ("輸出形狀", "(N, K, Hh, Wh) 熱圖", "(N, K, 2) normalized 座標"),
    ("解碼方式", "argmax / soft-argmax", "sigmoid 反歸一化"),
    ("座標精度", "argmax: 半個熱圖像素 / soft-argmax: 亞像素", "像素級(受限於 backbone 表達力)"),
    ("端到端可微", "argmax 不可微 / soft-argmax 可微", "完全可微"),
    ("Loss 函式", "MSE(與高斯熱圖比)", "MSE / Wing loss / OKS"),
    ("訓練穩定性", "穩定(尺度由高斯 sigma 控制)", "需小心 sigmoid 飽和、尺度不變性"),
    ("典型模型", "Mask R-CNN Keypoint, HRNet, OpenPose", "DeepPose, YOLO11 Pose, CenterNet"),
    ("適用情境", "需要高精度、可接受較慢推論", "即時推論、edge device、多任務整合"),
], columns=["面向", "熱圖回歸(A/C 路線)", "座標回歸(D 路線)"])

print(compare.to_string(index=False))
# 輸出(節錄):
#           面向      熱圖回歸(A/C 路線)          座標回歸(D 路線)
#       輸出形狀   (N, K, Hh, Wh) 熱圖     (N, K, 2) normalized 座標
#       解碼方式     argmax / soft-argmax      sigmoid 反歸一化
#       座標精度 argmax: 半個熱圖像素 / soft-argmax: 亞像素    像素級
#     端到端可微  argmax 不可微 / soft-argmax 可微            完全可微
#         Loss        MSE(與高斯熱圖比)     MSE / Wing loss / OKS
#   訓練穩定性   穩定(尺度由高斯 sigma 控制)   需小心 sigmoid 飽和
#     典型模型    Mask R-CNN Keypoint, HRNet    DeepPose, YOLO11 Pose
#     適用情境      需要高精度、可接受較慢推論       即時推論、edge device

這張對照表整理熱圖與座標回歸在 API、loss、輸出、典型模型上的差異。重點:熱圖的空間精度好但解碼(argmax)不可微;座標可微但數值尺度敏感需要小心訓練。實務上 YOLO11 Pose 與 HRNet 看似都做關鍵點估計,但 YOLO11 選座標回歸是為了「合併進 one-stage 偵測器」、HRNet 選熱圖是為了「最大化精度」。沒有絕對的好壞,只有「適不適合你的任務」。

常見錯誤與踩雷

錯誤一:由上而下模型忘了過濾低信心 bbox。物件偵測器在 NMS 之後通常會留下信心 > 0.05 的 bbox(保留召回率),但這些低信心 bbox 可能是「背景誤判」,把它們送進關鍵點估計器會浪費時間、也可能產生錯誤的關節。對應排查方向:在由上而下的關鍵點估計前加 score_thresh=0.3 的過濾,並在視覺化時把低信心預測淡化。

錯誤二:熱圖與原圖座標轉換忘記乘 stride。熱圖尺寸通常是原圖的 1/4,要把熱圖上的 (y, x) 轉回原圖的 (Y, X) 必須乘 stride=4。如果忘記乘 stride,預測的關節會全部擠在影像左上角的 1/4 區域。對應排查方向:在解碼函式結尾加 xy *= stride,並用一個簡單測試:影像中心 (128, 96) 的關節點應該解碼到熱圖的 (32, 24)。

錯誤三:把熱圖 loss 用 sigmoid 前後值直接比。如果模型最後一層是 sigmoid(值域 0-1),但真實熱圖是「最大值 1、其他接近 0」,看起來一致;但如果最後一層是 logits(值域 -∞ 到 +∞),直接跟 0/1 的熱圖比 MSE loss 會非常大、梯度也大、訓練不穩定。對應排查方向:用 F.mse_loss(logits, target_heatmap) 而不是 F.mse_loss(torch.sigmoid(logits), target_heatmap),前者數值穩定得多(自帶 sigmoid + BCE 的數值穩定版本)。

錯誤四:由下而上的群組演算法沒處理「未配對」關節。匈牙利匹配會把所有關節分到某個人身上,但實務上有些關節可能沒被任何骨架連上(例如被遮擋的手腕)。如果你直接丟給匹配演算法,會把這些關節硬配給最近的骨架邊,造成誤連。對應排查方向:在群組前先對每個關鍵點類別做 NMS 過濾(保留 top-K 個偵測),並對信心低於門檻(例如 0.1)的關節直接標記為「未配對」。

錯誤五:座標回歸的 sigmoid 在訓練末期飽和。sigmoid 函式在輸入接近 ±6 時梯度接近 0,模型如果把 logit 推到 ±10,就完全學不到東西。這個現象在座標回歸裡特別嚴重——bbox 邊緣的關節點經常被推到 sigmoid 的極端。對應排查方向:用 torch.clamp(logits, -6, 6) 限制 logit 範圍,或改用 tanh(值域 -1 到 1 但同樣有飽和問題)。更根本的解法是把座標當「相對於 anchor 的偏移」而不是 normalized 座標,這樣數值範圍更穩定。

效能與實務提醒

推論成本:由上而下的模型(Mask R-CNN Keypoint)在單張 800×600 影像、1 個人時約 0.15 秒(Colab T4)、10 個人時約 0.8 秒;由下而上(OpenPose)在同樣影像上不管幾個人都約 0.3 秒。這就是為什麼「群眾計數」與「舞廳人流分析」通常選由下而上。YOLO11 Pose 是特殊案例——它把偵測與關鍵點合併成 one-stage,所以 N 個人與 1 個人的時間差異約 20%(主要是 NMS),比傳統由上而下快很多。

訓練資料的差異也值得注意。熱圖回歸需要把關鍵點 (x, y) 渲染成 2D 高斯圖(σ 通常設為 2 像素),這個渲染是訓練前處理的標準動作;如果你的資料集是自己標的,記得把 σ 設成跟 COCO 一致(σ=2 是 SimpleBaseline 與 HRNet 的預設)。座標回歸則直接用 (x, y),但要做 normalization(除以影像尺寸)以及對應的 bbox 裁切(把關鍵點映射回裁切後的座標系)。

選用框架的優先序:研究與高精度選 HRNet 或 HigherHRNet(即時推論與 edge device 選 YOLO11 Pose(Ultralytics 8.3.x);同時做偵測與關鍵點選 Mask R-CNN Keypoint(torchvision 0.20);群眾分析選 OpenPose 或 PifPaf。明天我們會用 YOLO11 Pose 在 Colab T4 上把這條路線完整跑一遍。

小結

今天把姿態估計的四條技術路線一次對照清楚:由上而下(先 bbox、再關節)vs 由下而上(先關節、再群組);熱圖回歸(空間精度好)vs 座標回歸(端到端可微)。重點回顧:第一,由上而下的單人精度高但推論時間隨人數線性增加;第二,由下而上的推論時間與人數無關但群組演算法複雜;第三,熱圖的 argmax 解碼不可微(soft-argmax 可微且給亞像素精度);第四,座標回歸需要小心 sigmoid 飽和與尺度不變性;第五,實務上 YOLO11 Pose 採座標回歸是為了合併進 one-stage 偵測器、HRNet 採熱圖是為了最大化精度。明天我們用 YOLO11 Pose 把「由上而下 + 座標回歸」這條路線完整跑一遍——從安裝、訓練、自定義資料集到推論視覺化。

結語

今天的重點是「建立四條技術路線的決策框架」。我們從由上而下與由下而上的策略差異開始,理解「為什麼 HRNet 與 OpenPose 都用熱圖但分屬不同路線」、「為什麼 YOLO11 Pose 放棄熱圖改用座標回歸」;接著用 PyTorch 寫了 6 段程式碼,把熱圖 argmax、soft-argmax、座標 sigmoid、批次群組等關鍵操作量化成可比較的數字;最後用一張對照表整理熱圖回歸與座標回歸的差異。讀完這篇你應該能回答:由上而下與由下而上的 crossover 點在哪?熱圖 argmax 為什麼不可微、soft-argmax 怎麼解決?YOLO11 Pose 為什麼選座標回歸?

這四條路線的取捨也呼應了前幾個任務的設計:分類任務(Day 3–9)選用端到端設計、偵測任務(Day 10–17)選用 one-stage vs two-stage、分割任務(Day 18–25)選用 encoder-decoder vs FPN,姿態任務(Day 26–)則要選「由上而下 vs 由下而上」與「熱圖 vs 座標」。每個任務都有自己的「策略 × 表示」軸,理解這個二維決策空間是 CV 工程師的關鍵能力。明天,我們會把今天的概念落到實作:Ultralytics YOLO11 Pose 在 Colab T4 上跑訓練與推論,並把 17 個關鍵點與 19 條骨架視覺化到原圖上。整個流程大約 25 分鐘,你會拿到一個可以在自己資料集上微調的姿態模型。

延伸資源

  • Cao 等人,2017,Realtime Multi-Person 2D Pose Estimation using Part Affinity Fields(CVPR 2017):https://arxiv.org/abs/1611.08050,OpenPose 原始論文,由下而上 + PAF 的經典設計。
  • Sun 等人,2019,Deep High-Resolution Representation Learning for Human Pose Estimation(HRNet,CVPR 2019):https://arxiv.org/abs/1902.09212,熱圖回歸的高解析度骨幹。
  • Cheng 等人,2020,HigherHRNet: Scale-Aware Representation Learning for Bottom-Up Human Pose Estimation(CVPR 2020):https://arxiv.org/abs/1908.10357,由下而上 + 高解析度熱圖。
  • Sun 等人,2018,Integral Human Pose Regression(ECCV 2018):https://arxiv.org/abs/1711.08229,soft-argmax 把不可微的 argmax 變成可微分的 integral regression。
  • Toshev & Szegedy,2014,DeepPose: Human Pose Estimation via Deep Neural Networks(CVPR 2014):座標回歸的開山之作,理解 YOLO11 Pose 設計動機的歷史起點。
  • Ultralytics Pose 訓練文件(8.3.x,2024):https://docs.ultralytics.com/tasks/pose/,YOLO11 Pose 模型的 model.train(data=..., task="pose") 完整 API。

留言

這個網誌中的熱門文章

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 建構深度學習模型。 開發者與研究人員 :想更深入了...