Paper · 2023-03-13 · cs.CV
RTMPose: Real-Time Multi-Person Pose Estimation based on MMPose
RTMPose is a top-down 2D pose estimator. It predicts each keypoint as two 1-D SimCC bin distributions, using a CSPNeXt backbone, a 7x7 convolution and one gated attention unit, trained with strong-then-weak augmentation. Author-reported: 75.8 AP on COCO (with flip test) for RTMPose-m and 72.2 AP for RTMPose-s, which runs at 70+ FPS on a Snapdragon 865.
Coordinate classificationSelf-attentionEfficient vision backbonesLearning-rate schedulespose-estimationkeypointsreal-timesimccdeployment
arXivPDFCodeMarkdown14 min read
AI-compiled by claude-haiku-5-5 on 2026-10-24, not yet reviewed by a human. Every claim cites its source; check it when a detail matters.
In plain words
Pose estimation finds the joints of people in a picture, such as the wrists, knees and head. The goal here is to do this fast enough for real-time use on modest hardware. The usual approach draws a heat map for each joint and picks the brightest spot. That works well but costs a lot of computation.
This paper builds a fast system in two steps. First a separate detector finds each person. Then a small network looks at the cropped person and finds each joint. The network does not draw heat maps. For each joint it guesses where the joint lies along the horizontal axis and along the vertical axis, by choosing among many small numbered slots on each axis. Because the output is two short lists of scores instead of large maps, the last part of the network stays small.
The authors did not invent one new trick. They tried many choices, one after another: the network's building blocks, the training recipe, the output format and the way the pieces are joined at run time. They kept the choices that helped and report how fast the result runs on a CPU, a graphics card and a mobile chip. The numbers are the authors' own.
TL;DR
RTMPose is a top-down pose estimator. A detector crops each person, and a CSPNeXt-based network predicts each keypoint as two 1-D SimCC distributions, one for x and one for y §3.1 §3.3. The head is a 7x7 convolution, a 256-dimensional expansion and one gated attention unit §3.3 §3.4. Training uses AdamW with flat-cosine decay, an EMA of the weights and a two-stage augmentation schedule §3.2 tab. 7. Author-reported: RTMPose-m reaches 75.8 AP on COCO with flip test, which the abstract gives without a table entry §1; RTMPose-s reaches 72.2 AP at 70+ FPS on a Snapdragon 865 chip §1.
Problem
Real-time multi-person pose estimation is hard on devices with little computing power. Earlier efficient networks and detection-free methods did not reach the accuracy that industrial use needs §1. Heat-map methods need high-resolution features or upsampling layers, which cost compute and latency §2 §3.1. The paper's premise is that a top-down pipeline can be fast if the detector is fast and the pose network is small §1.
Method
The pipeline has four parts. Each step below cites where the paper or the official code describes it.
- Detection and crop. A real-time detector gives one box per person. The box is warped to the network input, 256x192 in the released configs §1 §3.5 rtmpose-m_8xb256-420e_coco-256x192.py rtmpose-m_8xb256-420e_coco-256x192.py. The paper's detectors are YOLOv3, Faster R-CNN, PicoDet-s and RTMDet §4.2 tab. 4.
- Backbone. CSPNeXt-m, the backbone from RTMDet §3.1 rtmpose-m_8xb256-420e_coco-256x192.py.
- SimCC head. A 7x7 convolution gives one map per keypoint §3.4 tab. 2. The maps are flattened, expanded to 256 dimensions and refined by one gated attention unit (GAU). Two linear layers output the x and y bin logits §3.3 rtmcc_head.py.
- Training and decoding. Keypoints become Gaussian soft labels over bins. The loss is a KL divergence on temperature-softened distributions. At test time the argmax of each axis gives the coordinate §3.1 §3.4 simcc_label.py.
Pseudocode for the GAU, which is the only attention in the model §3.3 eq. 1 eq. 2:
# Gated Attention Unit over K keypoint tokens [§3.3] [eq. 1]
# Input: tokens (B, K, d), d = 256
# Output: tokens (B, K, d)
# Sizes: e = 2 * d = 512, s = 128 [code: rtmcc_block.py#L176]
def gated_attention_unit(tokens):
# Normalise the tokens before the block [code: rtmcc_block.py#L197]
normed = scale_norm(tokens) # (B, K, d)
# One projection gives U, V and a shared base [code: rtmcc_block.py#L187]
# The activation is SiLU in the config; the paper leaves phi unstated
hidden = activation(linear_uv(normed)) # (B, K, 2e + s)
u, v, base = split(hidden, [e, e, s])
# Q and K are per-dimension affine maps of the shared base [code: #L254]
queries, keys = unbind(base * gamma + beta) # each (B, K, s)
# Scaled scores, ReLU then square [eq. 2] [code: rtmcc_block.py#L283]
scores = queries @ transpose(keys) # (B, K, K)
attn = relu(scores / sqrt(s)) ** 2
# Gate V-weighted context with U [eq. 1] [code: rtmcc_block.py#L288]
out = u * (attn @ v) # (B, K, e)
out = linear_out(out) # (B, K, d)
# Residual with a learnable scale, used when dims match [code: #L210]
return out + res_scale * tokensPseudocode for the SimCC head, the training targets and the loss. The
pseudocode blocks below follow the official code, which differs from the
paper in places (see Paper vs code):
# SimCC head for a 256x192 crop [§3.1] [§3.3] [§3.4] [tab. 2]
# Input: feature map (B, C, 8, 6), height x width, from the backbone
# Output: logits_x (B, K, 384), logits_y (B, K, 512) [code: #L5, #L204]
def simcc_head(feature_map):
# 7x7 convolution, one map per keypoint [§3.4] [tab. 2]
maps = conv_7x7(feature_map) # (B, K, 8, 6)
# Flatten, normalise, then expand to 256 dims [§3.3] [code: #L111-L113]
tokens = linear_256(scale_norm(flatten(maps))) # (B, K, 256)
tokens = gated_attention_unit(tokens) # [§3.3] [eq. 1]
# Two linear layers give the bin logits [§3.1] [code: #L131-L132]
logits_x = linear_x(tokens) # (B, K, 2 * 192)
logits_y = linear_y(tokens) # (B, K, 2 * 256)
return logits_x, logits_y# Gaussian SimCC target for one visible keypoint [§3.4] [eq. 4] [eq. 6]
# Input: keypoint (x, y) in crop pixels; sigma_x, sigma_y (see eq. 6)
# Output: target_x (384,), target_y (512,)
def simcc_target(keypoint, sigma_x, sigma_y):
# Map pixels to bins, split ratio 2 [code: simcc_label.py#L185]
mu_x, mu_y = round(keypoint * 2)
bins_x = arange(384)
bins_y = arange(512)
# Unnormalised Gaussian over the bins [code: simcc_label.py#L278-L279]
target_x = exp(-(bins_x - mu_x) ** 2 / (2 * sigma_x ** 2))
target_y = exp(-(bins_y - mu_y) ** 2 / (2 * sigma_y ** 2))
return target_x, target_y# Temperature-softened KL loss on SimCC outputs [§3.4] [eq. 5]
# Input: logits and targets per axis (B, K, W), visibility weights (B, K)
# Output: scalar loss
def simcc_loss(logits_x, logits_y, targets_x, targets_y, weights):
# beta = 1 / tau, with tau = 0.1 [§3.4] [code: classification_loss.py#L147]
beta = 10.0
total = 0
for logits, targets in [(logits_x, targets_x), (logits_y, targets_y)]:
# Softmax over bins, applied to model output and to the label
log_probs = log_softmax(logits * beta) # over bins
label_probs = softmax(targets * beta) # over bins
# KL per keypoint, averaged over bins [code: #L149-L150]
kl = mean(label_probs * (log(label_probs) - log_probs), bins)
# Weighted, summed over the batch and both axes [code: #L175]
total = total + sum(kl * weights)
# Divide by the number of keypoints K [code: classification_loss.py#L177]
return total / K# Decoding, with the flip test [code: rtmcc_head.py#L196-L207]
# Input: logits_x (K, 384), logits_y (K, 512), optional flipped pair
# Output: keypoint (x, y) in crop pixels, and a score per keypoint
def decode(logits_x, logits_y, flipped=None):
if flipped is not None:
# Average raw logits of the original and the mirrored pass
logits_x = (logits_x + flip_back(flipped.x)) * 0.5
logits_y = (logits_y + flip_back(flipped.y)) * 0.5
# Argmax per axis, then undo the split ratio 2 [code: simcc_label.py#L173]
x = argmax(logits_x) / 2
y = argmax(logits_y) / 2
# Score: smaller of the two peaks [code: post_processing.py#L85-L86]
score = min(max(logits_x), max(logits_y))
return (x, y), scoreInference, as the paper describes it §3.5: the detector runs only every K frames (skip-frame, as in BlazePose), and in between the boxes come from the last pose result. The poses are then filtered with OKS-based pose NMS and a OneEuro filter §3.5. I did not find the skip-frame logic or the OneEuro filter in the code I read (see Pitfalls).
Hyperparameters
Values for RTMPose-m (COCO, 256x192). The paper's table covers all sizes tab. 7. The config is the fine-tuning config, and the pre-training config is listed in the last rows.
| Name | Value | Where | Notes |
|---|---|---|---|
| optimizer | AdamW | tab. 7 rtmpose-m_8xb256-420e_coco-256x192.py | |
| weight decay | 0.05 (RTMPose-m/l); 0 (RTMPose-t/s) | tab. 7 | Bias and norm weight decay set to 0 in the config |
| base learning rate | 0.004 | tab. 7 | |
| batch size | 1024 | tab. 7 | Config uses 256 per data loader . See Paper vs code |
| warm-up | 1000 iterations, linear from 1e-5 x base | tab. 7 | Start factor is not in the paper |
| LR schedule | Flat cosine: constant to epoch 210, then cosine to 0.05 x base | §3.2 tab. 7 | Constant phase is set by the cosine begin |
| gradient clipping | L2 norm 35 | Not in the paper | |
| EMA | decay 0.9998 (s/m/l); none (t) | tab. 7 | Config momentum 0.0002; the mapping momentum = 1 - decay is my reading of the library |
| epochs, pre-training | 210 | tab. 7 cspnext-m_udp_8xb256-210e_coco-256x192.py | Heat-map UDP pre-training of the backbone §3.2 |
| epochs, fine-tuning | 420, last 30 with weak augmentation | tab. 7 | Strong/weak split for 420 epochs is not stated in the paper |
| strong stage: scale | [0.6, 1.4] | §3.2 | |
| strong stage: rotation | 80 | §3.2 | Unit not stated in the paper |
| strong stage: Cutout | probability 1 | §3.2 | Code uses CoarseDropout, one hole, sizes up to 0.4 |
| weak stage: scale, rotation | [0.75, 1.25], rotation 60, no shift | Paper says "small rotation range" and "turn off random shift", with no numbers | |
| weak stage: Cutout | probability 0.5 | §3.2 | |
| backbone | CSPNeXt-m: expand 0.5, deepen 0.67, widen 0.75, channel attention on, SiLU | §3.1 | Paper names CSPNeXt-m only |
| backbone init | UDP checkpoint trained on AIC+COCO | ||
| head input channels | 768 | Not stated in the paper | |
| feature dimension | 256 | §3.3 | |
| GAU width s | 128 | §3.3 eq. 2 | |
| GAU expansion | 2 | Not stated in the paper | |
| GAU dropout, drop path, bias, RoPE | 0, 0, off, off | ||
| GAU activation | SiLU | Head default is ReLU rtmcc_head.py | |
| final conv kernel | 7x7 | §3.4 tab. 2 | |
| temperature | tau = 0.1, beta = 10 | §3.4 tab. 3 | Applied to outputs and to labels eq. 5 |
| label softmax | on | ||
| sigma | sqrt(W / 16) per axis: 4.90 (x, 384 bins), 5.66 (y, 512 bins) | §3.4 eq. 6 | Config uses the rounded constants |
| split ratio | 2.0 bins per pixel | Not stated in the paper; implied by W_S | |
| label normalisation | off | ||
| decoding | argmax; DARK off | DARK is not discussed in the paper | |
| input normalisation | mean (123.675, 116.28, 103.53), std (58.395, 57.12, 57.375), BGR to RGB | Not in the paper | |
| test-time flip | on in config | See Results for which tables use it | |
| detector (COCO table) | YOLOv3, Faster R-CNN, PicoDet-s, RTMDet-nano, RTMDet-m | tab. 4 | Detector changes AP, not the pose GFLOPs |
Results
Author-reported. Training data: COCO train2017 (118K images) and val2017 (5K) §4.2. Hardware: eight NVIDIA A100 GPUs §4.1. Metric: mean AP §4.1.
Development chain on COCO val (§3, author-reported). The paper gives a step-by-step gain from a lightweight SimCC baseline.
| Step | AP | Source |
|---|---|---|
| SimCC* baseline, CSPNeXt-m backbone | 69.7 | §3.1 |
| + UDP heat-map pre-training | 70.3 | §3.2 |
| + EMA | 70.4 | §3.2 |
| + flat-cosine schedule | 70.7 | §3.2 |
| + two-stage augmentation (no own number; next step starts from it) | 71.2 | §3.3 |
| + feature dimension 256 | 71.4 | §3.3 |
| + GAU self-attention | 71.9 | §3.3 |
| + temperature tau = 0.1 | 72.7 | §3.4 |
| + separate sigma per axis | 72.8 | §3.4 |
| + 7x7 final conv | 73.3 | §3.4 tab. 2 |
| + 270 / 420 epochs | 73.5 / 73.7 | §3.4 |
| + COCO and AI Challenger pre-training and fine-tuning | 75.3 | §3.4 |
Table 1 (computational cost of the baselines) is a separate comparison: SimCC* at 4.03 GFLOPs and 23.59M parameters reaches 71.3 AP, and the caption does not state the backbone tab. 1.
COCO val2017, top-down with a given detector (flip test not used; GFLOPs of the pose model only) tab. 4. The RTMPose rows are complete. Baseline rows are in the paper and are not repeated here.
| Detector | Extra data | t (0.36 GF) | s (0.68 GF) | m (1.93 GF) | l (4.16 GF) |
|---|---|---|---|---|---|
| Faster R-CNN | none | 65.8 | 69.6 | 73.6 | 74.8 |
| YOLOv3 (608) | AIC (220K) | 66.0 | 70.3 | 74.7 | 75.7 |
| Faster R-CNN | AIC (220K) | 67.1 | 71.1 | 75.3 | 76.3 |
| PicoDet-s (320) | AIC (220K) | 64.3 | 68.8 | 73.2 | 74.2 |
| RTMDet-nano (320) | AIC (220K) | 64.4 | 68.5 | 73.2 | 74.2 |
| RTMDet-m (640) | AIC (220K) | - | - | 75.7 | 76.6 |
The abstract and the README give 75.8 AP for RTMPose-m with flip test. That number is not in any table §1 tab. 4.
COCO-SinglePerson, 1045 images from val2017 (flip not used; GFLOPs include detector and pose) tab. 5. Setup: RTMDet-nano at 320x320 detector, AIC (220K) tab. 5.
| Model | Input | GFLOPs | AP |
|---|---|---|---|
| RTMPose-t | 256x192 | 0.67 | 72.1 |
| RTMPose-s | 256x192 | 0.91 | 77.1 |
| RTMPose-m | 256x192 | 2.23 | 82.4 |
| RTMPose-l | 256x192 | 4.47 | 83.5 |
| MoveNet Lightning (*) | 192x192 | 0.54 | 53.6 |
| MoveNet Thunder (*) | 256x256 | 2.44 | 64.8 |
| TinyPose, PicoDet-s, + internal data | 256x192 | 0.80 | 69.4 |
(*) Double inference, per the paper's table note tab. 5.
COCO-WholeBody v1.0, flip test used tab. 6. Whole-body AP and AR.
| Model | Input | GFLOPs | AP | AR |
|---|---|---|---|---|
| RTMPose-m* | 256x192 | 2.2 | 58.2 | 67.4 |
| RTMPose-l* | 256x192 | 4.5 | 61.1 | 70.0 |
| RTMPose-l* | 384x288 | 10.1 | 64.8 | 73.0 |
| RTMPose-x | 384x288 | 18.1 | 65.2 | 73.2 |
| RTMPose-x* | 384x288 | 18.1 | 65.3 | 73.3 |
| ZoomNAS (baseline) | 384x288 | 18.0 | 65.4 | 74.4 |
(*) Pre-trained on AIC+COCO tab. 6.
Other datasets, flip test used tab. 8 tab. 9.
| Dataset | Model | AP or PCKh@0.5 |
|---|---|---|
| AP-10K | RTMPose-m (ImageNet pre-training) | 68.4 |
| AP-10K | RTMPose-m* (AIC+COCO pre-training) | 72.2 |
| CrowdPose | RTMPose-m | 66.9 |
| CrowdPose | RTMPose-m* | 70.6 |
| MPII | RTMPose-m | 88.9 |
| MPII | RTMPose-m* | 90.7 |
Baselines in the same tables (HRNet, SimpleBaseline, SimCC, TokenPose) are between 63.7 and 90.2 tab. 8 tab. 9.
Speed, batch 1 tab. 10 tab. 11 tab. 12 §4.3. Setup: ncnn on a Snapdragon 865 with 4 threads, ONNX Runtime on an Intel i7-11700 with one thread, and TensorRT FP16 on a GTX 1660 Ti. Each run had 50 warm-up and 200 timed iterations §4.3. The paper says these were run with MMDeploy §4.3.
| Model | Input | GFLOPs | Snapdragon 865 FP16 (ms) | CPU ONNX (ms) | GPU TensorRT FP16 (ms) |
|---|---|---|---|---|---|
| RTMPose-t | 256x192 | 0.36 | 9.02 | 3.204 | 1.064 |
| RTMPose-s | 256x192 | 0.68 | 13.89 | 4.481 | 1.392 |
| RTMPose-m | 256x192 | 1.93 | 26.44 | 11.060 | 2.288 |
| RTMPose-l | 256x192 | 4.16 | 45.37 | 18.847 | 3.459 |
| TinyPose | 256x192 | 0.33 | 8.33 | 10.580 | 3.055 |
| HRNet-w32+UDP | 256x192 | 7.7 | - | 37.734 | 5.133 |
Pipeline speed with RTMDet-nano (320x320) as the detector tab. 12:
| Pose model | Pipeline AP | CPU (ms) | GPU (ms) | Mobile (ms) |
|---|---|---|---|---|
| RTMPose-t | 64.4 | 12.403 | 2.467 | 18.780 |
| RTMPose-s | 68.5 | 16.658 | 2.730 | 21.683 |
| RTMPose-m | 73.2 | 26.613 | 4.312 | 32.122 |
| RTMPose-l | 74.2 | 36.311 | 4.644 | 47.642 |
Paper vs code
Code: the official MMPose 1.x branch, linked from the paper, at commit
db4ebd6e0cf1b113e20a7cbf94cdaba4ca2fcb85. The paper links
open-mmlab/mmpose/tree/1.x/projects/rtmpose in the abstract. The repository
is Apache-2.0 LICENSE.
- Attention normalisation. Paper: A = (1/n) relu^2(...), and n is not defined in the text §3.3 eq. 2. Code: the same ReLU-squared kernel with no 1/n factor rtmcc_block.py.
- GAU projections. Paper: separate U and V projections from X, with Z feeding the attention §3.3 eq. 1 eq. 2. Code: one linear layer produces U, V and a shared base, which is then turned into Q and K by per-dimension scale and offset, after a scale norm rtmcc_block.py [code: rtmcc_block.py#L197] rtmcc_block.py.
- GAU activation. Paper: phi is not specified §3.3. Code: SiLU in the config rtmpose-m_8xb256-420e_coco-256x192.py and ReLU as the class default rtmcc_head.py.
- Batch size. Paper: 1024 tab. 7. Config: 256 per data loader on eight GPUs (the paper's hardware), which is 2048 in total [code: projects/rtmpose/rtmpose/body_2d_keypoint/rtmpose-m_8xb256-420e_coco-256x192.py#L11]. The auto-scale reference batch is 1024 . The paper does not say which setting produced the reported numbers.
- Two-stage split. Paper: §3.2 gives 180 strong epochs and 30 weak epochs, and tab. 7 lists 210 epochs for pre-training §3.2 tab. 7. The paper gives no split for the 420-epoch fine-tuning. The config uses 390 strong and 30 weak for it .
- Channel attention in the backbone. Config enables it [code: projects/rtmpose/rtmpose/body_2d_keypoint/rtmpose-m_8xb256-420e_coco-256x192.py#L71]. The paper does not mention it §3.1.
- Sigma constants. Paper gives sigma = sqrt(W_S / 16) eq. 6. The config uses 4.9 and 5.66, which match 384 and 512 bins. Other input sizes must recompute the formula .
- Flip test. Paper: method not stated; tables mark some results as flip and some as no-flip tab. 4 tab. 6. Code: averages the raw logits of the two passes before argmax rtmcc_head.py.
- Speed headline. Abstract: 430+ FPS on a GTX 1660 Ti for RTMPose-m §1. Table 11 gives 2.288 ms for the pose model alone, with TensorRT FP16, batch 1 tab. 11. The pipeline with RTMDet-nano and RTMPose-m takes 4.312 ms on the GPU tab. 12.
- Backbone initialisation. The RTMPose-m config starts from a UDP checkpoint trained on AIC+COCO . The COCO-only rows in tab. 4 are a separate setting §4.2 tab. 4.
Pitfalls
- The headline 75.8 AP has no table row. It is stated in the abstract and the intro with flip test §1. The table row for RTMPose-m with the best detector is 75.7 (RTMDet-m detector, flip off) tab. 4. Do not take 75.8 as a reproduction target without the detector and flip setting.
- RTMPose-s AP differs by table. The abstract's 72.2 AP is a COCO number §1. Table 10 gives AP(GT) 72.8 for the same model, and the paper does not define the GT column tab. 10.
- Speed numbers are pose-only unless noted. The 430+ FPS figure is the pose model alone, batch 1, TensorRT FP16 tab. 11. A detector plus pose pipeline runs at 4.312 ms per frame on the GPU, about 232 FPS by 1000 / 4.312 (my arithmetic) tab. 12.
- Table 1 disagrees with the text. The text gives 69.7 AP for the CSPNeXt-m baseline §3.1, while Table 1 lists SimCC* at 71.3 tab. 1. The caption does not name the backbone for Table 1.
- Loss scale depends on batch size. The code sums over the batch and averages over bins, then divides by K classification_loss.py. The loss grows with batch size, so a learning rate tuned for one batch size does not transfer.
- Weak-stage and Cutout details are in the code only. The paper gives "small" rotation and "turn off shift" with no numbers §3.2. The config uses scale [0.75, 1.25] and rotation 60 . The CoarseDropout hole sizes are in code only .
- Split ratio is config-only. The bin count W_S in eq. 6 depends on a split ratio of 2 bins per pixel. The paper never states that ratio; the config sets it .
- Skip-frame, pose NMS and OneEuro are not in the code I read. The paper describes them §3.5. The repo's project README mentions the pipeline, but I did not locate the implementation. Treat that part of the pipeline as unverified.
- Input normalisation constants are code-only . An implementation that uses ImageNet statistics will not match the released weights.
- Decoding is argmax with no sub-bin refinement. DARK is off in the config . The paper's HRNet+DARK baseline is a separate model tab. 11.
- Learning-rate warm-up and gradient clipping are code-only .
- The paper does not describe decoding. The code takes the argmax of each axis and divides by the split ratio, and uses the smaller peak as the score simcc_label.py post_processing.py.
Lineage
- Builds on: RTMDet for the CSPNeXt backbone and the training recipe §3.1 §3.2.
- Builds on: SimCC, the coordinate-classification formulation §3.1. The paper's bibliography entry has no arXiv id, and no card exists for it.
- Builds on: the GAU attention block from "Transformer Quality in Linear Time" §3.3. The bibliography has no arXiv id, and no card exists for it.
- Builds on: SORD ordinal-regression soft labels §3.4. No card exists for it.
- Compared against: heat-map methods (HRNet, SimpleBaseline), TinyPose, MoveNet, BlazePose and ZoomNAS tab. 4 tab. 5 tab. 6 tab. 10.
- Superseded by: not known from the content of this wiki.
Source and license
Adapted from “RTMPose: Real-Time Multi-Person Pose Estimation based on MMPose” by Tao Jiang, Peng Lu, Li Zhang, Ningsheng Ma, Rui Han, Chengqi Lyu, Yining Li, Kai Chen (arXiv:2303.07399), used under CC BY 4.0. Changes: condensed and restructured for implementation, with tables, equations and algorithms taken from the paper and its code, each cited to its source. Not affiliated with or endorsed by the authors. Code citations refer to https://github.com/open-mmlab/mmpose at commit db4ebd6e0cf1 (Apache-2.0). Any quoted code is shown under that license.