WellAlly Logo
WellAlly康心伴
Development

Detecting Running Form Mistakes with Python, OpenCV, and Computer Vision

Learn how to build an automated running form analyzer using Python, OpenCV, and MediaPipe. This tutorial guides you through detecting common issues like overstriding and vertical oscillation from video footage.

W
2025-12-15
9 min read

Whether you're a seasoned marathoner or just starting your running journey, proper form is crucial for both performance and injury prevention. Two of the most common and detrimental running form mistakes are overstriding and excessive vertical oscillation (bouncing). These issues can lead to wasted energy and an increased risk of injuries like shin splints and runner's knee.

Traditionally, getting feedback on your running form required an expensive session with a coach or a visit to a specialized gait analysis lab. But what if you could get valuable insights using just a video of yourself running and a bit of Python code?

In this tutorial, we'll build a running form analyzer that can automatically detect overstriding and excessive vertical oscillation from a video file. We'll use the power of OpenCV for video processing and Google's MediaPipe library for accurate pose estimation. This project is a fantastic example of how computer vision can be applied to fitness and health technology.

Prerequisites:

  • Basic understanding of Python.
  • Familiarity with NumPy for numerical operations.
  • Python and pip installed on your system.
  • A video of a person running on a treadmill, filmed from the side.

Understanding the Problem

The Biomechanics of Running Mistakes

To build our detector, we first need to understand what we're looking for.

1. Overstriding: This happens when your foot lands too far in front of your body's center of mass, often with a straightened knee. This creates a braking force with every step, slowing you down and increasing the impact on your joints. We can detect overstriding by measuring the shin angle at the moment the foot strikes the ground. A shin that is close to vertical is ideal, while a large forward angle indicates overstriding.

2. Excessive Vertical Oscillation: This is the "bouncing" motion some runners have. While some vertical movement is necessary, too much of it means you're wasting energy moving up and down instead of forward. A typical vertical oscillation for efficient runners is between 5-10 cm. We can measure this by tracking the vertical movement of the runner's hip throughout their gait cycle.

Prerequisites

Let's set up our Python environment. It's a good practice to use a virtual environment to manage your project's dependencies.

code
# Create a virtual environment
python -m venv running_analyzer_env

# Activate the virtual environment
# On Windows:
running_analyzer_env\Scripts\activate
# On macOS/Linux:
source running_analyzer_env/bin/activate

# Install the necessary libraries
pip install opencv-python mediapipe numpy
Code collapsed

You should see a successful installation message for each of these packages.

Step 1: Setting Up the Pose Estimation Pipeline

What we're doing

First, we need to process the video frame by frame and use MediaPipe to detect the runner's body landmarks. MediaPipe's Pose model provides 33 keypoints on the body, which is more than enough for our analysis.

Implementation

Create a new Python file, for example running_analyzer.py, and let's start by importing the necessary libraries and initializing the MediaPipe Pose model.

code
# running_analyzer.py
import cv2
import mediapipe as mp
import numpy as np

# Initialize MediaPipe Pose
mp_pose = mp.solutions.pose
pose = mp_pose.Pose(min_detection_confidence=0.5, min_tracking_confidence=0.5)
mp_drawing = mp.solutions.drawing_utils

def process_video(video_path):
    cap = cv2.VideoCapture(video_path)

    if not cap.isOpened():
        print("Error: Could not open video.")
        return

    while cap.isOpened():
        ret, frame = cap.read()
        if not ret:
            break

        # Convert the BGR image to RGB
        image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        image.flags.writeable = False

        # Make detection
        results = pose.process(image)

        # Recolor back to BGR
        image.flags.writeable = True
        image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)

        # Render detections
        if results.pose_landmarks:
            mp_drawing.draw_landmarks(image, results.pose_landmarks, mp_pose.POSE_CONNECTIONS)

        cv2.imshow('Running Form Analysis', image)

        if cv2.waitKey(10) & 0xFF == ord('q'):
            break

    cap.release()
    cv2.destroyAllWindows()

if __name__ == '__main__':
    # Replace 'running_video.mp4' with the path to your video
    process_video('running_video.mp4')
Code collapsed

