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).
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 PostureFixSwiftUI + 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.
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.pyThis 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.
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 # markerlessSit 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.
| 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 |
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.
.venv/bin/python scripts/calibrate_lens.py # camera 0Print 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.
- 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.
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).
- 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).
Per-frame metrics land in sessions/session-*.csv (disable with --no-log).
The Swift app writes to
~/Library/Application Support/PostureFix/sessions/ instead.
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.
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
.venv/bin/python -m pytest # 60 unit/synthetic-frame tests
.venv/bin/python scripts/smoke_live.py # 60-frame live pipeline benchmarkThe 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.
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.
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.