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
- (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.
- (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.
- (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
- OpenPose (Cao et al., 2017) — bottom-up con PAFs; aún el mejor texto sobre el enfoque
- HRNet (Sun et al., 2019) — la arquitectura top-down de referencia
- ViTPose (Xu et al., 2022) — ViT puro como backbone de pose; SOTA actual en muchos benchmarks
- MediaPipe Pose — pose en tiempo real de producción; el stack desplegado más rápido en 2026