How it works

  1. We import cv2 for video handling, mediapipe for pose estimation, and numpy for calculations.
  2. We initialize the mp.solutions.pose.Pose class, which contains the machine learning model for detecting body landmarks.
  3. The process_video function opens the video file and reads it frame by frame.
  4. For each frame, we convert it from BGR (OpenCV's default) to RGB, which is what MediaPipe expects.
  5. pose.process(image) runs the pose estimation model on the frame.
  6. The results contain the detected landmarks, which we then draw onto the frame using mp_drawing.draw_landmarks for visualization.
  7. Finally, we display the processed frame.

Common pitfalls

  • Video Path: Make sure the path to your video file is correct.
  • Camera Angle: For this analysis to be accurate, the video should be a clear side-view of the runner.

Step 2: Calculating Joint Angles and Detecting Overstriding

What we're doing

Now for the core logic of detecting overstriding. We need to:

  1. Identify the moment of "foot strike" – when the runner's foot first makes contact with the ground.
  2. At that moment, calculate the angle of the shin (the line from the knee to the ankle) relative to a vertical line.

Implementation

Let's create a helper function to calculate angles and then integrate it into our main video processing loop.

code
# Add this function to running_analyzer.py

def calculate_angle(a, b, c):
    """Calculates the angle between three points."""
    a = np.array(a)  # First point
    b = np.array(b)  # Mid point
    c = np.array(c)  # End point

    radians = np.arctan2(c[1] - b[1], c[0] - b[0]) - np.arctan2(a[1] - b[1], a[0] - b[0])
    angle = np.abs(radians * 180.0 / np.pi)

    if angle > 180.0:
        angle = 360 - angle

    return angle

# ... (rest of the code)

# Inside the 'while' loop of the process_video function, after getting the 'results':
# try:
#     landmarks = results.pose_landmarks.landmark
#
#     # Get coordinates for left side of the body
#     left_hip = [landmarks[mp_pose.PoseLandmark.LEFT_HIP.value].x, landmarks[mp_pose.PoseLandmark.LEFT_HIP.value].y]
#     left_knee = [landmarks[mp_pose.PoseLandmark.LEFT_KNEE.value].x, landmarks[mp_pose.PoseLandmark.LEFT_KNEE.value].y]
#     left_ankle = [landmarks[mp_pose.PoseLandmark.LEFT_ANKLE.value].x, landmarks[mp_pose.PoseLandmark.LEFT_ANKLE.value].y]
#
#     # Simple foot strike detection (when ankle is at its lowest point)
#     # A more robust method would be needed for varied terrain
#     if 'prev_left_ankle_y' not in locals():
#         prev_left_ankle_y = left_ankle[1]
#
#     if prev_left_ankle_y < left_ankle[1]: # If ankle is moving down
#         # Foot strike is likely happening or about to happen
#         vertical_point = [left_knee[0], left_knee[1] + 0.1] # A point directly below the knee
#         shin_angle = calculate_angle(left_ankle, left_knee, vertical_point)
#
#         # Display shin angle and check for overstriding
#         cv2.putText(image, f"Shin Angle: {int(shin_angle)}",
#                     (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2, cv2.LINE_AA)
#
#         if shin_angle > 15: # Threshold for overstriding
#             cv2.putText(image, "Overstriding Detected",
#                         (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2, cv2.LINE_AA)
#
#     prev_left_ankle_y = left_ankle[1]
#
# except:
#     pass
Code collapsed

Note: For simplicity, the above code snippet shows the logic. You'll want to integrate this within the while loop of the process_video function and handle which leg is in front.

How it works

  1. The calculate_angle function takes three points and uses the arctangent of their coordinates to find the angle at the midpoint.
  2. We extract the x, y coordinates for the hip, knee, and ankle from the MediaPipe landmarks.
  3. For foot strike detection on a treadmill, a simple heuristic is to find when the ankle reaches its lowest vertical point in a stride.
  4. To calculate the shin angle relative to the vertical, we create a point directly below the knee and calculate the angle between the ankle, knee, and this vertical point.
  5. We then display this angle on the screen. If it exceeds a certain threshold (e.g., 15 degrees for heel strikers), we flag it as overstriding.

Common pitfalls

  • Coordinate System: Remember that in OpenCV, the y-axis increases downwards.
  • Foot Strike Detection: This simple method works best for treadmill running. For outdoor running, a more advanced algorithm might be needed to detect when the foot stops moving relative to the ground.

Step 3: Measuring Vertical Oscillation

What we're doing

To measure vertical oscillation, we'll track the vertical position of the hip over time. We need to find the highest and lowest points of the hip during a gait cycle to determine the amount of bounce.

Implementation

We'll need to keep track of the hip's vertical position across frames.

code
# Add these variables before the 'while' loop in process_video
hip_y_positions = []
vertical_oscillation = 0

# Inside the 'while' loop, within the 'try' block:
#     # ... (after getting landmark coordinates)
#
#     left_hip_y = landmarks[mp_pose.PoseLandmark.LEFT_HIP.value].y
#     hip_y_positions.append(left_hip_y)
#
#     # Analyze over a window of frames (e.g., 30 frames)
#     if len(hip_y_positions) > 30:
#         max_hip_y = max(hip_y_positions)
#         min_hip_y = min(hip_y_positions)
#
#         # Convert normalized coordinates to pixels to get a meaningful measurement
#         frame_height, frame_width, _ = image.shape
#         vertical_oscillation_pixels = (max_hip_y - min_hip_y) * frame_height
#
#         # You'd need a reference object of known size in the frame to convert pixels to cm
#         # For now, we'll just display the pixel value
#         cv2.putText(image, f"Vertical Oscillation (pixels): {int(vertical_oscillation_pixels)}",
#                     (10, 90), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2, cv2.LINE_AA)
#
#         hip_y_positions.pop(0) # Keep the list size constant
Code collapsed

How it works

  1. We store the vertical (y) coordinate of the hip in a list for each frame.
  2. We use a sliding window approach to find the maximum (lowest point on the screen) and minimum (highest point) y-values over the last 30 frames.
  3. The difference between these values gives us the vertical oscillation in terms of normalized coordinates.
  4. To get a real-world measurement (in cm), you would need to calibrate the system by having an object of a known size in the video frame to establish a pixel-to-cm ratio.

Putting It All Together

Here is the more complete code structure:

code
import cv2
import mediapipe as mp
import numpy as np

# ... (calculate_angle function here) ...

def analyze_running_form(video_path):
    mp_pose = mp.solutions.pose
    pose = mp_pose.Pose(min_detection_confidence=0.5, min_tracking_confidence=0.5)
    mp_drawing = mp.solutions.drawing_utils

    cap = cv2.VideoCapture(video_path)

    hip_y_positions = []
    prev_ankle_y = 0

    while cap.isOpened():
        ret, frame = cap.read()
        if not ret:
            break

        image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        image.flags.writeable = False
        results = pose.process(image)
        image.flags.writeable = True
        image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)

        try:
            landmarks = results.pose_landmarks.landmark
            
            # Use the side that is more visible to the camera
            hip = landmarks[mp_pose.PoseLandmark.LEFT_HIP.value]
            knee = landmarks[mp_pose.PoseLandmark.LEFT_KNEE.value]
            ankle = landmarks[mp_pose.PoseLandmark.LEFT_ANKLE.value]

            # --- Overstriding Detection ---
            if prev_ankle_y > 0 and ankle.y > prev_ankle_y: # Simple foot strike detection
                vertical_point = [knee.x, knee.y + 0.1]
                shin_angle = calculate_angle([ankle.x, ankle.y], [knee.x, knee.y], vertical_point)
                
                if shin_angle > 15:
                    cv2.putText(image, "OVERSTRIDING", (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2, cv2.LINE_AA)

            prev_ankle_y = ankle.y

            # --- Vertical Oscillation ---
            hip_y_positions.append(hip.y)
            if len(hip_y_positions) > 30: # Sliding window of 30 frames
                min_y = min(hip_y_positions)
                max_y = max(hip_y_positions)
                oscillation_normalized = max_y - min_y
                
                # To convert to cm, you need a reference object
                # oscillation_cm = oscillation_normalized * frame_height_in_pixels * (real_object_height_cm / object_height_in_pixels)
                
                if oscillation_normalized * 1000 > 60: # Example threshold
                     cv2.putText(image, "EXCESSIVE BOUNCE", (50, 100), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2, cv2.LINE_AA)

                hip_y_positions.pop(0)

        except Exception as e:
            # print(e) # For debugging
            pass

        mp_drawing.draw_landmarks(image, results.pose_landmarks, mp_pose.POSE_CONNECTIONS)
        cv2.imshow('Running Form Analyzer', image)

        if cv2.waitKey(5) & 0xFF == ord('q'):
            break

    cap.release()
    cv2.destroyAllWindows()

if __name__ == '__main__':
    analyze_running_form('running_video.mp4')
Code collapsed

Production Deployment Tips

  • Calibration: For accurate real-world measurements (cm instead of pixels), you must perform a camera calibration. This could involve placing an object of known height (like a cone) next to the treadmill.
  • Performance: MediaPipe is quite efficient, but for real-time analysis on low-power devices, you might consider using a lighter model or reducing the video resolution.
  • User Interface: For a user-friendly application, you could build a simple web interface using Flask or Streamlit to allow users to upload their videos and view the results.

Conclusion

We've successfully built a tool that can provide valuable biomechanical feedback to runners. By using OpenCV and MediaPipe, we were able to move beyond simple video playback and into the realm of quantitative analysis. This project demonstrates the power of computer vision in the health and fitness domain and serves as a great starting point for more advanced sports analytics projects.

Next Steps

  • Implement a more robust foot strike detection algorithm.
  • Add analysis for other running metrics like cadence (steps per minute).
  • Create a system to track form changes over time to monitor progress.

Resources

#

Article Tags

pythonopencvcomputervisionhealthtech
W

WellAlly's core development team, comprised of healthcare professionals, software engineers, and UX designers committed to revolutionizing digital health management.

Expertise

Healthcare TechnologySoftware DevelopmentUser ExperienceAI & Machine Learning

Found this article helpful?

Try KangXinBan and start your health management journey

© 2024 康心伴 WellAlly · Professional Health Management