TAI BUI
Back to curriculum
BuildPython

Keypoint Detection & Pose Estimation

A pose is a set of ordered keypoints. A keypoint detector is a heatmap regressor. Everything else is bookkeeping.

TIME: ~45 minutesPREREQUISITES: Phase 4 Lesson 06 (Detection), Phase 4 Lesson 07 (U-Net)

Learning Objectives

  • Distinguish top-down and bottom-up pose estimation and state when each is used
  • Regress heatmaps for K keypoints with a Gaussian-per-keypoint target and extract keypoint coordinates at inference
  • Explain Part Affinity Fields (PAFs) and how bottom-up pipelines associate keypoints into instances
  • Use MediaPipe Pose or MMPose for production keypoint estimation and understand their output format

The Problem

Keypoint tasks hide under half a dozen different names: human pose (17 body joints), face landmarks (68 or 478 points), hand pose (21 points), animal pose, robotic object pose, medical anatomy landmarks. Strip the domain label off any of them and the task is identical: detect K discrete points on an object and report their (x, y) coordinates.

That single structure underwrites motion capture, fitness apps, sports analytics, gesture control, animation, AR try-on, and robotic grasping. The 2D case is solved at production quality. 3D pose, recovering joint positions in world coordinates from a single camera, is still where the open research problems live.

The engineering question that actually matters here is scale, not accuracy. A single-image, single-person pose is a 20ms problem you can solve with a small model. Multi-person pose in a crowd at 30 fps is a different problem, with different architectures, even though both get filed under "pose estimation."

Warm-up0/3
1. Sports broadcasts often overlay a stick-figure skeleton on an athlete to show joint angles in real time. In plain terms, what is the system tracking frame to frame to draw that skeleton?Warm-up
Two video frames of the same athlete mid-motion, each with a few small unlabeled marked points on the body, with the points shifted slightly between frame N and frame N+1.
2. A segmentation model outputs a full-resolution map, one prediction per pixel, instead of a single label for the whole image. In general terms, why does that kind of per-pixel output suit tasks that need a precise spatial location better than a model that outputs just one or two raw numbers?Warm-up
A card labelled output A holding two raw numbers x and y, drawn below as two cells, next to a card labelled output B holding one value per pixel, drawn below as a five by five grid of cells on the same layout as the image.
3. In object detection, what does IoU (Intersection over Union) measure for a predicted box?Warm-up
A dashed orange predicted box and a solid blue ground-truth box drawn over the same region with their shared overlap shaded, next to a dashed question mark box labelled IoU equals question mark.
Check: The Problem0/3
1. Face landmarks, hand pose and animal pose sound like different tasks. What structure do they share once the domain label is removed?Define
Three panels holding a face, a hand and an animal, each marked with a handful of small orange dots, under a line asking what all three models produce once the domain name is stripped off.
2. What is a keypoint?Define
A simple stick figure with one highlighted dot midway along its right arm, a dashed line to a card reading name right elbow, location x y equals 142, 88.
Illustrative coordinates, not taken from the lesson's code.
3. In this lesson's terms, what is a pose?Define
A stick figure carrying five numbered orange dots, next to the same five numbered dots drawn on their own and joined by dashed lines.

The Concept

Top-down vs bottom-up

Mermaid diagram

  • Top-down: detect people first, then run a per-person keypoint model on each crop. Highest accuracy, and cost scales linearly with the number of people in frame.
  • Bottom-up: one forward pass predicts every keypoint plus an association field, then groups them into instances. Constant time no matter how crowded the scene gets.

Top-down (HRNet, ViTPose) wins on accuracy. Bottom-up (OpenPose, HigherHRNet) wins on throughput once the scene fills up with people.

Heatmap regression

Instead of regressing (x, y) directly, predict an H x W heatmap per keypoint with a Gaussian blob centred at the true location.

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

At inference, the argmax of each heatmap is the predicted keypoint location.

Heatmaps beat direct regression because a conv feature map's spatial structure already lines up with a spatial output; asking the network to collapse that structure into two raw numbers throws information away. A Gaussian target also regularises the loss: a small localisation error costs a small loss, not zero.

Sub-pixel localisation

Argmax only gives integer coordinates. For sub-pixel precision, fit a parabola to the argmax and its neighbours, or use the well-known offset (dx, dy) = 0.25 * (heatmap[y, x+1] - heatmap[y, x-1], ...) direction.

Part Affinity Fields (PAFs)

