Faces, People and Pose

Top-down vs bottom-up pose estimation

Top-down finds each person then their joints, bottom-up finds all joints then groups them, and the choice is decided by how many people are in your frames.

On this page 8
  1. The short answer
  2. How each one runs
  3. The trade, stated plainly
  4. How the grouping is done
  5. Which to pick
  6. Where you have already seen this
  7. Remember this
  8. What to learn next

One lesson, three depths. Pick the one that fits you today — you can switch any time.

Beginner — No maths. Plain English.

The short answer

Top-down finds each person first, then their joints. Bottom-up finds every joint in the picture first, then works out who owns what.

Think of a family sorting a huge pile of washing. One way is to pull out one person's clothes at a time and fold them into their pile. Steady, tidy, and it takes longer the more people there are.

The other way is to sort the whole pile into shirts, trousers and socks. Then work out whose is whose from sizes and patterns. One sweep, no matter how big the family — and the guessing at the end can go wrong.

Those are the two designs, and there is no third.

How each one runs

   TOP-DOWN
     picture ─► person detector ─► box, box, box
                                    │    │    │
                                    ▼    ▼    ▼
                              crop, crop, crop
                                    │    │    │
                                    ▼    ▼    ▼
                             joints for each one

   BOTTOM-UP
     picture ─► one pass ─► every left elbow, every right knee, ...
                              │
                              ▼
                        group them into people
                              │
                              ▼
                        one skeleton each

The trade, stated plainly

Top-down is more accurate. Each person is cropped and enlarged before the joint model sees them. A small figure at the back of a crowd fills the frame by the time it is examined. Detail that would have been lost is there.

Top-down slows down as the crowd grows. One person, one pass. Ten people, ten passes. A hundred people, a hundred. The cost is linear in the number of people, and there is no way around it.

Bottom-up runs at one speed. One pass over the picture, whether it holds one person or eighty. The extra grouping work is small.

Bottom-up struggles when people overlap. Every joint is found, and now the wrong wrist can be attached to the wrong elbow. When two people stand close and cross arms, mistakes appear that top-down never had the chance to make.

Top-down inherits the detector's mistakes. A missed person has no joints at all. Two overlapping people merged into one box give one confused skeleton.

How the grouping is done

This is the interesting part of bottom-up. Alongside the joint locations, the network predicts a set of arrows.

Take a limb: elbow to wrist, or hip to knee. The network fills the space between those joints with small arrows pointing along it. To test whether an elbow connects to a wrist, walk in a straight line between them. Check whether the arrows agree with your direction of travel.

If they do, the two belong to the same person. If they point elsewhere, that wrist belongs to somebody else.

   person A's arm            person B's arm

     elbow                      elbow
       \  ↘ ↘ ↘                    /  ↙ ↙ ↙
        \                         /
       wrist                    wrist

   walk from elbow to wrist: do the arrows agree?

This is far better than picking the nearest wrist. Two people standing close often have each other's wrists nearer than their own.

Which to pick

One person, high accuracy needed — top-down. A fitness app, a clinical gait assessment.

Many people, fixed budget — bottom-up. A stadium camera, a crowd counter.

A phone, one user, real time — a purpose-built single-person model, which is what MediaPipe does.

Anything in between — measure. The crossover depends on how fast your detector runs and how fast one crop runs. Both numbers are yours to find.

Where you have already seen this

  • Sports broadcast overlays drawing skeletons on every player.
  • A dance game scoring your moves against a reference.
  • Crowd-safety systems watching a festival for people falling.
  • A phone camera suggesting you straighten up in a group photo.

Remember this

  • Top-down: detect people, then joints. More accurate, and slower with each extra person.
  • Bottom-up: all joints at once, then group them. Constant speed, and it fails in crowds.
  • Grouping uses learned direction arrows along each limb, not nearest-neighbour distance.

What to learn next

  • Object detection — the first half of every top-down pipeline.
  • YOLO — the detector most pose pipelines actually use, and the base for YOLO-Pose.
  • Latency and throughput — measuring the constants the crossover depends on.

