---
title: "Build a posture checker with pose estimation"
description: "Use MediaPipe Pose and a webcam to catch slouching and forward head in real time. Calculate angles from landmarks, calibrate to yourself, and get reminders."
url: https://articles.sythra.ai/articles/posture-checker-pose-estimation-python
slug: posture-checker-pose-estimation-python
author: "vaibhavkothari"
author_url: https://articles.sythra.ai/writers/vaibhavkothari
date_published: 2026-11-12T21:11:00.153Z
date_modified: 2026-11-12T21:11:00.153Z
topics: ["Python", "Mediapipe", "Opencv", "Computer Vision"]
reading_time_minutes: 7
publisher: "Sythra"
publisher_url: https://sythra.ai
access: free
language: en
---

# Build a posture checker with pose estimation

> Use MediaPipe Pose and a webcam to catch slouching and forward head in real time. Calculate angles from landmarks, calibrate to yourself, and get reminders.

Source: https://articles.sythra.ai/articles/posture-checker-pose-estimation-python · Author: vaibhavkothari · Published: 2026-11-12 · Reading time: 7 min · Topics: Python, Mediapipe, Opencv, Computer Vision

Hours at a desk turn into a slow forward slump. A webcam and a pose model can watch for it and nudge you — no wearable required. This project turns body landmarks into two simple measurements, calibrates them to *your* posture, and warns you when you drift. It builds on the same pose pipeline as the [push-up counter](https://articles.sythra.ai/articles/pushup-counter-pose-estimation-python).

## How it works

Pose estimation gives us points. Posture is geometry on those points. We measure two things:

1. **Shoulder tilt** — is one shoulder higher than the other? (works from the front)
2. **Forward head** — how far is the ear ahead of the shoulder? (works best from the side, or three-quarter view)

You pick one camera angle. Front-facing is the easiest to set up on a laptop, so we start there and add the side-view measurement afterwards.

## Install

```bash
python -m pip install opencv-python mediapipe numpy
```

## Landmarks we use

MediaPipe Pose returns 33 landmarks with `x`, `y` normalised to 0–1 and a `visibility` score.

| Landmark | Constant |
|---|---|
| Left / right ear | `PoseLandmark.LEFT_EAR`, `RIGHT_EAR` |
| Left / right shoulder | `PoseLandmark.LEFT_SHOULDER`, `RIGHT_SHOULDER` |
| Nose | `PoseLandmark.NOSE` |

## Step 1 — Helper functions

```python
import math
import cv2
import mediapipe as mp

mp_pose = mp.solutions.pose
mp_draw = mp.solutions.drawing_utils
L = mp_pose.PoseLandmark


def point(lm, idx, w, h):
    p = lm[idx]
    return p.x * w, p.y * h, p.visibility


def tilt_degrees(left, right):
    """Angle of the line joining two points relative to horizontal."""
    dx = right[0] - left[0]
    dy = right[1] - left[1]
    return math.degrees(math.atan2(dy, dx))


def distance(a, b):
    return math.hypot(a[0] - b[0], a[1] - b[1])
```

## Step 2 — Measurements

**Shoulder tilt.** With a mirrored front view, if the shoulders are level the line between them is horizontal, so the angle is near 0. A persistent lean shows up as a larger angle.

**Head drop (front view).** When you slouch toward the screen your head moves down relative to your shoulders and your shoulders rise in the frame. A scale-independent measure is the vertical gap between nose and shoulder-midpoint, divided by shoulder width:

```python
def posture_metrics(lm, w, h):
    ls = point(lm, L.LEFT_SHOULDER, w, h)
    rs = point(lm, L.RIGHT_SHOULDER, w, h)
    nose = point(lm, L.NOSE, w, h)

    if min(ls[2], rs[2], nose[2]) < 0.5:
        return None                                   # landmarks not reliable

    shoulder_width = distance(ls, rs)
    if shoulder_width < 1:
        return None

    mid = ((ls[0] + rs[0]) / 2, (ls[1] + rs[1]) / 2)
    return {
        "tilt": abs(tilt_degrees(ls, rs)),
        "head_gap": (mid[1] - nose[1]) / shoulder_width,   # bigger = head held higher
    }
```

Dividing by `shoulder_width` is the important trick: the numbers stay meaningful whether you sit close to the camera or far from it.

## Step 3 — Calibrate to yourself

There is no universal "good" number. Body proportions and camera angle vary, so measure a few seconds of your *good* posture and use that as the baseline:

```python
def calibrate(cap, pose, seconds=5):
    import time
    tilt, gap = [], []
    start = time.time()
    while time.time() - start < seconds:
        ok, frame = cap.read()
        if not ok:
            continue
        frame = cv2.flip(frame, 1)
        h, w = frame.shape[:2]
        res = pose.process(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
        if res.pose_landmarks:
            m = posture_metrics(res.pose_landmarks.landmark, w, h)
            if m:
                tilt.append(m["tilt"])
                gap.append(m["head_gap"])
        cv2.putText(frame, "Sit up straight - calibrating...", (20, 40),
                    cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), 2)
        cv2.imshow("Posture", frame)
        cv2.waitKey(1)

    if not gap:
        raise RuntimeError("No pose detected during calibration")
    return {
        "tilt": sum(tilt) / len(tilt),
        "head_gap": sum(gap) / len(gap),
    }
```

## Step 4 — Decide what counts as bad

Compare each frame with the baseline and allow a margin:

```python
TILT_MARGIN = 6.0          # degrees of extra shoulder tilt
GAP_DROP_RATIO = 0.80      # head_gap falling below 80% of baseline = slouching


def judge(metrics, baseline):
    problems = []
    if metrics["tilt"] > baseline["tilt"] + TILT_MARGIN:
        problems.append("shoulders uneven")
    if metrics["head_gap"] < baseline["head_gap"] * GAP_DROP_RATIO:
        problems.append("head dropping / slouching")
    return problems
```

## Step 5 — Avoid false alarms with a timer

Reaching for a coffee is not bad posture. Only warn when the problem lasts:

```python
import time

BAD_SECONDS = 5
bad_since = None


def update_alert(problems):
    """Return True once the problem has persisted long enough."""
    global bad_since
    if problems:
        if bad_since is None:
            bad_since = time.time()
        return time.time() - bad_since >= BAD_SECONDS
    bad_since = None
    return False
```

## Step 6 — The full loop

```python
def main():
    cap = cv2.VideoCapture(0)
    with mp_pose.Pose(
        min_detection_confidence=0.6,
        min_tracking_confidence=0.6,
    ) as pose:
        baseline = calibrate(cap, pose)
        print("Baseline:", baseline)

        while True:
            ok, frame = cap.read()
            if not ok:
                break
            frame = cv2.flip(frame, 1)
            h, w = frame.shape[:2]

            res = pose.process(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
            status, colour = "no person", (200, 200, 200)

            if res.pose_landmarks:
                mp_draw.draw_landmarks(frame, res.pose_landmarks, mp_pose.POSE_CONNECTIONS)
                m = posture_metrics(res.pose_landmarks.landmark, w, h)
                if m:
                    problems = judge(m, baseline)
                    if update_alert(problems):
                        status = "FIX POSTURE: " + ", ".join(problems)
                        colour = (0, 0, 255)
                    elif problems:
                        status, colour = "check posture...", (0, 165, 255)
                    else:
                        status, colour = "good", (0, 200, 0)

            cv2.putText(frame, status, (20, 40),
                        cv2.FONT_HERSHEY_SIMPLEX, 1, colour, 2)
            cv2.imshow("Posture", frame)
            if cv2.waitKey(1) & 0xFF == ord("q"):
                break

    cap.release()
    cv2.destroyAllWindows()


if __name__ == "__main__":
    main()
```

## Side-view version: forward head

Place the camera to your side. Now the ear sitting ahead of the shoulder is visible directly. Measure the angle between the shoulder→ear line and the vertical:

```python
def forward_head_angle(lm, w, h, side="left"):
    ear_id = L.LEFT_EAR if side == "left" else L.RIGHT_EAR
    sh_id = L.LEFT_SHOULDER if side == "left" else L.RIGHT_SHOULDER
    ear = point(lm, ear_id, w, h)
    sh = point(lm, sh_id, w, h)
    dx = ear[0] - sh[0]
    dy = sh[1] - ear[1]               # positive when the ear is above the shoulder
    return abs(math.degrees(math.atan2(dx, dy)))
```

An upright head gives a small angle (ear almost straight above the shoulder). As the head juts forward the angle grows. Calibrate the same way and flag when it exceeds your baseline by about 10–15 degrees.

## Send a desktop reminder

Beeps and on-screen text are easy to miss. On Windows:

```python
import winsound
winsound.Beep(880, 300)
```

For cross-platform notifications, try the `plyer` package (`notification.notify(title=..., message=...)`).

## Accuracy and privacy notes

- Good, even lighting matters more than camera quality. Avoid a bright window behind you.
- The model reports landmarks even when guessing. The `visibility` check in `posture_metrics` is what stops it judging you on invented points.
- Everything here runs locally. No frames are saved or uploaded, and you should keep it that way if you share the tool with others.
- This is a nudge tool, not a medical device. Persistent pain deserves a professional, not a webcam.

## FAQ

### How can I detect bad posture with Python?

Use a pose model like MediaPipe Pose to get shoulder, ear and nose positions, calculate shoulder tilt or the ear-to-shoulder angle, and compare them with a baseline captured while you sit correctly.

### Why calibrate instead of using fixed thresholds?

Camera angle, distance and body proportions change the raw numbers. A baseline from your own good posture makes the comparison fair, and normalising by shoulder width keeps it scale-independent.

### What is the visibility score in MediaPipe Pose?

Each landmark has a `visibility` value from 0 to 1 estimating how likely it is to be visible. Ignore landmarks with low visibility so you do not act on guessed positions.

### Does this work with a laptop webcam?

Yes. A front view from a laptop works for shoulder tilt and head drop. For forward head posture, a side view gives a cleaner measurement.

## Next reading on Sythra Articles

- [Build a push-up counter with pose estimation](https://articles.sythra.ai/articles/pushup-counter-pose-estimation-python)
- [Draw in the air with MediaPipe finger tracking](https://articles.sythra.ai/articles/draw-in-air-mediapipe-opencv)
- [Detect colors and track objects with OpenCV HSV](https://articles.sythra.ai/articles/opencv-color-detection-hsv-tracking)

## Glossary (terms defined in this article)
- **Pose estimation** (medium) — Finding the position of body joints such as shoulders, ears and hips in an image