OpenPose's trick for bottom-up association. For each pair of connected keypoints (left shoulder to left elbow, say), predict a 2-channel field encoding the unit vector that points from one to the other. To decide whether a candidate shoulder and a candidate elbow belong to the same person, integrate the PAF along the line connecting them: the pair with the highest integral wins the match.

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

Simple idea, and it scales to arbitrary crowd sizes without ever cropping a single person out of the frame.

COCO keypoints

The standard body-pose dataset annotates 17 keypoints per person and reports two metrics: PCK (Percentage of Correct Keypoints) and OKS (Object Keypoint Similarity). OKS is the keypoint analogue of IoU, and it's what COCO's mAP@OKS number is actually measuring.

2D vs 3D

  • 2D pose: image coordinates, solved at production quality (MediaPipe, HRNet, ViTPose).
  • 3D pose: world or camera coordinates, still active research. Three common approaches:
    • Lift 2D predictions to 3D with a small MLP (VideoPose3D).
    • Regress 3D directly from the image (PyMAF, MHFormer).
    • Use a multi-view capture rig (CMU Panoptic) to generate ground truth.
Check: The Concept0/23
1. What is a top-down pose pipeline?Define
A frame holding three people, an arrow into two blank stage boxes labelled stage 1 and stage 2, then an arrow to a panel holding figures whose joints are marked, under a line asking which two components run and in which order.
2. What defines a bottom-up pose pipeline?Define
A frame holding three people, an arrow into one large dashed question mark panel, then an arrow to a panel holding figures whose joints are marked, labelled out: one pose per person.
3. In pose estimation, what is the heatmap target for one keypoint?Define
A seven by seven block of cells shaded from dark to bright around a marked point at (32, 32), next to an outline of the whole 64 by 64 array with that small region ringed in the middle.
Cell brightness is the value from the lesson's own gaussian_heatmap, sigma = 2.0.
4. What are Part Affinity Fields (PAFs)?Define
A shoulder point and an elbow point joined by a line, with a dashed question mark box sitting over the line and a caption asking what the model predicts along it.
5. OKS (Object Keypoint Similarity) is the pose-estimation analogue of which detection metric?Define
A card for detection comparing a predicted box with a ground-truth box whose score name is left as a question mark, next to a card for pose comparing predicted keypoints with ground truth and naming its score OKS.
6. What is HRNet?Define
A card headed HRNet with three blanked rows: family, what it does, and what it is known for.
7. Which pair of models does the lesson name as top-down methods that win on accuracy?Define
A two-row table of the lesson's pipeline families, top-down and bottom-up, with both model names in each row left as question marks.
8. How many keypoints per person does the standard COCO body-pose dataset annotate, and which metric besides OKS does the lesson name for it?Define
Three cards for COCO body, hand and face keypoint counts with every count left as a question mark, above a line naming OKS as one pose metric and a dashed question mark box for the second.
9. Which 3D pose approach lifts existing 2D keypoint predictions into 3D coordinates with a small MLP?Define
A table of the lesson's three ways to get 3D pose, each described by how it works, with every approach name left as a question mark.
10. Why do pose models regress heatmaps instead of (x, y) coordinates directly?Explain
A conv feature map drawn as a grid of cells, with arrows to two cards: head A outputting two numbers x and y, and head B outputting one H by W grid of values per keypoint.
11. Top-down versus bottom-up pose estimation: which scales better with crowd size, and why?Explain
A table with a top-down row and a bottom-up row and columns for 1 person, 5 people and 15 people, every cost cell left as a question mark.
12. Why does integrating a PAF along the line between a candidate shoulder and a candidate elbow help decide whether they match?Explain
A shoulder point S joined by two dashed lines of sample points to candidate elbow points A and B, with a dashed question mark box to the side and a line asking why integrating along each line settles the match.
13. Why does sub-pixel refinement around the argmax meaningfully lift keypoint accuracy?Explain
A bar chart of six sampled heatmap values along one row, labelled x equals 30 through 35, with the tallest bar highlighted and the two tallest bars almost the same height.
Values computed with the lesson's own gaussian_heatmap formula, sigma = 2.0.
14. Bottom-up models like OpenPose lose accuracy benchmarks to HRNet and ViTPose but still ship in production. Why?Explain
A card stating the premise that OpenPose loses accuracy benchmarks to HRNet and ViTPose yet still ships in production in 2026, above a dashed question mark box asking why.
15. Near a heatmap peak, the left neighbour holds 0.2 and the right neighbour holds 0.6. Using the offset 0.25 * (right - left), how is x adjusted?Apply
A code box listing a left neighbour value of 0.2 and a right neighbour value of 0.6, with the offset dx equal to 0.25 times their difference left as a question mark.
16. A candidate shoulder S has two candidate elbows, A and B. Along the line S-A, the PAF dotted with the line direction is 1 at each of 5 sample points. Along S-B it is 0 at each sample. Which pairing wins?Apply
A shoulder point S joined by two dashed lines of five sample points to candidate elbows A and B, a card for S to A giving 1 at each of 5 samples, a card for S to B giving 0 at each of 5 samples, and a dashed question mark box asking which pairing wins.
17. Your keypoint model's heatmap head outputs at one-quarter the input resolution to save compute. At inference, you scale the argmax position back up to full resolution and use it directly, with no sub-pixel refinement. What is the practical effect?Analyze
A 4 by 4 low-resolution grid with one highlighted cell marked as the argmax, an arrow toward a dashed question mark box captioned scaled to full resolution with no refinement, effect.
18. A pose overlay on video jitters from frame to frame, even though the heatmaps look stable. Which post-processing choice is the likely cause?Analyze
Two three-cell heatmap rows for consecutive frames whose values differ only in the third decimal place, under a line noting the values barely move yet the drawn overlay jumps, and asking for the root cause.
Values computed with the lesson's own gaussian_heatmap formula, sigma = 2.0.
19. A pose pipeline takes about 20 ms on a frame with one person, but latency climbs steadily as a crowd of 15 people enters. Which design best explains this?Analyze
A bar chart with a fixed 20 millisecond bar for 1 person and a dashed question mark bar for 15 people marked rising, with a caption asking which pipeline stage causes the climb.
20. A bottom-up model finds every shoulder and elbow correctly, but in a crowd it links one person's shoulder to a neighbour's elbow. Which component needs fixing?Analyze
Two stick figures, each with a shoulder dot marked S and an elbow dot marked E, with a dashed warning-coloured line running from person 1's shoulder to person 2's elbow, labelled linked (wrong).
21. A fitness app guarantees exactly one person in frame and wants the best per-person accuracy. Which design fits?Design
One stick figure inside a dashed phone-shaped frame, captioned exactly 1 person every frame, with a note asking for the best per-person accuracy and a dashed question mark box for the design.
22. A broadcast tool must process frames holding anywhere from 2 to 40 people within a fixed per-frame time budget. Which approach fits?Design
A row of person glyphs captioned anywhere from 2 to 40 people, next to a dashed box labelled fixed time budget, above a dashed question mark box and a line asking which approach fits.
23. You have a working 2D pose model and a single camera, and you want 3D joint positions by adding only a small extra model. Which approach fits?Design
A box labelled 2D keypoints from the existing model, an arrow to a dashed question mark box, with a caption stating the goal is 3D joint positions using only a small extra model.

