Article · · 7 min read

Counting figure-skating jump rotations from one phone video

Why pose models fail on fast spins, and how the projected width of the shoulders and hips gives a rotation count that matched the human count in 93.3% of motion-capture clips.

A coach films a jump on a phone and wants one number: how many rotations. It sounds like the easiest part of the problem. It turned out to be the hardest, and the fix came from looking at the data instead of the model.

What the video actually contains

A jump lasts about half a second. At 30 frames per second that is roughly 15 frames in the air, and at five rotations per second one turn takes about six frames. Phones record at a variable frame rate, compress hard, and the clip has often been re-sent through Telegram. Every number lives at the edge of what the pixels hold.

Why the obvious approach fails

The obvious approach is to run a pose model, take the line between the shoulders, and count how many times it turns. I measured what the pose model really returns during a fast spin: the true shoulder rotation was about 69° per frame, while the pose said about 2° per frame. The model hallucinates a front-facing body because that is what it saw most in training. Unwrapping the azimuth fails, and so does a model trained to predict orientation, because both start from angles that are wrong.

The signal that survives

Angles are wrong, but distances are not. The projected width of the body, the pixel distance between the shoulders and between the hips, honestly pulses as the skater turns: wide when the body faces the camera, narrow when it is side-on. In one clip it went 32 → 8 → 32 pixels. The width pulses at twice the rotation frequency, because a body looks narrow twice per turn.

  • Fit a harmonic model of the width over a grid of possible rotation speeds.
  • Pick the speed that explains the pulsing best.
  • Run a permutation test: shuffle the frames and check the fit is far better than chance. If it is not, there is no rotation signal, and the system says so.
# simplified: score each candidate speed by how well a harmonic explains the width
for speed in speeds:                 # revolutions per second
    phase = 2 * pi * 2 * speed * t   # width pulses twice per turn
    fit = least_squares(width, [cos(phase), sin(phase), 1])
    score[speed] = explained_variance(fit)
best = argmax(score)
p_value = permutation_test(width, best)  # shuffled frames as the null

How I know it works

I validated it against 3D motion capture from the FS-Jump3D research dataset, where the true rotation comes from unwrapping the hip line in 3D. On the full set of 216 clips at 30 fps the method measured 189, with a median error of 0.04 revolutions; 180 of the 189 were within a quarter turn, and the rounded count matched the human count in 93.3% of cases. Where there was no signal, it returned nothing instead of a guess.

Saying "I don't know" on purpose

Every value in the report carries a 68% interval, a tier and a status. When the system cannot measure something, for example when seven turns per second at 30 fps make the count ambiguous, it returns "absent" with a machine-readable reason. On a corpus of fresh real-world clips it never printed a number where there was no signal. For a coach, a refusal is useful; a confident wrong number is not.

What I would tell anyone building on pose models

  • Measure what the model returns on your hardest case before building on top of it.
  • Look for the quantity the model gets right, even if it is not the one you wanted.
  • Validate against ground truth, not against your own expectations, and make refusal a first-class output.

The project behind this article: jumpsight →

Have a similar problem?

Tell me what you are building and what data you have. I reply with how I would approach it.

Discuss a project →