The Slouch Punisher
The Slouch Punisher is your Raspberry Pi 5 posture-correction device. It watches you with two USB cameras. One front-facing to catch left/right leaning, one side-facing to catch forward head posture and slouching. The Raspberry runs the frames through MediaPipe Pose to extract body landmarks, then feeds those into a RandomForest classifier that decides whether you're sitting properly or have drifted into one of the bad postures (Forward Slouch, Slump, Side-lean, etc.). When you slouch, it punishes you via hardware feedback: buzzer and LED's.
The underlying motivation for the project is as follows; bad sitting posture is a real problem, and a device that catches you in real time and nags you to straighten up is a genuinely useful thing to build while also hitting every skill area the project needs to demonstrate: computer vision, ML, full-stack software, hardware integration, and physical enclosure design.
Supplies
All the Suplpies required for the build:
- Raspberry Pi 5
- Powersupply for the raspberry pi 5
- ethernet cable
- laptop or desktop
- 2 webcams with a USB cable
- 4 x A4 sheets of 3 mm acriylic glass
- Prototyping board to solder components on
- jumper cables (male to male, female to female and male to female)
- 3 x Freenove 8 RGB modules
- 4 active buzzers
- 1 NPN transistor
- 5 inch LCD DSI display
- 2 round 22 mm pushbuttons
- 2 male to female USB-A cables (0.5 metres)
- 1 male to female ethernet cable (cat 6a) (0.5 meters)
- 1 male to female USB-C cable (0.5 meters)
- superglue
- zipties
- set of clear transparent hinges (35 x 25mm)
- resistor 2100 ohm +/- 5%
- screwdriver (crosshead)
Concept
Before building anything, three things need to be locked down , everything that comes afterwards depends on the answers:
- How do we detect a slouch? Decide what the system actually watches to tell good posture from bad. Here we use cameras to track your body's key points (landmarks) rather than relying on pressure alone — and crucially, two angles, since one front view can't see you leaning forward.
- What postures should it recognise? These are the "classes" the model sorts each frame into: Perfect, Forward Slouch, Slump, Side-lean Left, Side-lean Right, and a "Not There" state for when you've stepped away.
- How does it tell you to fix it? The feedback that nudges you upright; a buzzer, LED strip, on-screen prompt, or some combination.
With those three defined, we can move on to choosing components, since each one is dictated by the decisions above.
Components
The brains
- Raspberry Pi 5 — runs everything: camera capture, pose detection, the classifier, and the web interface.
- Power supply for the Pi 5 — ideally the official 27 W USB-C one; the Pi 5 is picky about underpowered supplies.
- Ethernet cable — for a stable wired connection during setup and use.
The eyes
- 2 × USB webcams (with cables) — one front-facing, one side-facing (the side view is what catches forward slouching).
Feedback & electronics
- 3 × Freenove 8-LED RGB modules — the WS2812B lights that show your posture status at a glance.
- 4 × active buzzers — the audible "sit up straight" nag.
- 1 × NPN transistor + 1 × 2.1 kΩ resistor (±5%) — the transistor switches the buzzers from a single GPIO pin (four buzzers pull more current than a pin can safely give), with the resistor limiting base current. A standard 2.2 kΩ works fine if 2.1 kΩ is hard to find.
- 5-inch DSI LCD display + 1 x 12 cm DSI cable 15 to 22 pin — the on-device screen / UI.
- 2 × 22 mm round pushbuttons — physical controls, e.g. start/stop a session.
Wiring
- Prototyping board — to solder the buzzer/transistor circuit and keep connections tidy.
- Jumper cables — male-to-male, female-to-female, and male-to-female.
Panel connectors (enclosure feedthroughs)
- 2 × USB-A male-to-female cables (0.5 m)
- 1 × Cat 6a ethernet male-to-female cable (0.5 m)
- 1 × USB-C male-to-female cable (0.5 m)
- These short extensions are what let you plug into the outside of the sealed case while the Pi stays mounted inside.
Enclosure
- 4 × A4 sheets of 3 mm acrylic — laser-cut into the panels of the case.
- Set of clear hinges (35 × 25 mm) — for the service door.
- Superglue — for bonding the acrylic panels.
- Zip ties — cable management inside.
Tools
- Laptop or desktop — for flashing the Pi, writing the code, and reaching the interface during development.
- Crosshead screwdriver.
Designing the Enclosure
Every part of the enclosure was designed in Autodesk Inventor before anything was cut or printed. The body is a simple cube — six flat panels joined with finger joints so they self-align and glue into a rigid box — plus two extra parts that exist purely for modularity, so the electronics aren't permanently trapped inside.
The eight parts:
- Front plate — holds the 5-inch LCD display and the two 22 mm buttons.
- Left & right plates — the side walls.
- Back plate — has a cutout for the keystone.
- Top plate — hinges open as a lid for access.
- Base plate — the floor everything sits on.
- Keystone (3D-printed) — slots into the back-plate cutout and holds all the external cable connectors in one place.
- Pi base plate (3D-printed) — a removable mount for the Raspberry Pi.
Why the two extra parts? This is the design decision worth understanding. Rather than fixing connectors and the Pi straight to the acrylic, each gets its own dedicated part:
- The keystone consolidates all external I/O — power, ethernet, USB — into a single insert. If a port or cable ever needs changing, you swap the keystone, not the whole back panel.
- The Pi base plate is glued to the floor of the enclosure; standoffs are glued onto the plate, and the Pi screws down onto those (M2.5, matching the Pi's mounting holes). The whole Pi then lifts out as one unit for servicing, and no screw is ever driven into the acrylic itself — which would risk cracking it.
Designing these as separate, reusable parts keeps the build serviceable and lets the design be modified or reused later without starting from scratch.
Finger joints were used on all six panels instead of plain butt joints — the interlocking tabs give far more gluing surface and hold the panels square while the glue cures.
Files for this step (in the attached ZIP):
- Laser-cut panels: DXF cutting files and a PDF layout for the six acrylic panels, plus dimensioned drawings and editable STEP files.
- 3D-printed parts: STL/3MF files for the keystone and Pi base plate with recommended print settings, plus STEP files if you want to modify them.
Cut & 3D-print the Parts
With the design finished, the files from Step 3 get turned into physical parts — two different processes for two different materials.
Laser-cutting the panels
The six acrylic panels are laser-cut from 3 mm sheet using the DXF/PDF files. You can do this on a makerspace or school laser cutter, or send the files to an online cutting service. A few notes:
- Use cast acrylic if you can — it laser-cuts with a cleaner, slightly frosted edge and is far less prone to melting than extruded acrylic.
- LEAVE THE PROTECTIVE FILM ON THE SHEETS WHILE CUTTING; it shields the surface from scorch marks and residue. Peel it off once the parts are cut.
- Cut power and speed are machine-specific, so if you're cutting it yourself, dial those in for 3 mm acrylic on your particular machine. A service handles this for you.
3D-printing the keystone and Pi base plate
The two printed parts come straight from the STL/3MF files. Both are small and print quickly.
At the end of this step you've got a flat-pack kit: six panels, two printed parts, and the clear hinges — all ready to bring together next.
Assemble & Finish the Enclosure
Now the flat-pack comes together into the finished shell. One rule drives the whole order: all gluing happens before the matte spray, because glue won't bond to a frosted surface. So build the box raw, then frost it last.
- Glue the box. Dry-fit the four walls (front, back, left, right) onto the base plate first to check the finger joints seat cleanly, then glue them up. The interlocking tabs self-align the panels — just keep the box square while the glue cures. (Superglue works on acrylic and is what's used here; acrylic solvent cement gives a stronger weld if you have it.)
- Hinge the top. Fit the clear hinges between the back wall and the top plate so it opens as a lid — that's your access into the sealed box later.
- Glue in the keystone. Seat the 3D-printed keystone into the back-plate cutout and glue it in place. This is what all the external cables pass through.
- Glue in the Pi base plate. Glue the printed base plate to the floor of the enclosure, then glue the standoffs on top of it. The Pi itself screws onto these standoffs in the next step, so leave it off for now.
The matte finish. With everything glued and the box still empty, give the whole enclosure a matte/frosting spray. This is the step that earns its keep visually: left clear, the acrylic would show the internal LEDs as harsh bright dots and put all your wiring on display. Frosting the surface diffuses the light so the LEDs glow softly through the panels, and it hides the electronics for a clean, finished look. Use thin, even coats and let it dry fully before handling. (Mask the hinges first if you'd rather keep them clear.)
That leaves you with a finished, frosted shell with its mounts glued in — ready to populate with electronics.
Mount the Components
With the shell finished, everything gets mounted into it. This step is physical placement only — nothing is wired up yet (that's Step 7).
- Mount the Pi. Screw the Raspberry Pi onto the standoffs on the base plate (M2.5). It now sits raised off the floor and still lifts out as a unit thanks to the removable plate.
- Glue in the screen. The 5-inch LCD sits behind the front-plate cutout, glued around its edge so the display shows through cleanly.
- Mount the buttons. The two 22 mm pushbuttons drop into their holes in the front plate and lock in with their threaded collar and nut.
- Mount the LED modules. Fix the three Freenove modules inside against the panels so their light diffuses out through the frosted acrylic — position them wherever you want the glow to show.
- Fit the panel connectors. Seat the female ends of the short USB-A, USB-C, and ethernet extension cables into the keystone with the ports facing outward; the cables route back inside to the Pi.
The box is now fully populated — every component in place, ready to be wired together.
Wiring
This is where the mounted components get connected. Three circuits hang off the Pi's GPIO header — the buzzers, the LED ring chain, and one button — plus a separate power button and the screen's ribbon. The buzzer driver gets soldered onto the proto board first.
1. Build the buzzer driver on the proto board
The four buzzers don't connect to the Pi directly — a single GPIO pin can't safely supply enough current for four of them (a pin tops out around 16 mA; four active buzzers pull several times that). So they're switched through the 2N3904 NPN transistor (the "904" in the diagram). Solder onto the proto board: the four buzzers wired in parallel, the 2N3904, and the 2.1 kΩ resistor on the transistor's base.
It works as a low-side switch — a GPIO pin drives the base through the resistor, the emitter goes to ground, and the buzzers sit between 5 V and the collector. When the GPIO goes high, the transistor conducts and all four buzzers sound at once.
2. Connect the buzzer driver to the Pi
Three wires run from the proto board to the header:
- GPIO 14 (pin 8) → through the 2.1 kΩ resistor to the transistor base. GPIO 14 is also the UART TX pin, so disable the serial console (raspi-config → Interface Options → Serial) or it will fight this pin.
- 5 V → the buzzers' positive rail.
- GND → the transistor's emitter.
3. Wire the LED ring chain
The three Freenove WS2812B rings are daisy-chained: 5 V and ground are shared across all three, and a single data line carries the signal from the Pi into the first ring's DIN, then DOUT→DIN along the chain. That one data pin drives all 24 LEDs.
- GPIO 10 (pin 19, SPI MOSI) → the first ring's data-in. Driving WS2812B over SPI is one of the cleaner methods on a Pi — it doesn't need root and avoids the PWM/audio conflicts of GPIO 18.
- 5 V and GND → shared power and ground for all three rings.
WS2812B expect a roughly 3.5 V data signal and the Pi's GPIO is 3.3 V, just below spec. It usually works fine over short runs; if the LEDs ever flicker, a 3.3→5 V level shifter on the data line clears it up.
4. Wire the buttons
The two buttons do different jobs:
- The power button is wired straight to the Pi's power-on connection. Pressing it boots or shuts down the whole unit — no code reads it.
- The second button is a normal GPIO input on GPIO 2 (pin 3), with its other leg to ground. GPIO 2 has a permanent on-board pull-up (it's an I²C pin), which holds the input high and lets the press pull it low — so no external resistor is needed.
5. Connect the screen
The 5-inch display connects over its ribbon cable into the Pi's DSI port. It isn't part of the GPIO wiring, so it doesn't appear in the diagram.
Flashing Raspberry Pi
Before any code, the Raspberry Pi needs an operating system. This is done with Raspberry Pi Imager (free, from raspberrypi.com), which writes the OS to the SD card and — importantly — can pre-configure the Pi so it boots ready to use without ever plugging in a monitor.
- Install and open Raspberry Pi Imager on your laptop, with the SD card in a reader. https://downloads.raspberrypi.com/imager/imager_latest.exe
- Choose the OS and storage. Pick your Pi model (Raspberry Pi 5), Raspberry Pi OS (64-bit) as the operating system, and the SD card as the target. The 64-bit version matters — the ML libraries used later (MediaPipe, scikit-learn) require it.
- Set the OS customisation before writing. Open the settings (gear icon) and:
- set a hostname (e.g. slouchpunisher) so the Pi is easy to find on the network,
- enable SSH so you can connect to it remotely,
- enter your Wi-Fi name, password and country so it connects to the internet on first boot,
- set your locale and timezone.
- This headless setup is what lets you skip the keyboard-and-monitor stage entirely.
- Write the card, then pop it into the Pi and power on. On first boot it joins your Wi-Fi and is reachable on the network — no screen needed.
At this point the Pi is running and online, ready to connect to from your laptop in the next step.
Connecting Over SSH
With the Pi on the network, all the development happens on it remotely from your laptop — editing files and running commands on the Pi through VS Code, with no monitor or keyboard plugged into the Pi itself.
- Connect the Pi over ethernet. A wired link gives you a stable, fast connection for the SSH session and for pulling down the Docker images and ML libraries, which are large.
- Set up Remote-SSH in VS Code. Install the Remote - SSH extension, then connect to username@hostname — the hostname you set when flashing (e.g. slouchpunisher.local). VS Code opens a window that's actually running on the Pi: the terminal and file explorer are now the Pi's, not your laptop's.
- Clone the repository. In the Pi's terminal:
- git clone https://github.com/howest-mct/2025-26-projectone-ctai-lloydkendall1.git
This pulls down the full project — the FastAPI backend, the Gradio frontend, the services layer, and the ML code, all in the layered structure.
4. Install the dependencies.
- Docker + Compose — the backend runs Postgres (and the API) in containers. The official script handles it:
- curl -fsSL https://get.docker.com | sh
- sudo usermod -aG docker $USER
Log out and back in so the group change takes effect.
- Python packages — MediaPipe, scikit-learn, OpenCV, FastAPI, Gradio and the rest, from the repo's requirements file. Install them in a virtual environment (Raspberry Pi OS won't let pip install system-wide otherwise):
- python3 -m venv .venv && source .venv/bin/activate
- pip install -r requirements.txt
- One reproducibility warning worth putting in the step: install the exact pinned versions from the requirements file. The scikit-learn version on the Pi in particular must match the one the model was trained with, or the saved model simply won't load.
The Pi is now connected, the code is on it, and the dependencies are in — ready to collect data and train the model.
Collecting Data & Training Model
What you're actually training. MediaPipe Pose Landmarker (pretrained, off the shelf) finds 33 body landmarks in any frame. You don't train it and you don't annotate keypoints by hand. What you train is a small scikit-learn RandomForest classifier that looks at those landmarks and decides which posture they represent. That split is what makes the project tractable — the hard computer vision is already solved for you.
Two models, one per camera. Because the two cameras see different things, each gets its own classifier:
- Front camera → centred, left_lean, right_lean
- Side camera → good, forward_head, slouching
A front-on view can't see you leaning forward, which is the whole reason the side camera and its separate model exist.
1. Collect the data. For each class, sit in that posture and capture several hundred frames from the relevant camera. Each frame is run through MediaPipe, which outputs the 33 landmarks — so you're not labelling points, just tagging each captured sample with its posture class.
2. Turn landmarks into features. From the landmarks, build your feature set: the landmark coordinates plus a handful of joint angles (neck, shoulder, spine, and so on). Save these as labelled rows — one row per frame, columns are the features, last column is the class.
3. Train the classifier. Fit a RandomForestClassifier on the labelled features (with a scaler), holding back a test split to check accuracy — the front model lands around 99% on three clean, distinct classes. Save the trained model together with its scaler and the exact feature order as a single bundle (a .pkl).
4. The one thing that will silently break it. The features at inference time must be computed exactly the same way as at training time — same landmarks, same angle definitions, same order. If the live script computes even one angle differently from the training script, the model returns confident nonsense with no error at all. Keep the feature and angle definitions in one shared file that both training and inference import.
With both models trained and saved, they're ready to be loaded on the Pi for live inference — the integration step.
Integrate & Run
With both models trained and the Pi set up, this step brings everything together into a running system.
1. Start the database.
The project uses PostgreSQL running in Docker. From the project root:
- docker compose up -d
This starts Postgres and creates the schema — all seven tables (users, sessions, posture readings, alerts, and so on) are initialized from init.sql automatically on first run. One important note: if you ever need to reinitialize the schema from scratch, run:
- docker compose down -v
The -v flag drops the volume too. Without it, Docker reuses the old volume and your init.sql changes never apply — a silent, confusing failure.
2. Place the trained models.
Copy the two .pkl bundles (front camera model + side camera model) into the location the backend expects. Each bundle contains three things that must stay together: the trained RandomForest, its scaler, and the feature order the model was trained on. If the scaler or feature order is missing the service won't load.
3. Start the backend.
The FastAPI backend runs the camera capture, MediaPipe inference, classification, and database logging — all in the services layer, completely separate from the frontend:
- source .venv/bin/activate
- uvicorn main:app --host 0.0.0.0 --port 8000
--host 0.0.0.0 makes it reachable from your laptop's browser, not just from localhost on the Pi.
4. Start the Gradio frontend.
In a second terminal:
- python gradio_app.py
Gradio opens on port 7860. It never touches the cameras directly — it polls the backend API for JPEG frames and posture data, which is why the cameras and all inference live entirely in the backend.
5. Open the interface.
Navigate to http://slouchpunisher.local:7860 in your browser. You should see:
- Live camera feeds from both cameras.
- The current posture classification updating in real time.
- Start/stop session controls that gate the database logging.
- The LED strip, buzzer, and any other feedback hardware responding to posture events.
6. Verify the hardware feedback.
Sit in each posture class deliberately and check that the correct response fires — LEDs change colour, buzzers sound on a bad posture, and the readings appear in the database (Adminer runs alongside Postgres and lets you check the tables directly at http://slouchpunisher.local:8080).
How the posture fusion works. The two camera models run independently, but the backend fuses their outputs before deciding whether to trigger an alert. A single bad reading doesn't fire the buzzer — the bad posture has to be sustained for a set number of consecutive frames. This prevents a momentary shift from nagging you constantly.
If something isn't working — a short checklist:
- Model loads silently but gives wrong results → feature mismatch between training and inference. Check that the feature definitions are imported from the shared config, not defined separately in each script.
- init.sql changes not applying → docker compose down -v then up -d again.
- LEDs not responding → confirm SPI is enabled (raspi-config → Interface Options → SPI).
- Buzzer silent → confirm the serial console is disabled (raspi-config → Interface Options → Serial → disable console), otherwise UART TX fights GPIO 14.
- Camera feed not appearing → check both USB cameras are detected (ls /dev/video* should show at least two devices).
Using the Slouch Punisher
Starting a session — power on, open the browser to slouchpunisher.local:7860, press the physical button or hit start in the UI. What the LEDs show when it's active.
What the feedback means — what each LED colour corresponds to (good posture vs each bad class), when the buzzer fires, and how long you have to be slouching before it nags you (the sustained-posture threshold).
Stopping a session — how to end it cleanly so the session gets written to the database properly.
Checking your history — if you want to expose that, even just mentioning Adminer at port 8080 lets a technically minded reader look at their own posture data over time.
Future Improvements
The current build works well for a single user in a controlled environment, but a few things would make it significantly more robust in the real world.
Better training data diversity
The models were trained on one person, in one outfit, under consistent lighting. In practice that means the classifier can struggle when:
- A different person uses the device — different body proportions shift where the landmarks land.
- The user wears baggy or layered clothing — MediaPipe's landmark confidence drops when joints are obscured.
- The lighting changes — shadows across the torso or strong backlight can throw off landmark detection.
The fix is straightforward: retrain with data collected across multiple people, a range of outfits (fitted, loose, layered), and varied lighting conditions (overhead, side-lit, natural, dim). More variety in training data directly translates to a more reliable classifier in the wild.
Multi-user support via RFID
Right now the device has no concept of who is sitting in front of it — every session goes into the database against the same user. Adding an RFID reader would let multiple people use the same device properly:
- Each user taps their key fob to identify themselves before starting a session.
- Sessions, posture history, and alert counts are logged against that user's profile.
- Over time each user builds up their own posture data and can track improvement independently.
Combined with the broader training data, this would turn the Slouch Punisher from a personal prototype into something genuinely usable by a household or a small office.