Phase 04 - Lesson 21

Detección de Keypoints y Estimación de Pose

This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.

Una pose es un conjunto ordenado de keypoints. Un detector de keypoints es un regresor de heatmaps. Todo lo demás es contabilidad.

Tipo: Build Lenguajes: Python Prerrequisitos: Fase 4 Lección 06 (Detección), Fase 4 Lección 07 (U-Net) Tiempo: ~45 minutos

Objetivos de Aprendizaje

  • Distinguir la estimación de pose top-down y bottom-up e indicar cuándo se usa cada una
  • Regresar heatmaps para K keypoints con un objetivo Gaussiano por keypoint y extraer las coordenadas de los keypoints en la inferencia
  • Explicar los Part Affinity Fields (PAFs) y cómo los pipelines bottom-up asocian keypoints en instancias
  • Usar MediaPipe Pose o MMPose para estimación de keypoints en producción y entender su formato de salida

El Problema

Las tareas de keypoints se esconden bajo muchos nombres: pose humana (17 articulaciones del cuerpo), landmarks faciales (68 o 478 puntos), mano (21 puntos), pose de animales, pose de objetos robóticos, landmarks anatómicos médicos. Cada una de ellas comparte la misma estructura: detectar K puntos discretos en un objeto y producir sus coordenadas (x, y).

La estimación de pose es la base de la captura de movimiento, las apps de fitness, el análisis deportivo, el control por gestos, la animación, la prueba de ropa en AR y el agarre robótico. El caso 2D está maduro; la pose 3D (estimar posiciones de articulaciones en coordenadas de mundo a partir de una sola cámara) es la frontera actual de la investigación.

La pregunta de ingeniería es la escala. Una pose de una sola persona en una sola imagen es un problema de 20ms. La pose de múltiples personas en una multitud a 30 fps es un problema distinto, con arquitecturas distintas.

El Concepto

Top-down vs bottom-up

flowchart LR
    subgraph TD["Pipeline top-down"]
        A1["Detectar cajas de personas"] --> A2["Recortar cada caja"]
        A2 --> A3["Modelo de keypoints por caja<br/>(HRNet, ViTPose)"]
    end
    subgraph BU["Pipeline bottom-up"]
        B1["Una pasada sobre la imagen"] --> B2["Todos los heatmaps de keypoints<br/>+ campo de asociación"]
        B2 --> B3["Agrupar keypoints en<br/>instancias (matching voraz)"]
    end

    style TD fill:#dbeafe,stroke:#2563eb
    style BU fill:#fef3c7,stroke:#d97706
  • Top-down — detectar a las personas primero y luego ejecutar un modelo de keypoints por persona en cada recorte. La mayor exactitud; escala linealmente con el número de personas.
  • Bottom-up — una sola pasada hacia adelante predice todos los keypoints más un campo de asociación; agrúpalos. Tiempo constante sin importar el tamaño de la multitud.

Top-down (HRNet, ViTPose) es el líder en exactitud; bottom-up (OpenPose, HigherHRNet) es el líder en throughput para escenas con aglomeración.

Regresión de heatmaps

En lugar de regresar (x, y) directamente, predice un heatmap H x W por keypoint con un blob Gaussiano centrado en la ubicación verdadera.

target[k, y, x] = exp(-((x - cx_k)^2 + (y - cy_k)^2) / (2 sigma^2))

En la inferencia, el argmax de cada heatmap es la ubicación predicha del keypoint.

Por qué los heatmaps funcionan mejor que la regresión directa: la estructura espacial de la red (el mapa de features convolucional) se alinea naturalmente con la salida espacial. Los objetivos Gaussianos además regularizan — un pequeño error de localización produce una pérdida pequeña, no cero.

Localización sub-pixel

El argmax entrega coordenadas enteras. Para precisión sub-pixel, refina ajustando una parábola al argmax y sus vecinos, o usa la conocida dirección de offset (dx, dy) = 0.25 * (heatmap[y, x+1] - heatmap[y, x-1], ...).

Part Affinity Fields (PAFs)