Developer — Code and libraries.

Setup

bash
pip install numpy

Run against numpy 1.26. Two parts: the cost model that decides the architecture, and a working version of the grouping step that makes bottom-up possible.

Cost, then grouping

topdown_bottomup.py
import numpy as np

# ---- Part 1: the cost model. This is the whole reason both designs exist.
DET_MS, CROP_MS, HEAT_MS = 12.0, 9.0, 21.0   # detector, one top-down crop, one bottom-up pass
print(f"{'people':>7} {'top-down ms':>13} {'bottom-up ms':>14} {'faster':>10}")
for n in (1, 2, 5, 10, 30, 100):
    td, bu = DET_MS + n * CROP_MS, HEAT_MS + 0.05 * n
    print(f"{n:>7} {td:>13.1f} {bu:>14.1f} {'top-down' if td < bu else 'bottom-up':>10}")

# ---- Part 2: grouping. Bottom-up finds all the joints, then must decide who owns what.
def seg_dist_and_dir(p, a, b):
    v = b - a
    t = np.clip((p - a) @ v / (v @ v), 0, 1)
    return float(np.linalg.norm(p - (a + t * v))), v / np.linalg.norm(v)

def make_field(limbs):
    """Stand-in for the network's learned part affinity field: at each pixel,
       a unit vector along whichever true limb is nearest."""
    def field(p):
        return min((seg_dist_and_dir(p, a, b) for a, b in limbs), key=lambda r: r[0])[1]
    return field

def paf_score(a, b, field, n=16):
    v = b - a
    u = v / np.linalg.norm(v)
    return float(np.mean([u @ field(a + t * v) for t in np.linspace(0, 1, n)]))

def report(elbows, wrists, truth, label):
    limbs = [(elbows[i], wrists[j]) for i, j in truth]
    field = make_field(limbs)
    S = np.array([[paf_score(e, w, field) for w in wrists] for e in elbows])
    nn  = [(i, int(np.argmin(np.linalg.norm(wrists - e, axis=1)))) for i, e in enumerate(elbows)]
    paf = [(i, int(np.argmax(S[i]))) for i in range(len(elbows))]
    print(f"\n{label}")
    print("  PAF score matrix (rows = elbows, cols = wrists):")
    for row in np.round(S, 4):
        print("   ", row.tolist())
    print("  nearest-neighbour grouping:", nn, "correct" if nn == truth else "WRONG")
    print("  PAF grouping              :", paf, "correct" if paf == truth else "WRONG")

report(np.array([[100., 200.], [300., 210.]]),
       np.array([[120., 260.], [285., 268.]]),
       [(0, 0), (1, 1)], "two people, well separated")

report(np.array([[100., 200.], [150., 205.]]),
       np.array([[145., 262.], [108., 258.]]),
       [(0, 0), (1, 1)], "the same two people, standing close and crossing arms")
Output
 people   top-down ms   bottom-up ms     faster
      1          21.0           21.1   top-down
      2          30.0           21.1  bottom-up
      5          57.0           21.2  bottom-up
     10         102.0           21.5  bottom-up
     30         282.0           22.5  bottom-up
    100         912.0           26.0  bottom-up

two people, well separated
  PAF score matrix (rows = elbows, cols = wrists):
    [1.0, 0.3944]
    [0.2592, 1.0]
  nearest-neighbour grouping: [(0, 0), (1, 1)] correct
  PAF grouping              : [(0, 0), (1, 1)] correct

the same two people, standing close and crossing arms
  PAF score matrix (rows = elbows, cols = wrists):
    [1.0, 0.8106]
    [0.8, 1.0]
  nearest-neighbour grouping: [(0, 1), (1, 0)] WRONG
  PAF grouping              : [(0, 0), (1, 1)] correct

Reading it

