Ultralytics YOLO27: Join the waitlist.
Get Started
Computer Vision Foundations·Pick a Task·Lesson 5/10
Lessonbeginner

Pose Estimation and OBB

Read keypoints and rotated boxes from YOLO26, write a rule on top of each, and recognize when posture or angle is the real signal.

A care home needs to know when someone has fallen; a port needs to count ships packed side by side in a drone image. Plain boxes fail both: a box doesn't say whether a person is upright, and an upright box around a diagonal ship is mostly water. Pose estimation and OBB are built for exactly these two problems.

Outcome

Write a posture rule from YOLO26 keypoints and measure what rotated boxes save on aerial imagery.

Fast Track
If you already know your way around, here's the short version.
  1. Pose: a box plus 17 keypoints per person (COCO order: 0 nose, 5–6 shoulders, 11–12 hips, 15–16 ankles).

  2. Filter keypoints by confidence before using them; hidden joints still get coordinates.

  3. OBB: center, size, and angle (xywhr, angle in radians).

  4. Use pose when posture is the signal; OBB when objects lie at angles in top-down views; otherwise detection.

Hands-on

Pose: when posture is the signal#

Three runners on a waterfront path, each with a person box and a 17-keypoint skeleton from Ultralytics YOLO pose estimation

A pose model returns a box for each person plus a fixed list of keypoints. Models trained on COCO-Pose predict 17 per person — nose, eyes, ears, shoulders, elbows, wrists, hips, knees, and ankles — always in the same order, so index 0 is the nose, 11 the left hip, and 15 the left ankle. Each keypoint also has a confidence.

The simplest useful rule: a standing body is taller than it is wide; a body lying on the floor is wider than it is tall. Here it is on the bus image, using only keypoints the model is confident about:

from ultralytics import YOLO

model = YOLO("yolo26n-pose.pt")
result = model("https://ultralytics.com/images/bus.jpg", verbose=False)[0]

for xy, conf in zip(result.keypoints.xy, result.keypoints.conf):
    visible = xy[conf > 0.5]  # drop keypoints the model couldn't see
    width = float(visible[:, 0].max() - visible[:, 0].min())
    height = float(visible[:, 1].max() - visible[:, 1].min())
    posture = "unknown" if len(visible) < 5 else "lying" if width > height else "upright"
    print(f"{len(visible):2d}/17 keypoints visible, body {width:3.0f} px wide x {height:3.0f} px tall -> {posture}")
16/17 keypoints visible, body 117 px wide x 427 px tall -> upright
 6/17 keypoints visible, body  80 px wide x 359 px tall -> upright
16/17 keypoints visible, body  91 px wide x 384 px tall -> upright
 1/17 keypoints visible, body   0 px wide x   0 px tall -> unknown
 2/17 keypoints visible, body   3 px wide x   6 px tall -> unknown

Three people are clearly upright. The last two rows are the person cut off at the left edge and a low-confidence duplicate: with one or two visible keypoints there's nothing to judge, so the rule says "unknown" instead of guessing. A production fall detector adds time — lying for more than a few seconds, after a fast drop — through tracking.

Hidden joints still have coordinates

Symptom: skeletons with wrists in mid-air and posture rules that fire on half-visible people. Cause: keypoints.xy returns a position for all 17 keypoints, even ones the model couldn't see. Fix: filter with keypoints.conf, as above, and require a minimum number of visible keypoints before deciding anything.

Pose earns its place when the answer depends on what the body is doing: falls or lifting with a bent back for safety, joint angles and rep counts in sport as in workouts monitoring, or posture in livestock and fixed landmarks on parts with a custom keypoint layout. To answer "is there a person here?", use plain detection.

OBB: when angle matters#

Three yachts moored at angles in aerial imagery, each enclosed by a tight rotated box from Ultralytics YOLO OBB