El truco de OpenPose para la asociación bottom-up. Para cada par de keypoints conectados (por ejemplo, hombro izquierdo a codo izquierdo), predice un campo de 2 canales que codifica el vector unitario que apunta de uno al otro. Para asociar un hombro con su codo, integra el PAF a lo largo de la línea que conecta los pares candidatos; el par con la mayor integral es el elegido.

For each connection (limb):
  PAF channels: 2 (unit vector x, y)
  Line integral: sum over sample points of (PAF . line_direction)
  Higher integral = stronger match

Elegante y escala a multitudes de tamaño arbitrario sin recortes por persona.

Keypoints de COCO

El dataset estándar de pose corporal: 17 keypoints por persona, con PCK (Percentage of Correct Keypoints) y OKS (Object Keypoint Similarity) como métricas. OKS es el análogo del IoU para keypoints y es lo que reporta el mAP@OKS de COCO.

2D vs 3D

  • Pose 2D — coordenadas de imagen; resuelta con calidad de producción (MediaPipe, HRNet, ViTPose).
  • Pose 3D — coordenadas de mundo / cámara; aún es investigación activa. Enfoques comunes:
    • Elevar predicciones 2D a 3D con un pequeño MLP (VideoPose3D).
    • Regresión 3D directa a partir de la imagen (PyMAF, MHFormer).
    • Configuraciones multi-vista (CMU Panoptic) para ground truth.

Construyéndolo

Paso 1: Objetivo de heatmap Gaussiano

import numpy as np
import torch

def gaussian_heatmap(size, cx, cy, sigma=2.0):
    yy, xx = np.meshgrid(np.arange(size), np.arange(size), indexing="ij")
    return np.exp(-((xx - cx) ** 2 + (yy - cy) ** 2) / (2 * sigma ** 2)).astype(np.float32)

hm = gaussian_heatmap(64, 32, 32, sigma=2.0)
print(f"peak: {hm.max():.3f} at ({hm.argmax() % 64}, {hm.argmax() // 64})")

Los heatmaps por keypoint apilados a lo largo de un eje de canal forman el tensor de objetivo completo.

Paso 2: Cabeza de keypoint diminuta

Un modelo al estilo U-Net que produce K canales de heatmap.

import torch.nn as nn
import torch.nn.functional as F

class TinyKeypointNet(nn.Module):
    def __init__(self, num_keypoints=4, base=16):
        super().__init__()
        self.down1 = nn.Sequential(nn.Conv2d(3, base, 3, 2, 1), nn.ReLU(inplace=True))
        self.down2 = nn.Sequential(nn.Conv2d(base, base * 2, 3, 2, 1), nn.ReLU(inplace=True))
        self.mid = nn.Sequential(nn.Conv2d(base * 2, base * 2, 3, 1, 1), nn.ReLU(inplace=True))
        self.up1 = nn.ConvTranspose2d(base * 2, base, 2, 2)
        self.up2 = nn.ConvTranspose2d(base, num_keypoints, 2, 2)

    def forward(self, x):
        h1 = self.down1(x)
        h2 = self.down2(h1)
        h3 = self.mid(h2)
        u1 = self.up1(h3)
        return self.up2(u1)

Entrada (N, 3, H, W), salida (N, K, H, W). La pérdida es el MSE por pixel contra los objetivos Gaussianos.

Paso 3: Inferencia — extraer coordenadas de los keypoints