The crossover sits at two people, with these timings. That is unusually low and it depends entirely on CROP_MS. A lightweight single-person model at 3 ms per crop moves the crossover to around four; a heavy HRNet at 25 ms moves it to one. Measure your own three constants — this table is an argument for measuring, not a result to copy.

Bottom-up at a hundred people costs 26 ms, top-down 912 ms. That is the entire case for bottom-up in one line. For a crowd camera the choice makes itself.

Nearest-neighbour grouping gets the crossed case wrong. Elbow 0's nearest wrist belongs to person B. It is not a close call, and it is the normal situation whenever two people stand within arm's length.

The PAF score gets it right, and look at the margin. Well separated: 1.0 against 0.39. Standing close: 1.0 against 0.81. The correct answer still wins, and the gap has narrowed to a fifth of what it was. Add noise from a real network and that margin is where bottom-up methods start making mistakes. This is the accuracy gap between the two designs, made visible.

Real grouping is an assignment problem, not a per-row argmax. The code takes the best wrist for each elbow independently, which can assign one wrist to two elbows. OpenPose solves a bipartite matching per limb type with the PAF scores as edge weights, then merges limbs that share a joint into full skeletons.

Common mistakes

Benchmarking top-down on single-person images. The whole weakness of top-down is crowds. A single-person benchmark measures the part that was never in question.

Forgetting the detector in the latency budget. Top-down latency is detector plus N crops. Teams report the pose model's time alone and are surprised in production.

Batching crops without padding to a common size. Top-down crops have different aspect ratios. Resizing each independently distorts, and letterboxing wastes compute. Both are correct choices; making the choice by accident is not.

Assuming bottom-up is always faster. With one person in frame it is usually slower, because the single heatmap pass covers a full-resolution image rather than a small crop.

Ignoring what happens when the detector merges two people. Two overlapping figures in one box produce a single skeleton with limbs from both. The pose model has no way to signal this. Sanity-check limb lengths downstream.

Try it yourself

Set CROP_MS = 3.0 to model a lightweight single-person model, and rerun the first table. Find the new crossover. Then set HEAT_MS = 60.0 to model a high-resolution bottom-up network, and find it again. The crossover is a property of your two models, and it is the number that should decide your architecture.

What to learn next

  • Object detection — the first half of every top-down pipeline.
  • YOLO — the detector most pose pipelines actually use, and the base for YOLO-Pose.
  • Latency and throughput — measuring the constants the crossover depends on.

Researcher — Mathematics and papers.

The two paradigms

Top-down factorises the problem as detection followed by single-person pose estimation:

$$ p(\text{poses} \mid I) = \prod_{i=1}^{N} p(\text{pose}_i \mid \text{crop}(I, b_i)) \cdot p({b_i} \mid I) $$

Inference cost is $C_{\text{det}} + N \cdot C_{\text{pose}}$. Accuracy benefits from scale normalisation: each crop is resized to a fixed input, so the model always sees a person at roughly constant scale.

Bottom-up predicts all keypoints jointly, then partitions them. Cost is $C_{\text{heat}} + C_{\text{group}}$, with $C_{\text{group}}$ near-constant in $N$.

Grouping methods

Part Affinity Fields. Cao et al. (CVPR 2017; TPAMI 2021), OpenPose (arxiv.org/abs/1812.08008), predict a 2D vector field $\mathbf{L}c$ per limb type $c$. For a candidate connection between joints at $\mathbf{d}{j_1}$ and $\mathbf{d}_{j_2}$:

$$ E = \int_{u=0}^{1} \mathbf{L}c\big(\mathbf{p}(u)\big) \cdot \frac{\mathbf{d}{j_2} - \mathbf{d}_{j_1}}{\lVert \mathbf{d}_{j_2} - \mathbf{d}_{j_1} \rVert_2}\, du, \qquad \mathbf{p}(u) = (1-u)\mathbf{d}_{j_1} + u\,\mathbf{d}_{j_2} $$