A regular box has edges parallel to the image. An oriented bounding box adds a rotation, stored as center, width, height, and angle (xywhr). In top-down imagery — satellite, drone, overhead conveyor cameras — objects lie at every angle, and an upright box around a diagonal object is mostly background, as the yachts above would be. Measure how much on a drone shot of a marina:

from ultralytics import YOLO

model = YOLO("yolo26n-obb.pt")
result = model("https://ultralytics.com/images/boats.jpg", verbose=False)[0]

obb = result.obb
rotated_area = obb.xywhr[:, 2] * obb.xywhr[:, 3]  # width x height of each rotated box
x1, y1, x2, y2 = obb.xyxy.T  # the axis-aligned box that would enclose the same object
upright_area = (x2 - x1) * (y2 - y1)

print(f"{len(obb)} objects, classes: {sorted({result.names[int(c)] for c in obb.cls})}")
cx, cy, w, h, angle = obb.xywhr[0].tolist()
print(f"first box: center=({cx:.0f}, {cy:.0f}) size={w:.0f}x{h:.0f} angle={angle:.2f} rad")
ratio = float((upright_area / rotated_area).median())
print(f"an axis-aligned box would be {ratio:.1f}x larger (median)")
173 objects, classes: ['harbor', 'ship']
first box: center=(619, 850) size=33x108 angle=-0.25 rad
an axis-aligned box would be 2.1x larger (median)

An upright box covers about twice the area of the rotated box around the same boat, and the extra area is water. It hurts twice: NMS sees the upright boxes of neighboring boats overlapping and may suppress real ones, and any size estimate is inflated. OBB models trained on aerial datasets such as DOTA handle these scenes directly. Note the angle is in radians: -0.25 rad is about -14°.

Choosing between detection, pose, and OBB#

ScenarioBest taskWhy
Counting people on a sidewalkDetectionPresence and location are the answer
Detecting a fall in a care homePoseBody orientation is the signal
Counting squats in a gymPoseKnee and hip angles are the signal
Counting docked boats from a droneOBBBoats sit diagonally and tightly packed
Packages on a conveyor, seen from aboveOBBOrientation varies and neighbors touch
Cars at street levelDetectionCars appear roughly upright in the frame
Pose and OBB are detection plus one thing

Pose adds keypoints to each box; OBB adds an angle. If your code never reads the keypoints or the angle, a plain detector is faster to label, faster to run, and easier to debug.

Try It

Run the comparison in the solution on the marina image: the COCO-pretrained detector against the OBB model. Record both counts, then write one sentence on how much of the difference comes from box geometry and how much from what each model was trained on. Finally, name one problem in your own work where pose or OBB would beat plain detection, and the rule your code would apply.

Done When
You've finished the lesson when all of these are true.
  • You have both counts on boats.jpg and a one-sentence explanation of the gap.

  • You can point to the line in the posture rule that filters hidden keypoints and say why it's needed.

  • You have named one problem in your work for pose or OBB, with the rule your code would apply.

Show solution
from ultralytics import YOLO

image = "https://ultralytics.com/images/boats.jpg"
upright = YOLO("yolo26n.pt")(image, verbose=False)[0]
rotated = YOLO("yolo26n-obb.pt")(image, verbose=False)[0]
print(f"axis-aligned detector: {len(upright.boxes)} boxes")
print(f"OBB model: {len(rotated.obb)} rotated boxes")
axis-aligned detector: 0 boxes
OBB model: 173 rotated boxes

Most of the gap is training data, not geometry. yolo26n.pt was trained on COCO, where boats are mostly photographed from the side; from above it finds nothing at the default threshold (drop conf to 0.05 and it calls a few of them bottles). The OBB model was trained on aerial imagery. Geometry still matters on top of that: even a detector trained on aerial boats would draw boxes about twice too large and fight NMS in tightly packed rows. Both the task and the training data have to match the deployment view.

What's next

The marina showed that training data matters as much as the task. Next: checking whether a dataset matches the world you deploy into.