def heatmap_to_coords(heatmaps):
    """
    heatmaps: (N, K, H, W)
    returns:  (N, K, 2) float coordinates in image pixels
    """
    N, K, H, W = heatmaps.shape
    hm = heatmaps.reshape(N, K, -1)
    idx = hm.argmax(dim=-1)
    ys = (idx // W).float()
    xs = (idx % W).float()
    return torch.stack([xs, ys], dim=-1)

coords = heatmap_to_coords(torch.randn(2, 4, 32, 32))
print(f"coords: {coords.shape}")  # (2, 4, 2)

Una línea en la inferencia. Para refinamiento sub-pixel, interpola alrededor del argmax.

Paso 4: Dataset sintético de keypoints

Simple: dibuja cuatro puntos en un lienzo blanco y aprende a predecirlos.

def make_synthetic_sample(size=64):
    img = np.ones((3, size, size), dtype=np.float32)
    rng = np.random.default_rng()
    kps = rng.integers(8, size - 8, size=(4, 2))
    for cx, cy in kps:
        img[:, cy - 2:cy + 2, cx - 2:cx + 2] = 0.0
    hms = np.stack([gaussian_heatmap(size, cx, cy) for cx, cy in kps])
    return img, hms, kps

Suficientemente fácil para que un modelo diminuto lo aprenda en un minuto.

Paso 5: Entrenamiento

model = TinyKeypointNet(num_keypoints=4)
opt = torch.optim.Adam(model.parameters(), lr=3e-3)

for step in range(200):
    batch = [make_synthetic_sample() for _ in range(16)]
    imgs = torch.from_numpy(np.stack([b[0] for b in batch]))
    hms = torch.from_numpy(np.stack([b[1] for b in batch]))
    pred = model(imgs)
    # Upsample pred to full resolution
    pred = F.interpolate(pred, size=hms.shape[-2:], mode="bilinear", align_corners=False)
    loss = F.mse_loss(pred, hms)
    opt.zero_grad(); loss.backward(); opt.step()

Usándolo

  • MediaPipe Pose — el estimador de pose de producción de Google; entrega runtimes WebGL + móviles con latencia sub-10ms.
  • MMPose (OpenMMLab) — base de código de investigación integral; toda arquitectura SOTA con pesos preentrenados.
  • YOLOv8-pose — la pose multipersona en tiempo real más rápida, con una sola pasada hacia adelante.
  • transformers HumanDPT / PoseAnything — enfoques vision-language más recientes para pose de vocabulario abierto (cualquier objeto, cualquier conjunto de keypoints).

Entregándolo

Esta lección produce:

  • outputs/prompt-pose-stack-picker.md — un prompt que elige MediaPipe / YOLOv8-pose / HRNet / ViTPose dadas la latencia, el tamaño de la multitud y la necesidad de 2D vs 3D.
  • outputs/skill-heatmap-to-coords.md — una skill que escribe la rutina sub-pixel de heatmap a coordenada usada por todo modelo de pose de producción.

Ejercicios

  1. (Fácil) Entrena el modelo diminuto de keypoints en el dataset sintético de 4 puntos. Reporta el error L2 medio entre los keypoints predichos y los verdaderos después de 200 pasos.
  2. (Medio) Agrega refinamiento sub-pixel: dada la posición del argmax, ajusta una parábola 1D a lo largo de x e y a partir de los pixeles vecinos. Reporta la ganancia de exactitud vs el argmax entero.
  3. (Difícil) Construye un dataset sintético de 2 personas en el que cada imagen muestre dos instancias del patrón de 4 keypoints. Entrena un pipeline bottom-up con PAFs que prediga qué keypoint pertenece a qué instancia y evalúa el OKS.

Términos Clave

Término Lo que dice la gente Lo que realmente significa
Keypoint "Un landmark" Un punto específico y ordenado en un objeto (articulación, esquina, feature)
Pose "El esqueleto" Un conjunto ordenado de keypoints que pertenecen a una instancia
Top-down "Detectar y luego pose" Pipeline de dos etapas: detector de personas + modelo de keypoints por recorte; la mayor exactitud
Bottom-up "Pose primero, agrupar después" Predicción de todos los keypoints en una pasada + agrupamiento; tiempo constante en el tamaño de la multitud
Heatmap "Objetivo Gaussiano" Tensor H x W por keypoint con pico en la ubicación verdadera; el objetivo de regresión preferido
PAF "Part Affinity Field" Campo de vector unitario de 2 canales que codifica direcciones de extremidades; usado para agrupar keypoints en instancias
OKS "IoU de keypoints" Object Keypoint Similarity; la métrica de COCO para pose
HRNet "High-Resolution Net" La arquitectura top-down dominante para keypoints; preserva features de alta resolución de principio a fin

Lecturas Adicionales

0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).