Counting push-ups live with iOS: brightness, Apple pose, then an LSTM

studiobokehai1 pts0 comments

Blind at the Bottom | BDYWRK<br>Blind at the Bottom<br>August 5, 2026 · 12 min read<br>BDYWRK is an iOS app that counts squats, sit-ups, pull-ups, and push-ups using sensors in your phone. Three of those are tractable because the exercise moves the phone itself, so an accelerometer or a gyroscope gives you something to work with.

Push-ups can be counted clumsily from an accelerometer too, but vision is a more robust way to track them. The phone goes on the floor, screen up, and you do push-ups over it.

9:41<br>Push-ups Placement

Place the phone face up on the floor under your chest. Reps are tracked using the front camera.

This is an in-app tooltip a user can play before a set, to show them where to place the phone and what a rep should look like. That is all nine frames of it, which is roughly where my animation career ended.Since the front camera points straight up at you, the first implementation used the obvious signal: as you lower yourself, your body covers the lens and the image goes dark, and when you push back up the light returns, so a bright to dark to bright cycle is one rep. It needed no pose estimation, no machine learning, and no model to ship, because the whole input was the average brightness of the frame.

That is what I built first, and this is the story of why it was not enough and of the rebuilds it took to fix it.

1.0: counting the dips

The first detector was a state machine over one brightness value per frame that is smoothed out with an exponential moving average.

The smoothed signal is compared against a slowly drifting baseline of what "uncovered" looks like. Falling far enough below the baseline means you are on the way down, and climbing back near enough means you have come up, which is a rep. The two cutoffs are deliberately different numbers, because a signal hovering at a single threshold would cross back and forth on noise alone and turn one slow push-up into a dozen. That gap between them is hysteresis, the same trick a thermostat uses so it does not cycle the AC every few seconds.

Two timing constraints finish the job: a dip has to last a minimum duration, and consecutive reps have to be separated by a minimum gap. Both exist to rule out anything faster than a controlled push-up.

That is about forty lines of code, it runs in microseconds, and on a clean clip it genuinely works:

shaded = one labelled rep0.000.250.500.751.000s5s10s15s20scenter brightnessbrightness detector firedOne number a frame is the whole of what the 1.0 detector sees. This is a real set of 7 push-ups, and brightness swings from near 1.0 down to roughly 0.17 and back, once per rep. The red marks are the calls the detector actually made, 5 fires for 7 reps, one of them before the set even starts. Hover or use the arrow keys to scrub.Even on a signal that clean, the thresholds still had to be tuned, so I tuned them hard with a grid search over 20,580 parameter combinations. On the clips it was tuned against, all of them full of push-ups, the best configuration scored a perfect F1 of 1.000.

Then I pointed it at a clip where nothing happens.

Why it was (obviously) not enough

no labelled reps: nothing happens in this clip0.000.250.500.751.000s10s20s30s40scenter brightnessbrightness detector firedThis is the same signal over 47 seconds of an empty, dimly lit room. Nobody is exercising, and every red mark is a push-up the detector believed it saw. It counts 5.No amount of tuning fixes this, because it is not a tuning problem. The detector's entire model of the world is "the frame got darker, then brighter," which fits a push-up, but also a passing shadow, or the auto-exposure hunting that is actually happening above. Across a limited labeled set it invents phantom reps in still rooms and around people moving without exercising. Counting the dips was the easy half, but deciding whether an oscillation is push-ups at all is the hard one, and one brightness channel could not make that call.

2.0: give it eyes

An obvious fix is to provide a signal to disambiguate the cases brightness could not: Apple's Vision framework hands you the position and confidence of every major body part, thirty times a second, for free.

The design was narrower than "find the push-up." Since there is nothing to see at the bottom of a push-up, pose never had to see the rep itself, only confirm the top. When you push back up, your body clears the lens and a shoulder should reappear, so requiring a keypoint at the up-transition checks that a real body returned, rather than just a dip in brightness.

The logic was sound, and the detector got worse anyway even after a similar tuning cycle: both precision and recall fell, while phantom reps on still-room clips went from 1.2 to 2.6.

Understanding why is the part of this project I would keep if I had to throw away the rest.

1.2 percent: how often pose saw a body during a rep

The table below shows the fraction of frames on one labeled clip where any keypoint cleared 0.05...

push detector brightness reps signal phone

Related Articles