The integral is approximated by uniform sampling. The full assignment over all joints is NP-hard, so OpenPose relaxes it to independent bipartite matchings per limb type, solved by the Hungarian algorithm, then merges limbs sharing a joint. The relaxation is the source of its characteristic crowd failures.

Associative embedding. Newell et al. (NeurIPS 2017) (arxiv.org/abs/1611.05424) predict a scalar tag per keypoint alongside the heatmap, trained so that tags of one person are close and tags of different people are far:

$$ L_{\text{pull}} = \frac{1}{NK}\sum_{n}\sum_{k} (h_k(x_{nk}) - \bar{h}n)^2, \qquad L{\text{push}} = \frac{1}{N^2}\sum_{n}\sum_{n'} \exp\left(-\frac{1}{2\sigma^2}(\bar{h}n - \bar{h}{n'})^2\right) $$

$h_k$ is the tag map for joint $k$, $x_{nk}$ person $n$'s joint $k$ location, $\bar{h}_n$ the mean tag for person $n$. Grouping is then a greedy 1D clustering over tag values. This underpins HigherHRNet (Cheng et al., CVPR 2020), which adds a higher-resolution deconvolution branch specifically to handle small people, the bottom-up weakness.

PersonLab / offset regression. Papandreou et al. (ECCV 2018) predict, at every keypoint, offsets to every other keypoint of the same person, then greedily decode from the most confident seed. Grouping becomes regression rather than combinatorial search.

The scale problem

Bottom-up processes the whole image at one scale, so a person occupying 5% of the frame and one occupying 80% share a feature representation. Top-down normalises this away by construction.

Bottom-up responses:

  • Multi-scale testing — accurate, and it destroys the constant-cost advantage.
  • Higher-resolution output — HigherHRNet's stride-2 branch.
  • Scale-aware supervision — per-person adaptive $\sigma$ in the target heatmaps.

The gap on COCO has narrowed and has not closed. Top-down still leads at the high-accuracy end.

Current numbers, with the usual caveats

MethodParadigmCOCO APNote
OpenPosebottom-up~0.65 (val)2017; the reference PAF implementation
HigherHRNet-W48bottom-up~0.72 (val)Multi-scale testing pushes higher
HRNet-W48top-down76.3 (val)384x288 input, with a detector
ViTPose-Gtop-down80.9 (test-dev)Large ViT backbone
RTMPose-mtop-down75.890+ FPS on an Intel i7-11700 CPU

RTMPose (Jiang et al., 2023, arxiv.org/abs/2303.07399) is the practically important entry: a top-down method fast enough on CPU that the top-down cost argument largely dissolves for small crowds. Reported throughput is hardware-specific; treat the ordering as reliable.

Single-stage methods

A third family predicts complete poses directly, avoiding both the detector and the grouping step.

DEKR (Geng et al., CVPR 2021) uses disentangled keypoint regression with adaptive convolutions. YOLO-Pose (Maji et al., 2022) attaches keypoint regression heads to a YOLO detector and optimises OKS directly, so one forward pass yields boxes and poses together. PETR and ED-Pose apply DETR-style set prediction, removing NMS as well.

Single-stage methods sit between the two paradigms on both cost and accuracy, and they are attractive when a person detector is needed anyway.

Choosing, in practice

The decision is dominated by the expected number of people per frame and the accuracy floor:

  • $N = 1$, accuracy critical → top-down, or a dedicated single-person model.
  • $N$ small and bounded, CPU budget → RTMPose-class top-down.
  • $N$ large or unbounded, fixed latency → bottom-up, and accept crowd errors.
  • Detector already in the pipeline → single-stage, which reuses it.

Measure $C_{\text{det}}$, $C_{\text{pose}}$ and $C_{\text{heat}}$ on your hardware before committing. The crossover moves by a factor of several between a laptop CPU and a datacentre GPU.

Papers

What to learn next

  • Object detection — the first half of every top-down pipeline.
  • YOLO — the detector most pose pipelines actually use, and the base for YOLO-Pose.
  • Latency and throughput — measuring the constants the crossover depends on.