Build It

Step 1: Gaussian heatmap target

python
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})")

Per-keypoint heatmaps stacked along a channel axis give the full target tensor.

Step 2: Tiny keypoint head

A U-Net-style model that outputs K heatmap channels.

python
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)

Input (N, 3, H, W), output (N, K, H, W). Loss is per-pixel MSE against Gaussian targets.

Step 3: Keypoint coordinates at inference

python
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)

That's the whole inference step, one line. For sub-pixel refinement, interpolate around the argmax instead of taking it raw.

Step 4: Synthetic keypoint dataset

Simple: draw four points on a white canvas and learn to predict them.

python
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

Easy enough for a tiny model to learn in a minute.

Step 5: Training

python
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()
Check: Build It0/4
1. gaussian_heatmap(64, 32, 32, sigma=2.0) peaks at (32, 32). Approximately what value does it hold at x=34, y=32?Apply
A one-row strip of three heatmap cells along y equals 32 reading 1.000 at the peak x equals 32, 0.882 at x equals 33 and a question mark at x equals 34, next to an outline of the whole 64 by 64 array with that row marked.
2. In heatmap_to_coords, a 32 x 32 heatmap's flattened argmax index is 100. Which (x, y) does it return?Apply
A code box setting a flattened argmax index of 100 and a width of 32, computing ys as idx floor-divided by W and xs as idx modulo W, with the returned (x, y) pair left as a question mark.
3. make_synthetic_sample(64) draws four keypoints and builds hms by stacking one gaussian_heatmap per keypoint. What shape is hms?Apply
A code box showing four (cx, cy) pairs and a np.stack call over one gaussian_heatmap per pair, with the resulting hms.shape left as a question mark.
4. TinyKeypointNet's output has a lower spatial resolution than the input. What does the training loop do to it before computing MSE against the Gaussian heatmap targets?Apply
A code box with the model's forward pass, a blanked-out line, then the MSE loss call, above a coarse grid labelled pred and a finer grid of the same size labelled hms with a dashed question mark box between them.

