Skip to content
AI360XpertBeta

OpenPose Real-Time Skeletons

OpenPose finds every visible joint plus a field of limb-direction arrows in one network pass, then links joints into skeletons by checking which connections follow the arrows.

OpenPose paints arrows along each limb so the assembler links the elbow-wrist pair whose segment follows the arrows.
OpenPose paints arrows along each limb so the assembler links the elbow-wrist pair whose segment follows the arrows.

Why Does This Exist?

Multi-person pose has a grouping problem: finding all elbows in a crowd photo is easy, but knowing whose elbow belongs to whose wrist is hard. Top-down systems crop each person first and run pose per crop, so their cost grows with headcount and they miss people the person-detector drops. A busy plaza frame with twenty people needs twenty network passes.

OpenPose (Cao and colleagues, 2017) made the bottom-up answer practical. One network pass predicts every joint location plus, for each limb, a Part Affinity Field (PAF): a dense 2D arrow map pointing along that limb. Linking becomes a scoring problem over arrows, and crowds assemble in milliseconds after the single pass. The general pose setup lives in pose estimation; lifting these flat skeletons into depth lives in 3D human pose estimation. This page covers heatmaps, affinity fields and greedy assembly.

The one idea you need first. A joint heatmap is an image-sized score sheet with one bright peak per visible instance of that joint. Peaks give candidates; they never say which elbow pairs with which wrist, which is exactly the gap the arrow fields fill.

Think of It Like This

Wind arrows between landing beacons

Every airport beacon flashes its position, and wind arrows paint the sky along each approved flight corridor. Controllers connect two beacons when the straight path between them rides the painted wind instead of crossing it. Elbow-to-wrist linking works the same: the candidate pair whose segment best follows the limb arrows belongs to one arm.

Where it stops: when flight corridors cross in a storm, arrows tangle and even good controllers connect the wrong pair. Tangled crowds tangle affinity fields the same way.

How It Actually Works

Heatmaps for joints, arrow fields for limbs

A convolutional backbone feeds refinement stages that output two things. First, one heatmap HjH_j per joint type jj (the original build predicts 19 sheets covering 18 joints plus background; COCO builds use its own keypoint set), where each pixel holds the confidence that joint jj sits there. Candidate joints come from non-maximum suppression over each heatmap. Second, one Part Affinity Field LcL_c per limb type cc, where each pixel holds a 2D unit vector pointing from the limb's start joint toward its end joint (19 limbs times 2 direction numbers gives 38 channels in the original build).

Scoring a candidate limb

Take candidate joints EE (elbow) and WW (wrist) and the unit vector uu pointing from EE to WW. Sample PP evenly spaced points p1p_1 through pPp_P along segment EWEW and average how well the field agrees with the segment:

E(E,W)=1P∑k=1PLc(pk)⋅uE(E, W) = \frac{1}{P} \sum_{k=1}^{P} L_c(p_k) \cdot u

where ⋅\cdot is the dot product, so each sample scores 11 when the painted arrow points exactly along the segment and near 00 when it points across it. Each limb type is then matched greedily: sort all candidate pairs by EE, link the best, remove its joints, and repeat. Low-total-score people are pruned at the end.

Worked assembly numbers

Elbow candidates E1,E2E_1, E_2 and wrist candidates W1,W2W_1, W_2, with P=10P = 10 samples each. Segment E1W1E_1W_1 points along u=(1,0)u = (1, 0) and every field sample reads (0.9,0.4359)(0.9, 0.4359), a unit arrow whose dot with uu is 0.90.9; the sum over 10 samples is 9.09.0, so E=0.90E = 0.90. Rival segment E1W2E_1W_2 samples read (0.2,0.9798)(0.2, 0.9798), each dotting to 0.20.2, so E=0.20E = 0.20. Segment E2W2E_2W_2 samples read (0.85,0.5268)(0.85, 0.5268), each dotting to 0.850.85, so E=0.85E = 0.85. Greedy order links E1W1E_1W_1 at 0.900.90 first, removes E1E_1 and W1W_1, then links E2W2E_2W_2 at 0.850.85. The 0.200.20 cross-connection never survives, which is the arrows doing the grouping work.

Code

pairs = [(("E1", "W1"), 0.9), (("E1", "W2"), 0.2), (("E2", "W2"), 0.85)]print(sorted(pairs, key=lambda p: p[1], reverse=True)[0][0])# -> ('E1', 'W1')

Watch Out For

Greedy links swapping limbs in tangled crowds

Overlapping arms create plausible wrong pairings with high field scores. The symptom is swapped forearms in hugs, scrums and dance lines. Fix it by raising input resolution so fields separate, or switch to top-down methods when accuracy outranks speed.

Running the 2016-era model file today

Old checkpoints ship outdated joint sets and weak small-person recall. The symptom is missing ears and vanishing distant figures. Fix it by using maintained builds with current COCO keypoint outputs and modern backbones.

The Quick Version

  • OpenPose predicts joint heatmaps plus limb-direction arrow fields in a single pass.
  • Each candidate limb scores the average arrow alignment along its segment.
  • Greedy matching links the best pair first, then removes those joints and repeats.
  • Bottom-up cost stays flat as headcount grows, unlike per-person top-down crops.
  • Tangled crowds are the accuracy ceiling, where arrows overlap and links swap.