---
title: "Exercise form rules: the joint angles open-source trackers use"
canonical: "https://aifitnessapi.com/tools/exercise-form-rules"
markdown: "https://aifitnessapi.com/tools/exercise-form-rules.md"
type: "tool"
last_reviewed: "2026-11-05"
description: "Joint-angle rules for squats, push-ups, lunges, planks, curls, presses and deadlifts from open-source trackers, each quoted from its code. JSON and CSV."
publisher: "AIFitnessAPI — funded by KinesteX; first-party pages disclose it"
cite_as: "\"Exercise form rules: the joint angles open-source trackers use\", AIFitnessAPI, https://aifitnessapi.com/tools/exercise-form-rules"
---

# Exercise form rules: the joint angles open-source trackers use

> Open-source pose-estimation exercise trackers count and check reps with fixed joint-angle thresholds, and they do not agree: one licensed project counts a push-up between elbow angles of 145 and 90 degrees, another between 160 and 90. This dataset collects 22 such thresholds for 7 exercises from 4 licensed projects, each with the line of code it was read from on 2026-11-05, and marks 3 rules null where no source read states a number. They are the authors’ choices, not biomechanical standards. CC BY 4.0, JSON and CSV.

- Canonical: https://aifitnessapi.com/tools/exercise-form-rules
- Last reviewed: 2026-11-05
- Publisher: AIFitnessAPI (https://aifitnessapi.com)
- Cite as: "Exercise form rules: the joint angles open-source trackers use", AIFitnessAPI, https://aifitnessapi.com/tools/exercise-form-rules

---

[Download JSON](https://aifitnessapi.com/datasets/exercise-form-rules-2026.json) · [Download CSV](https://aifitnessapi.com/datasets/exercise-form-rules-2026.csv) · [Try a rule in the rep counter](https://aifitnessapi.com/tools/rep-counter-playground)

## Squat

Load into the rep counter: [knee 160°/100° (PythonExcercisePoseEstimation (thphuc05))](https://aifitnessapi.com/tools/rep-counter-playground?preset=squat-knee-thphuc05)

| Rule | Measures | Value | Source and evidence |
|---|---|---|---|
| rep-count: Back at the top (standing): knee angle above this, together with the hip angle above its own top value, ends a rep. | knee · hip · knee · ankle | > 160° | `ExerciseConfig(name="Squat", knee_angle_up=160, knee_angle_down_entry=100,` · applied: `if knee_angle > cfg.knee_angle_up and hip_angle > cfg.hip_angle_up:` · measured: `knee_angle = calculate_angle(hip, knee, ankle)` · [PythonExcercisePoseEstimation (thphuc05)](https://github.com/thphuc05/PythonExcercisePoseEstimation/blob/main/pose_estimation.py#L49), MIT |
| rep-count: Going down: knee angle below this, from the top, enters the down phase. | knee · hip · knee · ankle | < 100° | `ExerciseConfig(name="Squat", knee_angle_up=160, knee_angle_down_entry=100,` · applied: `if knee_angle < cfg.knee_angle_down_entry and stage == "up":` · measured: `knee_angle = calculate_angle(hip, knee, ankle)` · [PythonExcercisePoseEstimation (thphuc05)](https://github.com/thphuc05/PythonExcercisePoseEstimation/blob/main/pose_estimation.py#L49), MIT |
| rep-count: Back at the top: hip angle above this is also required to end a rep. | hip · knee · hip · shoulder | > 160° | `knee_angle_down_entry=100, # Squat hip_angle_up=160, correct_knee_max=95` · applied: `if knee_angle > cfg.knee_angle_up and hip_angle > cfg.hip_angle_up:` · measured: `hip_angle = calculate_angle(knee, hip, shouder)` · [PythonExcercisePoseEstimation (thphuc05)](https://github.com/thphuc05/PythonExcercisePoseEstimation/blob/main/pose_estimation.py#L49), MIT |
| form-check: Depth: the smallest knee angle of the rep must fall below this (with the hip and hip-drop checks) for the rep to be marked correct. | knee · hip · knee · ankle | < 95° | `knee_angle_down_entry=100, # Squat hip_angle_up=160, correct_knee_max=95` · applied: `if is_hip_drop and min_knee_angle < cfg.correct_knee_max and min_hip_angle < cfg.correct_hip_max:` · measured: `knee_angle = calculate_angle(hip, knee, ankle)` · [PythonExcercisePoseEstimation (thphuc05)](https://github.com/thphuc05/PythonExcercisePoseEstimation/blob/main/pose_estimation.py#L49), MIT |
| form-check: Depth: the smallest hip angle of the rep must fall below this (with the knee check) for the rep to be marked correct. | hip · knee · hip · shoulder | < 95° | `correct_knee_max=95, correct_hip_max=95, hip_drop_min=0.08)` · applied: `if is_hip_drop and min_knee_angle < cfg.correct_knee_max and min_hip_angle < cfg.correct_hip_max:` · measured: `hip_angle = calculate_angle(knee, hip, shouder)` · [PythonExcercisePoseEstimation (thphuc05)](https://github.com/thphuc05/PythonExcercisePoseEstimation/blob/main/pose_estimation.py#L50), MIT |
| form-check: Foot placement: foot width divided by shoulder width must fall within this range. | stance | 1.2 to 2.8 | `FOOT_SHOULDER_RATIO_THRESHOLDS = [1.2, 2.8]` · applied: `if min_ratio_foot_shoulder <= foot_shoulder_ratio <= max_ratio_foot_shoulder:` · measured: `foot_shoulder_ratio = round(foot_width / shoulder_width, 1)` · [Exercise-Correction (Ngô Hồng Quốc Bảo)](https://github.com/NgoQuocBao1010/Exercise-Correction/blob/main/web/server/detection/squat.py#L162), MIT |

How these are detected on camera: [squat detection](https://aifitnessapi.com/motion/squat-detection-pose-estimation).

## Push-up

Load into the rep counter: [elbow 145°/90° (Ultralytics AIGym (workouts monitoring solution))](https://aifitnessapi.com/tools/rep-counter-playground?preset=push-up-elbow-ultralytics) · [elbow 160°/90° (PythonExcercisePoseEstimation (thphuc05))](https://aifitnessapi.com/tools/rep-counter-playground?preset=push-up-elbow-thphuc05)

| Rule | Measures | Value | Source and evidence |
|---|---|---|---|
| rep-count: Up: angle above this sets the stage to up. The documented default keypoints are for push-ups. | elbow · right_shoulder · right_elbow · right_wrist · COCO 6, 8, 10 | > 145° | `up_angle: float = 145.0` · applied: `elif state["angle"] > self.up_angle:` · measured: `kpts=[6, 8, 10], # keypoints for monitoring specific exercise, by default it's for pushup` · [Ultralytics AIGym (workouts monitoring solution)](https://github.com/ultralytics/ultralytics/blob/main/ultralytics/solutions/config.py#L79), AGPL-3.0 |
| rep-count: Down: angle below this sets the stage to down, and counts a rep if the stage was up (the source counts on reaching the bottom, not on the return). | elbow · right_shoulder · right_elbow · right_wrist · COCO 6, 8, 10 | < 90° | `down_angle: float = 90.0` · applied: `if state["angle"] < self.down_angle: if state["stage"] == "up": state["count"] += 1` · measured: `kpts=[6, 8, 10], # keypoints for monitoring specific exercise, by default it's for pushup` · [Ultralytics AIGym (workouts monitoring solution)](https://github.com/ultralytics/ultralytics/blob/main/ultralytics/solutions/config.py#L80), AGPL-3.0 |
| rep-count: Up: elbow angle above this, with the body straight, ends a rep. | elbow · wrist · elbow · shoulder | > 160° | `ExerciseConfig(name="PushUp", elbow_angle_up=160, elbow_angle_down_entry=90,` · applied: `if elbow_angle > cfg.elbow_angle_up:` · measured: `elbow_angle = calculate_angle(wrist, elbow, shouder)` · [PythonExcercisePoseEstimation (thphuc05)](https://github.com/thphuc05/PythonExcercisePoseEstimation/blob/main/pose_estimation.py#L52), MIT |
| rep-count: Down: elbow angle below this, from the top, enters the down phase. | elbow · wrist · elbow · shoulder | < 90° | `ExerciseConfig(name="PushUp", elbow_angle_up=160, elbow_angle_down_entry=90,` · applied: `if elbow_angle < cfg.elbow_angle_down_entry and stage == "up":` · measured: `elbow_angle = calculate_angle(wrist, elbow, shouder)` · [PythonExcercisePoseEstimation (thphuc05)](https://github.com/thphuc05/PythonExcercisePoseEstimation/blob/main/pose_estimation.py#L52), MIT |
| form-check: Depth: the smallest elbow angle of the rep must fall below this for the rep to be marked correct. | elbow · wrist · elbow · shoulder | < 90° | `correct_elbow_max=90, body_straight_min=160)` · applied: `if min_elbow_angle < cfg.correct_elbow_max:` · measured: `elbow_angle = calculate_angle(wrist, elbow, shouder)` · [PythonExcercisePoseEstimation (thphuc05)](https://github.com/thphuc05/PythonExcercisePoseEstimation/blob/main/pose_estimation.py#L53), MIT |
| form-check: Straight body: the shoulder-hip-ankle angle must stay above this for the source to track the rep at all. | body-line · shoulder · hip · ankle | > 160° | `correct_elbow_max=90, body_straight_min=160)` · applied: `is_body_straight = body_line_angle > cfg.body_straight_min` · measured: `body_line_angle = calculate_angle(shouder, hip, ankle)` · [PythonExcercisePoseEstimation (thphuc05)](https://github.com/thphuc05/PythonExcercisePoseEstimation/blob/main/pose_estimation.py#L53), MIT |

How these are detected on camera: [push-up detection](https://aifitnessapi.com/motion/push-up-counter-computer-vision).

## Lunge

No source read gives both a top and a bottom angle for counting, so there is no rep-counter preset for this exercise.

| Rule | Measures | Value | Source and evidence |
|---|---|---|---|
| form-check: At the bottom (the source's down stage): each knee angle must fall within this range, or the rep is flagged with a knee-angle error. | knee · hip · knee · ankle | 60° to 125° | `KNEE_ANGLE_THRESHOLD = [60, 125]` · applied: `if angle_thresholds[0] <= results["right"]["angle"] <= angle_thresholds[1]:` · measured: `results["right"]["angle"] = calculate_angle(right_hip, right_knee, right_ankle)` · [Exercise-Correction (Ngô Hồng Quốc Bảo)](https://github.com/NgoQuocBao1010/Exercise-Correction/blob/main/web/server/detection/lunge.py#L183), MIT |
| rep-count: Top and bottom knee angles for counting lunge reps. | knee · hip · knee · ankle | null | The one licensed lunge source read counts lunge stages with a trained classifier, not an angle threshold, so it states no top or bottom angle. The peer-reviewed state-machine definition cited on /motion/lunge-detection-pose-estimation is on a host the build environment could not reach on the fetch date. `STAGE_ML_MODEL_PATH = get_static_file_url("model/lunge_stage_model.pkl")` [NgoQuocBao1010/Exercise-Correction](https://github.com/NgoQuocBao1010/Exercise-Correction/blob/main/web/server/detection/lunge.py#L178) |

How these are detected on camera: [lunge detection](https://aifitnessapi.com/motion/lunge-detection-pose-estimation).

## Plank

No source read gives both a top and a bottom angle for counting, so there is no rep-counter preset for this exercise.

| Rule | Measures | Value | Source and evidence |
|---|---|---|---|
| hold: Hold: the hip angle must stay above this; dropping below stops the hold timer. | hip · knee · hip · shoulder | > 160° | `ExerciseConfig(name="Plank", plank_hip_min=160, hold_frames_required=60)` · applied: `if hip_angle > cfg.plank_hip_min:` · measured: `hip_angle = calculate_angle(knee, hip, shouder)` · [PythonExcercisePoseEstimation (thphuc05)](https://github.com/thphuc05/PythonExcercisePoseEstimation/blob/main/pose_estimation.py#L51), MIT |
| hold: Hold start: the hip rule must hold for this many consecutive frames before the timer starts (the source's comment says 2 s at 30 fps). | — | >= 60 frames | `ExerciseConfig(name="Plank", plank_hip_min=160, hold_frames_required=60)` · applied: `if down_frame_counter >= cfg.hold_frames_required and not timer_running:` · measured: `down_frame_counter += 1` · [PythonExcercisePoseEstimation (thphuc05)](https://github.com/thphuc05/PythonExcercisePoseEstimation/blob/main/pose_estimation.py#L51), MIT |

How these are detected on camera: [plank detection](https://aifitnessapi.com/motion/plank-detection-pose-estimation).

## Bicep curl

Load into the rep counter: [elbow 120°/100° (Exercise-Correction (Ngô Hồng Quốc Bảo))](https://aifitnessapi.com/tools/rep-counter-playground?preset=bicep-curl-elbow-exercise-correction)

| Rule | Measures | Value | Source and evidence |
|---|---|---|---|
| rep-count: Arm extended (the source's down stage): elbow angle above this. | elbow · shoulder · elbow · wrist | > 120° | `STAGE_DOWN_THRESHOLD = 120` · applied: `if bicep_curl_angle > self.stage_down_threshold:` · measured: `bicep_curl_angle = int(calculate_angle(self.shoulder, self.elbow, self.wrist))` · [Exercise-Correction (Ngô Hồng Quốc Bảo)](https://github.com/NgoQuocBao1010/Exercise-Correction/blob/main/web/server/detection/bicep_curl.py#L244), MIT |
| rep-count: Curled (the source's up stage): elbow angle below this, after the arm was extended, counts a rep (the source counts on reaching the curl, not on the return). | elbow · shoulder · elbow · wrist | < 100° | `STAGE_UP_THRESHOLD = 100` · applied: `elif bicep_curl_angle < self.stage_up_threshold and self.stage == "down":` · measured: `bicep_curl_angle = int(calculate_angle(self.shoulder, self.elbow, self.wrist))` · [Exercise-Correction (Ngô Hồng Quốc Bảo)](https://github.com/NgoQuocBao1010/Exercise-Correction/blob/main/web/server/detection/bicep_curl.py#L243), MIT |
| form-check: Full range: a rep whose smallest elbow angle is this or more is flagged as a peak-contraction error. | elbow · shoulder · elbow · wrist | < 60° | `PEAK_CONTRACTION_THRESHOLD = 60` · applied: `and self.peak_contraction_angle >= self.peak_contraction_threshold` · measured: `bicep_curl_angle = int(calculate_angle(self.shoulder, self.elbow, self.wrist))` · [Exercise-Correction (Ngô Hồng Quốc Bảo)](https://github.com/NgoQuocBao1010/Exercise-Correction/blob/main/web/server/detection/bicep_curl.py#L247), MIT |
| form-check: Loose upper arm: an angle between the upper arm and the vertical above this is flagged as an error. | upper-arm-vertical · elbow · shoulder · vertical | <= 40° | `LOOSE_UPPER_ARM_ANGLE_THRESHOLD = 40` · applied: `if ground_upper_arm_angle > self.loose_upper_arm_angle_threshold:` · measured: `calculate_angle(self.elbow, self.shoulder, shoulder_projection)` · [Exercise-Correction (Ngô Hồng Quốc Bảo)](https://github.com/NgoQuocBao1010/Exercise-Correction/blob/main/web/server/detection/bicep_curl.py#L251), MIT |
| form-check: Full range: if the smallest elbow angle over the set is above this, the set is flagged as not curled all the way up. | elbow · shoulder · elbow · wrist | <= 70° | `if upper_arm_forearm_min > 70.0:` · applied: `You are not curling the weight all the way to the top, up to your shoulders.` · measured: `upper_arm_forearm_angles = np.degrees(np.arccos(np.clip(np.sum(np.multiply(upper_arm_vecs, forearm_vecs), axis=1), -1.0, 1.0)))` · [Pose Trainer (Steven Chen and Richard Yang)](https://github.com/stevenzchen/pose-trainer/blob/master/evaluate.py#L77), GPL-3.0 |
| form-check: Upper arm still: if the upper-arm-to-torso angle varies by more than this over the set, the set is flagged for shoulder rotation. | upper-arm-torso · shoulder · elbow · hip · neck | <= 35° | `if upper_arm_torso_range > 35.0:` · applied: `Your upper arm shows significant rotation around the shoulder when curling.` · measured: `upper_arm_torso_range = np.max(upper_arm_torso_angles) - np.min(upper_arm_torso_angles)` · [Pose Trainer (Steven Chen and Richard Yang)](https://github.com/stevenzchen/pose-trainer/blob/master/evaluate.py#L72), GPL-3.0 |

How these are detected on camera: [bicep curl detection](https://aifitnessapi.com/motion/bicep-curl-counter-pose-estimation).

## Shoulder press

No source read gives both a top and a bottom angle for counting, so there is no rep-counter preset for this exercise.

| Rule | Measures | Value | Source and evidence |
|---|---|---|---|
| form-check: Lockout: if the largest elbow angle over the set is below this, the set is flagged as not pressed all the way up. | elbow · shoulder · elbow · wrist | >= 178° | `if upper_forearm_angle < 178:` · applied: `You are not lifting the weight all the way up. Extend your arms through the full range of motion.` · measured: `print("Max upper arm and forearm angle: ", np.max(upper_arm_forearm_angles))` · [Pose Trainer (Steven Chen and Richard Yang)](https://github.com/stevenzchen/pose-trainer/blob/master/evaluate.py#L257), GPL-3.0 |
| rep-count: Top and bottom elbow angles for counting shoulder-press reps. | elbow · shoulder · elbow · wrist | null | The one licensed shoulder-press source read judges a whole recorded set by its largest elbow angle and counts no reps, so it states no bottom position and no per-rep top. No other licensed source read gives one. `print("Max upper arm and forearm angle: ", np.max(upper_arm_forearm_angles))` [stevenzchen/pose-trainer](https://github.com/stevenzchen/pose-trainer/blob/master/evaluate.py#L244) |

How these are detected on camera: [shoulder press detection](https://aifitnessapi.com/motion/overhead-press-pose-estimation).

## Deadlift

No source read gives both a top and a bottom angle for counting, so there is no rep-counter preset for this exercise.

| Rule | Measures | Value | Source and evidence |
|---|---|---|---|
| rep-count: Top and bottom hip angles for counting deadlift reps. | hip · shoulder · hip · knee | null | No licensed open-source deadlift rule could be read: the two deadlift repositories found ship no LICENSE file. The peer-reviewed state-machine definition cited on /motion/deadlift-pose-estimation is on a host the build environment could not reach on the fetch date, so its thresholds are not copied; it is listed in ops/sources/form-rules.txt for a snapshot run. |

How these are detected on camera: [deadlift detection](https://aifitnessapi.com/motion/deadlift-pose-estimation).

## Sources

- [Ultralytics AIGym (workouts monitoring solution)](https://github.com/ultralytics/ultralytics) (ultralytics/ultralytics, branch main), licensed [AGPL-3.0](https://github.com/ultralytics/ultralytics/blob/main/LICENSE), built on Ultralytics YOLO pose (COCO 17 keypoints). Angles computed as `radians = math.atan2(c[1] - b[1], c[0] - b[0]) - math.atan2(a[1] - b[1], a[0] - b[0])`. Files read on 2026-11-05: ultralytics/solutions/config.py, ultralytics/solutions/ai_gym.py, ultralytics/solutions/solutions.py, docs/en/guides/workouts-monitoring.md.
- [Pose Trainer (Steven Chen and Richard Yang)](https://github.com/stevenzchen/pose-trainer) (stevenzchen/pose-trainer, branch master), licensed [GPL-3.0](https://github.com/stevenzchen/pose-trainer/blob/master/LICENSE), built on OpenPose. Angles computed as `upper_arm_forearm_angles = np.degrees(np.arccos(np.clip(np.sum(np.multiply(upper_arm_vecs, forearm_vecs), axis=1), -1.0, 1.0)))`. Its README warns: “WARNING: This code is experimental, and is no longer maintained or supported. It is not expected to work.” Files read on 2026-11-05: evaluate.py, README.md.
- [Exercise-Correction (Ngô Hồng Quốc Bảo)](https://github.com/NgoQuocBao1010/Exercise-Correction) (NgoQuocBao1010/Exercise-Correction, branch main), licensed [MIT](https://github.com/NgoQuocBao1010/Exercise-Correction/blob/main/LICENSE), built on MediaPipe Pose (BlazePose 33 landmarks). Angles computed as `angleInDeg = angleInDeg if angleInDeg <= 180 else 360 - angleInDeg`. Files read on 2026-11-05: web/server/detection/bicep_curl.py, web/server/detection/lunge.py, web/server/detection/squat.py, web/server/detection/plank.py, web/server/detection/utils.py.
- [PythonExcercisePoseEstimation (thphuc05)](https://github.com/thphuc05/PythonExcercisePoseEstimation) (thphuc05/PythonExcercisePoseEstimation, branch main), licensed [MIT](https://github.com/thphuc05/PythonExcercisePoseEstimation/blob/main/LICENSE), built on MediaPipe Pose Landmarker (BlazePose 33 landmarks). Angles computed as `if angle > 180.0: angle = 360 - angle`. Files read on 2026-11-05: pose_estimation.py.

## How this dataset is built, and its limits

- Only code under an open-source license whose LICENSE file could be read is used. Repositories whose README claims a license but that ship no LICENSE file were left out.
- No number is typed by hand. The generator fetches each file, refuses to write unless the quoted line is in it, and captures the value from that line; the line that applies the value and the line that computes the angle are stored and checked the same way, and a weekly job re-checks every quote against the live file.
- Peer-reviewed definitions, such as the finite-state exercise definitions cited on [deadlift detection](https://aifitnessapi.com/motion/deadlift-pose-estimation), were on hosts the build environment could not reach on 2026-11-05. Their numbers are not copied until a fetched copy can be quoted, which is why the deadlift rules are null.
- Every source measures 2D angles in the image, from one model and one camera setup its author chose. A threshold that suits a side-on phone at hip height can fail from the front. The values are starting points to test, not a standard of correct form, and nothing here is coaching or medical advice.
- Some sources count a rep on the way down, others on the way back up; the “meaning” column says which, in the source’s own terms where they differ from the rep counter’s.

An open dataset from AIFitnessAPI, CC BY 4.0, listed with the others on [open datasets](https://aifitnessapi.com/datasets). Test a rule with [the rep counter playground](https://aifitnessapi.com/tools/rep-counter-playground); the method behind it is [how form feedback works](https://aifitnessapi.com/motion/how-form-feedback-works).
