Camera angle buckets
PoseFlow classifies the camera’s position relative to the user into one of 16 canonical buckets every frame. This drives two things:
- Which slice of
angleBandsthe form service uses for view-dependent measurements (spine lean reads differently from front vs side). - Trainer UX, the Studio’s “where you’re standing” chip + grid highlight, the consumer app’s “rotate your phone” hints.
The 16 buckets
A semi-sphere orbit around the user:
| 90° Left | 45° Left | Front | 45° Right | 90° Right | |
|---|---|---|---|---|---|
| Head (camera at face level) | 90left_overhead | 45left_overhead | front_overhead | 45right_overhead | 90right_overhead |
| Hip (camera at hip level, default) | 90left_hip | 45left_hip | front_hip | 45right_hip | 90right_hip |
| Feet (camera on the ground) | 90left_floor | 45left_floor | front_floor | 45right_floor | 90right_floor |
Plus a 16th specialised bucket: behind_hip for movements that
authentically need a back view (rare).
The bucket IDs use
overheadandfloorfor backward-compatibility with.posefiles in the wild; the user-facing labels are Head and Feet. SeeCameraBucket.displayLabel.
How the detector works
CameraAngleDetector.detectBucket(pose)
runs every frame and aggregates evidence across 11 paired
anatomical landmarks:
| Pair | Weight | Why |
|---|---|---|
| Shoulders | 1.0 | Trunk anchor, always paired, spans rotation axis |
| Hips | 1.0 | Trunk anchor |
| Ears | 0.8 | Sharp signal at ±90° (far ear occludes) |
| Eyes | 0.6 | Backup face evidence |
| Eye inner / outer | 0.4 each | Catch subtle head turn |
| Knees | 0.6 | Survives upper-body occlusion |
| Ankles | 0.5 | Lower-body fallback |
| Heels | 0.4 | Last-resort lower body |
| Elbows | 0.4 | Static arm rotations |
| Wrists | 0.3 | Lowest, swing during movement |
Each pair contributes either:
- 3D world-z delta (preferred when
Pose.hasWorldLandmarks). The subject’s near-camera shoulder has smaller world z; the sign ofleft.z − right.zis unambiguous regardless of mirror state. - 2D visibility asymmetry (always available). When
vis(left) > vis(right), the subject’s left side faces the camera → camera is on their left → negative azimuth.
The final sign is the sign of the weighted sum of all contributing pairs. Confidence drops proportionally to how unanimous the vote was; if pairs cancel, the detector falls back to neutral and the temporal smoother holds the previously-committed direction.
Why image-space x-deltas aren’t used
The legacy detector relied on right_shoulder.x − left_shoulder.x as
a sign signal. We dropped this because:
- Mirror-dependent, the live PoseFlow Studio camera is a selfie / mirror surface; the pose engine emits anatomy-labeled landmarks; the x convention is opposite to the raw camera output.
- The shoulders don’t swap on rotation, they converge toward image-center. The image-space sign is dominated by perspective noise, not direction.
The new multi-landmark aggregator uses signals that are convention- invariant: world-z is in camera-relative depth (mirror doesn’t affect it), visibility is anatomy-labeled.
Occlusion resilience
The detector keeps working when:
- Legs are out of frame (zoom on upper body): 8 upper-body pairs still vote.
- Face is occluded by a hat or headphones: trunk + arm + lower-body pairs still vote.
- One ear is hidden by hair: the other 10 pairs outvote it.
- Extreme zoom on torso (face + limbs all out of frame): shoulders
- hips alone are enough at full weight.
The CameraAngleDetector occlusion tests inside the SDK pin this
behaviour.
Temporal smoothing
StableCameraAngleDetector wraps the stateless detector with a
10-frame rolling window + hysteresis. The runtime tracker uses the
smoother (not the raw detector) so the chosen bucket is stable
across frame-to-frame jitter. UI surfaces (Studio chip, picker grid
highlight) read from the same smoother instance per session.
The smoother only switches the committed bucket when a different bucket has been the raw pick for at least 3 consecutive frames AND those dissents agreed with each other, flickering between two candidates doesn’t flip the output.
angleBands, per-bucket measurement ranges
When a movement’s authored measurements behave differently from different angles (spine lean, knee valgus, shoulder abduction), the trainer authors multiple bands per channel, one per bucket:
angleBands: front_hip: - { phase: down, channel: spine_lean, min: -8, max: 8 } 90left_hip: - { phase: down, channel: spine_lean, min: -5, max: 15 } 90right_hip: - { phase: down, channel: spine_lean, min: -5, max: 15 }At runtime, MovementTracker._resolveBucketOverrides:
- Asks the smoother for the current bucket.
- Finds the bucket’s authored bands for the current phase.
- Overrides the form rule’s
FormCondition.valuewith that bucket’s range.
If the current bucket has no band for a channel, the runtime
interpolates between the two nearest authored buckets (by azimuth
distance on the ±180° ring). Falls back to the primary
front_hip band when confidence is too low.
View-invariant channels (joint flexion angles, distances, ratios)
ignore angleBands entirely, those numbers are intrinsic and
don’t change with camera position.
Authoring multiple buckets
In the Studio:
- Stand in your default position (e.g. front, hip-level).
- Drop measurements + conditions as usual.
- Open the Camera angles panel. Tap a different bucket cell to make it the authoring target.
- The conditions panel re-renders showing the bands authored for the new bucket. Re-drop or re-tune as needed.
- Save. The output
.posefile carries oneangleBandentry per authored channel × bucket combination.
The Studio’s detected-bucket chip shows where you ARE physically; the grid highlight shows where you’re AUTHORING. They’re independent on purpose, you can stand front-on while authoring side-on bands by typing the numbers in directly.
Movement.cameraAngle
Movement.cameraAngle (front / side / any) is a
high-level authoring hint that gets baked into the .pose file.
It does NOT pick the camera lens, every consumer uses the front-
facing camera. Its only runtime role: when bucket detection
confidence is low, it biases the bucket pick toward the authored
default.