Evaluation & Metrics
Percentage of Correct Keypoints
The family of hit-rate metrics for keypoints, PCK, PCKh, PCP and their 3D and correspondence forms, which count a predicted point or limb as correct when its error falls under a threshold scaled by the object, a body part or the limb itself; their definitions, implementations and the conventions that make numbers incomparable.
beginner
The percentage of correct keypoints (PCK) is the share of predicted keypoints that lie within a threshold distance of their ground truth, where the threshold is a fraction of a reference length: the size of the object’s box, the length of a body part, or a fixed physical distance in 3D. It is a hit rate, between 0 and 100% (or 0 and 1), that ignores how far a missed keypoint is off and how close a hit is. Ham et al. (2016), in semantic correspondence, expand the acronym as “probability of correct keypoint”. Its relatives share the counting idea and change the unit being counted: PCKh normalizes by head size, the percentage of correct parts (PCP) scores limbs by their two endpoints, and 3D PCK uses a metric distance. This article collects the family; the human pose versions and their benchmarks are described in human pose estimation and MPII Human Pose, and PCK across animals, hands and objects in keypoint detection.
Formula
For scored keypoints with predictions , ground truth and a reference length for the object or part that keypoint belongs to,
Three choices define a member of the family: the reference length , the threshold , and whether a distance equal to the threshold counts () or not (). As human pose estimation reports, Yang and Ramanan (2013) took of the tightest box around a person’s annotated keypoints; MPII’s PCKh takes the head size, and its code compares with (MPII Human Pose).
The percentage of correct parts scores a limb instead of a point. With predicted endpoints and the limb’s annotated length , the strict form requires both endpoints to be close, and the loose form, which Human Pose Estimation describes as the variant implemented in the Buffy toolkit, requires only their average:
A limb that passes the strict test always passes the loose one, so loose PCP is never lower.
Intuition
PCK asks a yes-or-no question per keypoint: is the prediction close enough to be useful? Dividing by a reference length makes “close enough” scale with the object, so a 40-pixel animal and a 400-pixel one are judged by the same rule when their errors scale with them. The threshold then sets which errors matter. In our reading, a loose threshold such as counts any prediction on the right part of the body; a tight one, such as PCKh at 0.1, separates methods by localization precision. Sweeping traces a curve, the fraction of keypoints with normalized error below each threshold, whose area summarizes precision over the swept thresholds; MPII’s evaluation code computes this AUC beside PCKh@0.5.
Interpretation
100% means every scored keypoint fell within its threshold; 0% means none did. The number is meaningful only with its protocol: the normalizer, , the joint set, how missing predictions and unannotated joints are treated, and whether the total pools all joints or averages per-joint scores. MPII’s Total pools joints, so frequently annotated joints weigh more (MPII Human Pose). Two results are comparable only when all of these match; keypoint detection lists how the thresholds differ between benchmarks.
When to Use It
PCK fits single-object evaluation in which each object has a known set of named keypoints and the question is how many are placed acceptably: single-person pose with the person given, animal and object keypoints with the box given, alignment between two images of the same category, and 3D joints in a known metric frame. It does not handle detection; when objects must be found and matched first, object keypoint similarity with average precision, or MPII’s multi-person AP, take its place. When the size of the errors matters, not just their count, MPJPE or a normalized mean error reports it.
Failure Cases
- Foreshortening in PCP. A limb that points toward the camera projects short, so the tolerance of half its projected length shrinks while the error of a 3D-consistent prediction does not. Yang and Ramanan made this point, as Human Pose Estimation reports, and, as MPII Human Pose describes, Andriluka et al.’s PCPm uses the mean length of the part over the test set instead, so that foreshortened parts are not held to a stricter threshold. The example below shows the effect.
- Pose-dependent normalizers. A box or torso normalizer grows and shrinks with articulation and viewpoint, so the same errors score differently in different poses; MPII Human Pose shows it with PCK by box, torso and head.
- Thresholds and ties. The inclusive and strict comparisons agree on continuous coordinates, but on integer pixel coordinates some distances equal an integer threshold exactly; the example shows a gap of about 0.3 points.
- Hit rate, not error. A prediction just outside the threshold and one across the image count the same, and so do a perfect prediction and one just inside; a loose threshold saturates.
- Names that look alike. CUB-200-2011’s and Keypoint-5’s “PCP” counts a single keypoint as correct within 1.5 standard deviations of the annotators’ spread, not a limb (CUB-200).
Variants
- PCK with a box normalizer (Yang and Ramanan, 2013), used for people, rigid objects and animals with benchmark-specific (keypoint detection).
- Part-normalized PCK. PCKh with the head (Andriluka et al., 2014; code details in MPII Human Pose), other body parts for hands and horses (keypoint detection), and the torso for PDJ and FLIC (human pose estimation).
- PCP, strict and loose, and PCPm, as above.
- Correspondence PCK. In semantic correspondence, keypoints of one image are transferred to another image of the same category by the estimated flow and compared with that image’s annotated keypoints. Ham et al. (2016, §5.3) count them as correct when they lie within pixels, with and the height and width of the object’s bounding box. Their wording, keypoints that “lie within max pixels of the predicted points”, fixes no choice between and . They report PCK as a fraction: at on their PF dataset, 0.56 for proposal flow (LOM with selective search proposals) against 0.38 for SIFT Flow and 0.37 for DSP (Table 2), and at on the 12 rigid PASCAL classes, 0.17 (LOM with selective search, as for PHM and for DSP) against 0.26 for FlowWeb, which uses more than two images (Table 4). The normalizer is the object’s box, not the keypoints’ box.
- 3D PCK and NPCK. In 3D, the threshold is a distance: 150 mm on MPI-INF-3DHP, with the AUC over a range of thresholds (3D human pose estimation). Rhodin et al. (2018) define PCK as the percentage of joints within 15 cm and NPCK as the same after scaling the prediction by the single factor that minimizes its squared distance to the label; MPJPE gives the scale and the related NMPJPE.
- Training accuracy in HRNet’s code. The
accuracyfunction of HRNet’sevaluate.py(commit 6f69e46), described in its docstring as accuracy according to PCK, compares the argmax cells of the predicted and target heatmaps (get_max_predsininference.py). It divides the horizontal coordinate by a tenth of the heatmap’s height and the vertical coordinate by a tenth of its width, counts a joint correct when the normalized distance is strictly less than 0.5 (np.lessindist_acc), skips joints whose target has a coordinate of 1 or less, and averages the per-joint accuracies. In our reading, its values are therefore not comparable with PCK or PCKh reported on a benchmark. - Point tracking. TAP-Vid’s counts points within fixed pixel thresholds with a strict comparison; point tracking describes it.
Example: strict PCP, loose PCP and PCK on foreshortened limbs
The script builds 100,000 limbs of length pixels in 3D, each with its proximal joint in the image plane and a random direction within the image, and tilts them by an inclination toward an orthographic camera that looks along the axis, so that a limb’s annotated length in the image is . Each predicted endpoint is the true 3D endpoint moved by a fixed distance pixels in a direction drawn uniformly on the sphere, independently for the two endpoints, and is then projected. The fixed error length, its uniform direction, and the box side pixels used for PCK at (a 20-pixel threshold, for a figure whose box this limb does not set) are our choices, not a benchmark’s. The same random numbers are used at every , so the rows differ only by the tilt. Every measure is computed with and with , on continuous coordinates and on coordinates rounded to whole pixels.
Under this construction the 2D error is with uniform on , so for and 1 beyond. Strict PCP is then with , which is 1 until falls below , at ; PCK is at every ; loose PCP is computed by a one-dimensional quadrature. The script prints these references beside the counts. It uses NumPy 2.3.5 and runs in about a second.
import os
os.environ["OMP_NUM_THREADS"] = os.environ["OPENBLAS_NUM_THREADS"] = "1"
os.environ["MKL_NUM_THREADS"] = "1"
import numpy as np
L, E, B, ALPHA, N, SEED = 100.0, 30.0, 200.0, 0.1, 100_000, 0
THETAS = [0, 20, 40, 50, 55, 60, 65, 70, 75, 80] # limb inclination toward the camera, degrees
def F(c):
"""P(2D error <= c) when a 3D error of length E, uniform in direction, is projected orthographically."""
c = np.asarray(c, float)
out = np.where(c >= E, 1.0, 1.0 - np.sqrt(np.clip(1.0 - (c / E) ** 2, 0, None)))
return np.where(c < 0, 0.0, out)
def loose_reference(c, m=2_000_000):
"""P((r1 + r2) / 2 <= c) by a midpoint rule over r1 = E sqrt(1 - t^2), t uniform on [-1, 1]."""
t = (np.arange(m) + 0.5) / m * 2 - 1
return F(2 * c - E * np.sqrt(1 - t ** 2)).mean()
rng = np.random.default_rng(SEED)
psi = rng.uniform(0, 2 * np.pi, N) # in-plane direction of each limb
p0 = np.c_[256 + rng.uniform(0, 1, (N, 2)), np.zeros(N)] # proximal joint, in the image plane
u = rng.normal(size=(2, N, 3)); u /= np.linalg.norm(u, axis=2, keepdims=True) # error directions
def scene(theta, rounding):
th = np.deg2rad(theta)
d = np.c_[np.cos(th) * np.cos(psi), np.cos(th) * np.sin(psi), np.full(N, np.sin(th))]
gt3 = np.stack([p0, p0 + L * d]) # (endpoint, limb, xyz)
pr3 = gt3 + E * u # fixed 3D error of length E
gt2, pr2 = gt3[..., :2], pr3[..., :2] # orthographic camera along z
if rounding: # annotations and predictions on pixels
gt2, pr2 = np.rint(gt2), np.rint(pr2)
return gt3, pr3, gt2, pr2
def pcp_pck(gt2, pr2, inclusive=True):
"""Counts: strict PCP (both endpoints), loose PCP (endpoint mean), PCK (endpoints, alpha * B)."""
ok = np.less_equal if inclusive else np.less
dist = np.linalg.norm(pr2 - gt2, axis=2) # (2, N)
half = 0.5 * np.linalg.norm(gt2[1] - gt2[0], axis=1) # half the annotated limb length
strict = (ok(dist[0], half) & ok(dist[1], half)).sum()
loose = ok(dist.mean(axis=0), half).sum()
pck = ok(dist, ALPHA * B).sum()
return int(strict), int(loose), int(pck)
# ---- checks on the data and the implementation
dev = {"3D error": 0.0, "3D length": 0.0, "2D length": 0.0, "2D error <= E": 0.0, "2D error vs theta": 0.0}
err0 = None
for th in THETAS:
gt3, pr3, gt2, pr2 = scene(th, False)
dev["3D error"] = max(dev["3D error"], np.abs(np.linalg.norm(pr3 - gt3, axis=2) - E).max())
dev["3D length"] = max(dev["3D length"], np.abs(np.linalg.norm(gt3[1] - gt3[0], axis=1) - L).max())
dev["2D length"] = max(dev["2D length"], np.abs(np.linalg.norm(gt2[1] - gt2[0], axis=1)
- L * np.cos(np.deg2rad(th))).max())
dev["2D error <= E"] = max(dev["2D error <= E"], (np.linalg.norm(pr2 - gt2, axis=2) - E).max())
err0 = pr2 - gt2 if err0 is None else err0
dev["2D error vs theta"] = max(dev["2D error vs theta"], np.abs((pr2 - gt2) - err0).max())
print("data checks (max deviation, pass at <= 1e-9):",
", ".join(f"{k} {v:.1e} {'ok' if v <= 1e-9 else 'FAIL'}" for k, v in dev.items()))
_, _, gi, pi = scene(60, True)
print("integer condition: all coordinates integral:", bool(np.all(gi == np.round(gi)) and np.all(pi == np.round(pi))))
g = np.array([[[0.0, 0.0]], [[100.0, 0.0]]]) # one limb of length 100, tolerance 50
for e0, e1 in [((50, 0), (0, 0)), ((60, 0), (10, 0)), ((30, 40), (0, -50))]:
p = g + np.array([[e0], [e1]], float)
print(f"hand case errors {e0} {e1}: <= strict/loose/pck {pcp_pck(g, p, True)} < {pcp_pck(g, p, False)}")
gt3, pr3, gt2, pr2 = scene(60, False)
loop = sum(1 for i in range(1000)
if all(np.hypot(*(pr2[k, i] - gt2[k, i])) <= 0.5 * np.hypot(*(gt2[1, i] - gt2[0, i])) for k in (0, 1)))
print("loop vs vectorized strict PCP, first 1000 limbs at 60 deg:", loop, pcp_pck(gt2[:, :1000], pr2[:, :1000])[0])
# ---- the table
print("\ncontinuous coordinates (percent; ref = closed form or quadrature; ties = count(<=) - count(<))")
print("theta len/L strict ref loose ref PCK ref ties s/l/p")
res = {}
for th in THETAS:
c = 0.5 * L * np.cos(np.deg2rad(th))
s, l, k = pcp_pck(*scene(th, False)[2:])
s2, l2, k2 = pcp_pck(*scene(th, False)[2:], inclusive=False)
ref = (100 * F(c) ** 2, 100 * loose_reference(c), 100 * F(ALPHA * B))
res[th] = (s, l, k, ref, (s - s2, l - l2, k - k2))
print(f"{th:5d} {np.cos(np.deg2rad(th)):.3f} {100*s/N:7.2f} {ref[0]:7.2f} {100*l/N:7.2f} {ref[1]:7.2f}"
f" {100*k/(2*N):6.2f} {ref[2]:6.2f} {s-s2}/{l-l2}/{k-k2}")
print("\ninteger pixel coordinates (percent with <=, then with <)")
print("theta strict<= strict< loose<= loose< PCK<= PCK<")
ires = {}
for th in THETAS:
a, b = pcp_pck(*scene(th, True)[2:]), pcp_pck(*scene(th, True)[2:], inclusive=False)
ires[th] = (a, b)
print(f"{th:5d} {100*a[0]/N:7.2f} {100*b[0]/N:7.2f} {100*a[1]/N:7.2f} {100*b[1]/N:7.2f}"
f" {100*a[2]/(2*N):6.2f} {100*b[2]/(2*N):6.2f}")
# ---- hypotheses, read mechanically from the counts above
cross = np.degrees(np.arccos(2 * E / L))
lo, hi = [t for t in THETAS if t <= cross], [t for t in THETAS if t > cross]
h1 = (all(res[t][0] == N for t in lo) and all(res[a][0] > res[b][0] for a, b in zip(hi, hi[1:]))
and 100 * res[80][0] / N < 1.0)
h2 = (all(res[t][1] >= res[t][0] for t in THETAS) and all(res[t][1] == N for t in lo)
and all(res[t][1] > res[t][0] for t in hi))
h3 = len({res[t][2] for t in THETAS}) == 1
devs = [abs(100 * res[t][i] / (N if i < 2 else 2 * N) - res[t][3][i]) for t in THETAS for i in range(3)]
h4 = max(devs) <= 0.8
h5 = (all(res[t][4] == (0, 0, 0) for t in THETAS) and all(all(b <= a for a, b in zip(*ires[t])) for t in THETAS)
and all(ires[t][0][2] > ires[t][1][2] for t in THETAS))
print(f"\ncrossover theta {cross:.2f} deg; max |empirical - reference| {max(devs):.3f} points (tolerance 0.8)")
print("H1", "held" if h1 else "failed", "| H2", "held" if h2 else "failed", "| H3", "held" if h3 else "failed",
"| H4", "held" if h4 else "failed", "| H5", "held" if h5 else "failed")
data checks (max deviation, pass at <= 1e-9): 3D error 4.6e-14 ok, 3D length 5.7e-14 ok, 2D length 5.7e-14 ok, 2D error <= E 0.0e+00 ok, 2D error vs theta 5.7e-14 ok
integer condition: all coordinates integral: True
hand case errors (50, 0) (0, 0): <= strict/loose/pck (1, 1, 1) < (0, 1, 1)
hand case errors (60, 0) (10, 0): <= strict/loose/pck (0, 1, 1) < (0, 1, 1)
hand case errors (30, 40) (0, -50): <= strict/loose/pck (1, 1, 0) < (0, 0, 0)
loop vs vectorized strict PCP, first 1000 limbs at 60 deg: 173 173
continuous coordinates (percent; ref = closed form or quadrature; ties = count(<=) - count(<))
theta len/L strict ref loose ref PCK ref ties s/l/p
0 1.000 100.00 100.00 100.00 100.00 25.45 25.46 0/0/0
20 0.940 100.00 100.00 100.00 100.00 25.45 25.46 0/0/0
40 0.766 100.00 100.00 100.00 100.00 25.45 25.46 0/0/0
50 0.643 100.00 100.00 100.00 100.00 25.45 25.46 0/0/0
55 0.574 49.88 49.91 86.52 86.62 25.45 25.46 0/0/0
60 0.500 20.02 20.00 54.22 54.29 25.45 25.46 0/0/0
65 0.423 8.39 8.42 28.44 28.34 25.45 25.46 0/0/0
70 0.342 3.19 3.18 10.72 10.64 25.45 25.46 0/0/0
75 0.259 0.98 0.96 2.82 2.76 25.45 25.46 0/0/0
80 0.174 0.19 0.18 0.53 0.50 25.45 25.46 0/0/0
integer pixel coordinates (percent with <=, then with <)
theta strict<= strict< loose<= loose< PCK<= PCK<
0 100.00 100.00 100.00 100.00 25.46 25.18
20 100.00 100.00 100.00 100.00 25.48 25.21
40 100.00 100.00 100.00 100.00 25.48 25.19
50 100.00 100.00 100.00 100.00 25.48 25.20
55 50.68 50.43 86.52 86.52 25.46 25.17
60 20.09 20.00 54.22 54.22 25.48 25.18
65 8.43 8.39 28.46 28.46 25.49 25.21
70 3.24 3.21 10.69 10.69 25.50 25.21
75 0.97 0.96 2.79 2.79 25.48 25.19
80 0.20 0.19 0.52 0.52 25.48 25.18
crossover theta 53.13 deg; max |empirical - reference| 0.100 points (tolerance 0.8)
H1 held | H2 held | H3 held | H4 held | H5 held
The first part checks the construction: the 3D error and length, the projected length and the independence of the 2D error from hold to rounding error, three hand-made limbs give the expected strict, loose and PCK verdicts under both comparisons, and an explicit loop agrees with the vectorized count. In the continuous table, strict PCP stays at 100% while half the projected length exceeds the largest possible 2D error, then collapses between 50° and 80°, from about 50% at 55° to under 1% at 75°, although the predictions are equally good in 3D at every angle. Loose PCP falls too, but later: at 60° it accepts 54% of limbs against strict PCP’s 20%, since a limb with one endpoint far off and the other close passes when their average is under the tolerance, as the second hand case shows. PCK with a fixed box does not move, because its threshold does not depend on the limb, and the counts are identical in every row. Every value lies within about 0.1 points (0.100 as printed) of its closed-form or quadrature value.
The inclusive and strict comparisons give identical counts on continuous coordinates, where an exact tie has probability zero. On whole pixels, PCK with is 0.27 to 0.30 points higher than with at every angle, because integer offsets such as or lie exactly 20 pixels away, and strict PCP shows smaller gaps (0.25 points at 55°). Rounding also moves strict PCP itself: at 55°, 50.68% on whole pixels against 49.88% on continuous coordinates. In our reading, PCP numbers for the same predictions can differ widely by the variant alone, and PCK on integer coordinates depends on the comparison operator in its last few tenths of a point.
How the example was planned and run. The five hypotheses were written from the derivation above before any script ran: strict PCP is 100% up to 53.1° and then strictly decreasing, below 1% at 80° (H1); loose PCP is never below strict, equals 100% at the same angles, and is strictly higher beyond them (H2); PCK counts are identical at every angle (H3); every value lies within 0.8 points, about five binomial standard errors of a 50% rate over 100,000 limbs, of its reference (H4); and the comparisons agree on continuous coordinates, while on whole pixels the strict count never exceeds the inclusive one and PCK’s inclusive count is higher at every angle (H5). A planning script then computed the references and ran the same construction on another seed; its first version stopped with an array-shape error after printing the closed-form values and before printing any table row, and was corrected. The bars were fixed with that script’s output in view, so the example illustrates the derivation rather than testing it. H1, H2, H3, H4 and H5 each held, and every data and implementation check held. The script shown is the first run, unchanged from the pilot, and two runs give identical output.
Related Metrics
- Object keypoint similarity replaces the hard threshold by a soft per-keypoint credit and drives keypoint AP (object keypoint similarity).
- MPJPE averages 3D joint errors instead of counting hits (MPJPE).
- Normalized mean error for face landmarks divides the mean error by an inter-ocular distance (keypoint detection).
Related
- MPII Human Pose
A benchmark for 2D human pose estimation from the Max Planck Institute for Informatics, released in 2014, with about 25,000 YouTube frames and 40,000 people annotated with 16 body joints, collected through a taxonomy of everyday activities; its single-person track is scored with PCKh, a joint-distance measure normalized by head size, and its multi-person track with average precision over body joints.
- Object Keypoint Similarity
The per-object similarity between predicted and annotated keypoints that COCO uses in place of IoU, its Gaussian keypoint similarity and constants, what it implies under Gaussian error, Ronchi and Perona's error taxonomy and rescoring analysis, and how evaluation code, toolkit forks and OKS-based NMS compute it.
- Mean Per Joint Position Error
The average Euclidean distance between predicted and true 3D joints, and the family built on it; absolute and root-relative error, the choice of root and joint set, scale-normalized N-MPJPE, Procrustes-aligned PA-MPJPE, root position error and per-vertex error, with what each alignment removes and what it hides.
- Point Tracking (Tracking Any Point)
Following arbitrary query points through a whole video, including through occlusion, with a visibility flag in every frame; the TAP-Vid benchmark and its measures, and trackers from Particle Video and PIPs to TAP-Net, TAPIR and CoTracker.
- 3D Human Pose Estimation
Recovering the 3D positions of a person's body joints from one image, a video or several calibrated cameras; lifting 2D joints, volumetric heatmaps, temporal convolutions, learnable triangulation, SMPL body models in outline, the Human3.6M, MPI-INF-3DHP and 3DPW protocols, and MPJPE with its Procrustes-aligned variant.
- Caltech-UCSD Birds (CUB-200)
A dataset of Flickr photographs of 200 bird species from Caltech and UC San Diego, released as CUB-200 in 2010 and extended in 2011 as CUB-200-2011 with 11,788 images, boxes, 15 part locations clicked by five crowd workers per image and 312 binary attributes; its part-localization benchmark scores errors in units of the annotators' own spread.
References
- Yang, Y. & Ramanan, D. (2013). Articulated Human Detection with Flexible Mixtures of Parts. IEEE Transactions on Pattern Analysis and Machine Intelligence, 35(12), 2878–2890 (not read).
- Andriluka, M., Pishchulin, L., Gehler, P. & Schiele, B. (2014). 2D Human Pose Estimation: New Benchmark and State of the Art Analysis. IEEE Conference on Computer Vision and Pattern Recognition (CVPR), 3686–3693 (not read).
- Ham, B., Cho, M., Schmid, C. & Ponce, J. (2016). Proposal Flow. IEEE Conference on Computer Vision and Pattern Recognition (CVPR).
- Rhodin, H., Spörri, J., Katircioglu, I., Constantin, V., Meyer, F., Müller, E., Salzmann, M. & Fua, P. (2018). Learning Monocular 3D Human Pose Estimation from Multi-view Images. IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR).
- Sun, K., Xiao, B. et al. deep-high-resolution-net.pytorch, official HRNet code: lib/core/evaluate.py and lib/core/inference.py (commit 6f69e46, 2022), GitHub.