Use It

  • MediaPipe Pose: Google's production pose estimator. Ships WebGL and mobile runtimes with sub-10ms latency. Output is 33 landmarks per person, each an (x, y, z, visibility) tuple: x and y normalised to image size, z a relative depth estimate, visibility a confidence score that the point is actually in frame.
  • MMPose (OpenMMLab): the comprehensive research codebase, every SOTA architecture with pretrained weights included. Output is a (num_people, K, 3) array, one (x, y, score) triple per keypoint, with K and the keypoint order set by whichever dataset format the model trained on (17 for COCO body).
  • YOLOv8-pose: the fastest real-time multi-person pose option, a single forward pass and nothing else.
  • transformers HumanDPT / PoseAnything: newer vision-language approaches for open-vocabulary pose, any object, any keypoint set.
Check: Use It0/4
1. Which tool is described as the fastest real-time multi-person pose option, using a single forward pass?Define
A table of the four tools the Use It section lists, each with the lesson's own one-line description, with every tool name left as a question mark.
2. In MediaPipe Pose output, what does a landmark's visibility value express?Explain
A stick figure with one highlighted landmark dot, a dashed line to a card showing that landmark as a four-slot tuple whose first three slots are question marks and whose fourth is named visibility, with its meaning left blank.
3. What does MediaPipe Pose return for each detected person, and what does MMPose return per keypoint?Explain
A card for MediaPipe Pose and a card for MMPose, each with what it returns per person and what each entry holds left as question marks.
4. MMPose runs a COCO-format model on an image containing 3 people. What is the output array shape and what does each keypoint entry hold?Apply
A code box naming an MMPose model trained on the COCO body format with 3 people found in the frame, with the output array shape and the contents of each keypoint entry left as question marks.

Ship It

This lesson produces:

  • outputs/prompt-pose-stack-picker.md: a prompt that picks MediaPipe, YOLOv8-pose, HRNet, or ViTPose given latency, crowd size, and whether you need 2D or 3D.
  • outputs/skill-heatmap-to-coords.md: a skill that writes the sub-pixel heatmap-to-coordinate routine every production pose model relies on.
Author's Note

Top-down versus bottom-up reads like an accuracy trade-off in the papers, but the decision that actually matters in production is whether you can bound the number of people in frame ahead of time. A fitness app or an AR try-on knows there's exactly one person in the shot, so a top-down pipeline is close to free: crop the person, run the keypoint model, done. A security camera or a sports broadcast doesn't get that guarantee, and that's where a linear-in-people-count cost model stops being a rounding error and starts being the whole latency budget. That's the real reason bottom-up architectures like OpenPose are still shipping in 2026 despite losing the accuracy benchmarks to HRNet and ViTPose: they trade a bit of per-person precision for a runtime that doesn't care how many people walk into the frame.

Sub-pixel refinement is the other thing worth internalizing early. It looks like a rounding-error optimization until you're staring at pose output that jitters frame to frame in a video, and the fix turns out to be exactly this: stop truncating the heatmap peak to its nearest pixel.

Exercises

  1. (Easy) Train the tiny keypoint model on the synthetic 4-point dataset. Report mean L2 error between predicted and true keypoints after 200 steps.
  2. (Medium) Add sub-pixel refinement: given the argmax position, fit a 1D parabola along x and y from the neighbouring pixels. Report the accuracy gain vs integer argmax.
  3. (Hard) Build a 2-person synthetic dataset where each image shows two instances of the 4-keypoint pattern. Train a bottom-up pipeline with PAFs that predict which keypoint belongs to which instance, and evaluate OKS.

Key Terms

TermWhat people sayWhat it actually means
Keypoint"A landmark"A specific ordered point on an object (joint, corner, feature)
Pose"The skeleton"An ordered set of keypoints belonging to one instance
Top-down"Detect then pose"Two-stage pipeline: person detector + per-crop keypoint model; highest accuracy
Bottom-up"Pose first, group later"Single-pass all-keypoint prediction + grouping; constant time in crowd size
Heatmap"Gaussian target"H x W tensor per keypoint with peak at the true location; the preferred regression target
PAF"Part Affinity Field"2-channel unit vector field encoding limb directions; used to group keypoints into instances
OKS"Keypoint IoU"Object Keypoint Similarity; the COCO metric for pose
HRNet"High-Resolution Net"The dominant top-down keypoint architecture; preserves high-res features throughout

Further Reading