Atiya12/Pose-correction-application
PostureCoach — Online Meeting Pose Monitor
A browser-only computer-vision app that watches your webcam during online meetings and reports what your current framing/pose state is — CENTERED IN MEETING, FACE TOO CLOSE, LOOKING AWAY, and so on. No backend, no uploads, no database.
PostureCoach/
├── index.html markup + MediaPipe imports
├── style.css visual system
├── script.js vision pipeline, state machine, dashboard
└── README.md this file1. Running it locally
MediaPipe and getUserMedia() both require a secure context, so opening `index.html` by double-clicking will not work (file:// is not secure). Serve the folder:
cd PostureCoach
python3 -m http.server 8000
# then open http://localhost:8000localhost counts as secure, so the camera prompt will appear. Any static server works (npx serve, VS Code Live Server, etc.).
Click Start camera → allow the permission → click Start meeting session. Sit normally for two seconds while the baseline is measured.
2. Major components
3. Landmark detection
MediaPipe Pose returns 33 body landmarks per frame as {x, y, z, visibility}, where x/y are normalized to the image (0 = left/top, 1 = right/bottom) and visibility is the model's confidence that the point is actually observable. The app uses:
MediaPipe Face Detection (short-range model, tuned for arm's-length webcam use) returns a bounding box per face as {xCenter, yCenter, width, height} plus a score. Pose is single-person; Face Detection genuinely returns multiple boxes, which is the only reason MULTIPLE PEOPLE DETECTED is honest — it is reported from the face pipeline alone.
Left and right always mean your left and right, matching MediaPipe's naming. The preview is mirrored for comfort, so your left appears on the right of the screen.
4. Angle calculations
All 2D vector geometry on normalized coordinates.
`angle(A, B, C)` — the angle formed at B:
v1 = A − B
v2 = C − B
θ = acos( (v1 · v2) / (|v1| · |v2|) ) × 180/πUsed for the shoulder joint angles: leftShoulderAngle = angle(rightShoulder, leftShoulder, leftElbow).
Vertical deviation — how far a segment departs from upright, 0° = perfectly vertical:
θ = |atan2(Bx − Ax, −(By − Ay))| × 180/π(The y term is negated because image y grows downward.) Used for:
- Neck angle = deviation of shoulder midpoint → nose
- Torso angle = deviation of hip midpoint → shoulder midpoint
Horizontal tilt — signed departure from level:
θ = atan2(By − Ay, Bx − Ax) × 180/πUsed for shoulder tilt (right shoulder → left shoulder) and head roll (right eye → left eye). Positive means the left side sits lower in the image.
Head-height ratio — nose height above the shoulder line, divided by shoulder width:
headGapRatio = (shoulderMidY − noseY) / shoulderWidthDividing by shoulder width makes it scale-invariant: it stays roughly constant whether you sit near or far, so HEAD TOO LOW / HEAD TOO HIGH don't fire just because you moved.
Head-turn ratio (yaw estimate) — where the nose falls between the two ears:
dL = |noseX − leftEarX|
dR = |noseX − rightEarX|
turn = (dR − dL) / (dR + dL) // +1 ≈ fully turned to your leftIf only one ear is visible at all, that alone implies a large rotation and the ratio is clamped to ±0.7.
5. State classification
23 states, listed in STATES. Each carries a tone that drives the colour:
The words "good posture" and "bad posture" appear nowhere in the UI, the code, or the comments.
Priority cascade
classify() returns on the first match, so a detection failure is never dressed up as a posture observation:
ANALYZING (first 1.4 s)
↓
CAMERA VIEW BLOCKED
↓
FACE NOT FOUND / LOW VISIBILITY
↓
MULTIPLE PEOPLE DETECTED
↓
LOW VISIBILITY (landmark confidence)
↓
FACE TOO CLOSE / FACE TOO FAR
↓
BODY OUT OF FRAME / CAMERA POSITIONING NEEDED
↓
NOT IN MEETING POSITION
↓
LOOKING AWAY → HEAD TURNED L/R
↓
LEANING FORWARD / BACK / LEFT / RIGHT
↓
HEAD TOO LOW / HIGH → HEAD TILTED L/R
↓
SHOULDERS MISALIGNED
↓
CENTERED IN MEETINGFACE TOO FAR sits below the face check and above everything positional: if a face is detected but tiny, it is FACE TOO FAR, never FACE NOT FOUND.
Face Too Close detection
Three independent signals, any of which triggers it:
- Box width —
faceWidth > 0.42of the frame width. - Box area —
faceWidth × faceHeight > 0.17of the frame. Catches faces that are wide but cropped short, e.g. only the eye-and-nose region filling the view. - Edge overflow — the box extends past the left or right edge (
cx ± w/2outside[-0.03, 1.03]) while already being large. A face spilling out of frame at normal size is a framing problem, not a distance problem, so the size condition prevents a misfire.
Calibration baseline
Leaning forward/back is a relative judgement — it depends on where your chair normally is. During the first 45 qualifying frames of a session the app records the median face width, shoulder width, shoulder midpoint and head-gap ratio. After that:
faceWidth / baseline > 1.16(or the same for shoulder width) →LEANING FORWARD< 0.86→LEANING BACK- shoulder midpoint drifting
> 0.085from baseline →LEANING LEFT/LEANING RIGHT
Before the baseline exists, forward/back is skipped entirely and side lean falls back to drift from frame centre. Recalibrate clears it; moving your chair or camera is a good reason to press it.
Camera-blocked detection
Every 400 ms the frame is drawn into a 32×24 offscreen canvas and the luminance mean and standard deviation are computed (Y = 0.2126R + 0.7152G + 0.0722B). A feed that is both dark (mean < 42) and flat (σ < 9) is a covered lens rather than a dim room, since a dim room still has structure.
6. Smoothing and debouncing
Three separate mechanisms stop the readout flickering:
- Moving average — metrics are averaged over the last 6 frames before classification. Boolean metrics use a majority vote over the same window.
- Confirmation window —
commit()holds each candidate state and only displays it once it has persisted for 550 ms (320 ms for critical states likeFACE NOT FOUND, so genuine failures still surface fast). A single noisy frame cannot flip the display. - Alert cooldown — the same state cannot post a second alert within 3 s, so a borderline reading can't spam the log.
Plus the `ANALYZING…` window: for the first 1.4 s after starting the camera or a session, no pose state is assigned at all. This is what stops a nonsense classification from the first few frames.
7. Deploying to a static Hugging Face Space
The app needs no Python, Flask, FastAPI, Node.js or database — just three static files.
- Go to huggingface.co → sign in → New → Space.
- Give it a name, e.g.
posturecoach. - Under Select the Space SDK, choose Static. (Not Gradio, not Streamlit, not Docker.)
- Choose Public or Private, then Create Space.
- Open the Files tab → Add file → Upload files.
- Upload
index.html,style.cssandscript.jsinto the repository root — not a subfolder.index.htmlmust be at the top level or the Space will show a blank page. - Add a commit message and Commit changes to main. The Space rebuilds in a few seconds.
- Open the Space. Hugging Face serves it over HTTPS, so
getUserMedia()works. - Click Start camera and accept the browser's camera prompt.
If the camera prompt never appears inside the embedded preview, open the Space in its own tab using the ⋮ → Embed this Space direct URL (https://<user>-<space>.hf.space). Embedded iframes sometimes withhold camera permission; the direct URL does not.
The README is required
Every Space needs a README.md in the repository root whose very first line is `---`, opening a YAML block that declares the SDK. Without it the Space fails to build with "Configuration error — missing configuration in README". This file already carries it:
---
title: PostureCoach
emoji: 🎥
colorFrom: green
colorTo: blue
sdk: static
app_file: index.html
pinned: false
license: mit
---Upload this README.md alongside the three code files. If you created the Space through the web form, Hugging Face already generated a README.md — overwrite it, or paste the block above at the top of the existing one.
Rules that trip people up:
- The
---must be on line 1. A blank line, a heading, or a UTF-8 BOM above it breaks parsing. - Use plain spaces for indentation, never tabs.
colorFrom/colorToaccept only:red,yellow,green,blue,indigo,purple,pink,gray.sdk: staticmust match the SDK chosen when the Space was created. If you picked Gradio or Docker by mistake, change it in Settings → Space SDK as well — a mismatch keeps the error alive.- Commit the README to the
mainbranch; the Space rebuilds automatically within a few seconds.
8. Testing guide
Start the camera, start a session, and let the baseline settle before running these. Each test lists how to reproduce it, the expected state name, message, and dashboard behaviour.
Debounce check: wave a hand briefly across your face. The state should not flip — the movement is shorter than the 550 ms confirmation window.
Privacy check: open DevTools → Network, run a full session. After the initial MediaPipe model files load from jsDelivr, no further requests should appear. Nothing is sent anywhere.
9. Troubleshooting
10. Scope and honesty
This is a camera-framing demo, not a medical, psychological or behavioural assessment. It does not measure eye gaze, attention, engagement, mental state, health or professionalism.
LOOKING AWAY is an estimate of head orientation derived from where the nose falls between the two ears — no eye-gaze model is implemented, and none is claimed. Meeting Position Percentage is defined as:
Meeting Position % = (Centered in Meeting frames / Total valid frames) × 100It describes how much of the session you spent well framed for the camera. It is not a posture score.
All processing happens locally in your browser. Webcam video, face images, pose landmarks and derived numbers are never uploaded, and nothing is written to storage.
