Skip to content

Repository files navigation

PostureFix

tests license: MIT

Hyper-accurate, real-time desk-posture monitoring from a Mac webcam. Works shirtless, survives camera bumps, and supports optional skin stickers for clinical-grade spine tracking. Everything runs locally — no video, no measurements, and no telemetry ever leave your machine.

Every accuracy-critical path is gated so it degrades to "no reading" instead of silently reporting wrong numbers. A plausible wrong angle is worse than a blank one, because you act on it.

Two implementations live here: a native Swift app (the thing you actually run all day) and a Python engine (the accuracy reference, with sticker tracking and camera-motion compensation the Swift app doesn't have).

Quick start — Mac app (native Swift)

Requires macOS 14+ and a Swift toolchain (Xcode or the Command Line Tools).

git clone https://github.com/adnanakil/PostureFix.git
cd PostureFix
scripts/build_swift_app.sh     # builds and installs PostureFix.app
open -a PostureFix

SwiftUI + Apple Vision (Neural Engine pose with a real neck/C7 joint), camera picker (FaceTime / iPhone Continuity), Calibrate with countdown, score + per-metric panel, notifications, CSV logs in ~/Library/Application Support/PostureFix/sessions/.

The headline feature: Pause any frame and drag the anchor points (ear, C7, shoulder, hip) to correct the analysis — metrics recompute live, corrections persist as offsets on the auto-tracking until cleared, and "Use this frame as baseline" calibrates from a hand-corrected frame.

The app is signed ad-hoc, so the first launch needs a right-click → Open. Source lives in swift-app/; re-run the build script to reinstall.

Optional: voice coaching

The app speaks its cues with the macOS system voice by default. For a warmer pre-rendered voice bank, generate it once with Gemini TTS:

GEMINI_API_KEY=... python3 scripts/generate_voice.py

This is the only script in the repo that touches a network service, it runs once, and the app never calls an API at runtime — it just plays the cached WAVs and falls back to the system voice for anything missing.

Quick start — Python engine

uv venv --python 3.12 .venv
uv pip install --python .venv/bin/python -e ".[dev]"
scripts/fetch_models.sh                              # ~40 MB of MediaPipe weights
.venv/bin/python -m posturefix                       # two-tone sticker scheme
.venv/bin/python -m posturefix --fast                # ~2x faster pose model
.venv/bin/python -m posturefix --marker-scheme none  # markerless

Sit side-on to the camera (sagittal view) and press B. A big on-screen countdown (default 5 s, --timer N, 0 = immediate) gives you time to get back into position if you sit beyond arm's reach; a sound marks the start of capture, then hold your best posture for 5 seconds until the chime confirms the baseline is locked in. Press Q to quit.

When the countdown expires — not when you press the key — the app captures your personal posture baseline, re-anchors the camera-motion reference, and re-learns the sticker colors under current lighting, so everything is anchored to you seated in position rather than leaning into the keyboard.

Accuracy tiers

Tier What you do What you get
1 — markerless nothing BlazePose landmarks; CVA, neck flexion + trunk metrics
2 — stabilized nothing (automatic) camera bump/roll compensation via background tracking
3 — markers 4–5 neon stickers sub-pixel spine polyline, true kyphosis angle, marker-grade C7
4 — lens-calibrated one-time checkerboard run systematic distortion bias removed

Sticker placement (tier 3)

Identity is carried by color, not position — a dropout can never silently relabel one vertebra as another (the failure mode that killed single-color schemes). Use matte 12–19 mm dot stickers:

  • Pink on C7 — the bony bump at the base of the neck
  • Green ×3 on T4 / T8 / T12 — upper, mid, and lower back
  • (--marker-scheme full) Orange on the acromion (shoulder tip) for marker-grade protraction

Kyphosis is only reported when the full chain (pink + all three green) is in view — a partial chain would measure a different spine section than your baseline did.

Lens calibration (tier 4)

.venv/bin/python scripts/calibrate_lens.py            # camera 0

Print a 9×6 chessboard, wave it around until 15 views are captured. The resulting intrinsics-cam0.json is picked up automatically and every frame is undistorted before any geometry. FaceTime HD and iPhone Continuity Camera have different distortion profiles — calibrate each camera index you use.

Metrics

  • CVA (craniovertebral angle) — C7→ear line vs horizontal. Below ~50° is the standard clinical threshold for forward head posture (absolute floor, flagged even if your baseline is bad).
  • Neck flexion — neck vector vs trunk axis. Disambiguates "neck is bent" from "whole trunk is reclined," which CVA alone conflates.
  • Head forward distance — ear-to-C7 offset ÷ trunk length.
  • Trunk lean — hip→shoulder vs vertical.
  • Spine bend — chord angle of the sticker polyline (180° = straight).
  • Shoulder forward — acromion forward of C7 (protraction), facing-aware.

Normalized metrics divide by a running median trunk length, not a single calibration snapshot, so they stay stable when hips drop out of frame; a sustained trunk-length change beyond 15% triggers a "chair moved?" prompt.

Scoring = deviation from your personal baseline + absolute clinical floors. Sustained bad posture fires a macOS notification, and a separate stillness alert fires after 20 minutes without meaningful movement — static loading is an ergonomic risk factor independent of joint angles.

Camera movement

Background features (with you masked out via segmentation) are tracked against a reference keyframe; a RANSAC similarity transform maps every landmark back into the calibration frame. All transforms compose back to the original session reference, so keyframe hops don't accumulate drift.

Reference integrity is never faked: if tracking is lost (hand over the lens, big bump), the state goes sticky-broken rather than silently resuming with an unrelated keyframe, and the app stops feeding baseline capture and alerts until geometry is trustworthy again. Recovery is automatic — an ORB wide-baseline matcher periodically re-registers the live view directly against the original reference frame and restores the exact mapping when the view returns. Parallax the similarity model can't represent is detected by checking whether RANSAC's outliers form a coherent second motion (true parallax) as opposed to scattering randomly (scrolling monitor content, exposure ramps — harmless and common on a real desk).

Design notes

  • Pose: MediaPipe PoseLandmarker (Tasks API, VIDEO mode); inference at 720p, marker detection at native resolution for sub-pixel centroids (~14–16 FPS on Apple Silicon CPU).
  • Markers: per-hue HSV segmentation → circularity-filtered blobs → saturation-weighted sub-pixel centroids (naturally down-weights specular highlights on skin). Pink/orange saturation floors are set high because those hues border skin and lip tones. At every baseline capture the bands are re-fit from actual sticker pixels (eroded blob cores only), with a fail-safe that reverts any adaptation that makes detection worse.
  • Filtering: One Euro filters + velocity-gated outlier rejection; separate filters per marker group, reset on dropouts.
  • Guards against silently-wrong readings: C7 sticker must sit within 0.8 ear–shoulder distances of the shoulder; kyphosis requires the full chain; protraction requires a resolvable facing direction (nose vs ear).

Sessions

Per-frame metrics land in sessions/session-*.csv (disable with --no-log). The Swift app writes to ~/Library/Application Support/PostureFix/sessions/ instead.

Privacy

Frames are analyzed in memory and discarded. Nothing is recorded, uploaded, or phoned home: the engine and the app contain no networking code at all, and the only files written are your own CSV logs, baseline.json, and any lens calibration — all local, all yours to delete. The optional voice-bank generator above is the single exception, and it runs once, offline of the app.

Repository layout

posturefix/      Python engine — accuracy reference implementation
swift-app/       native SwiftUI + Vision app (the daily driver)
scripts/         model fetch, app build, lens calibration, live benchmark
tests/           unit + synthetic-frame suite

Tests

.venv/bin/python -m pytest              # 60 unit/synthetic-frame tests
.venv/bin/python scripts/smoke_live.py  # 60-frame live pipeline benchmark

The two tests that need the pose model skip themselves if you haven't run scripts/fetch_models.sh; the rest run on synthetic geometry with analytically known answers.

Contributing

See CONTRIBUTING.md. The short version: a metric must never be silently wrong, so guards don't get loosened without an argument for why the reading is trustworthy without them.

License

MIT © Adnan Akil.

Pose estimation uses MediaPipe (Apache-2.0) in the Python engine and Apple Vision in the Swift app. Model weights are downloaded from Google's MediaPipe model store at setup time and are not redistributed here.

PostureFix is an ergonomics aid, not a medical device. The clinical thresholds it references are literature defaults, not a diagnosis.

About

Hyper-accurate real-time desk-posture monitoring from a Mac webcam. Native Swift app + Python/MediaPipe reference engine. 100% local.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages