Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Authored by Souvik Mandal, Ph.D.

Project Leader & Instructor, Computational Behavioral Sciences, LS100, FAS, Harvard University | Linkedin ID: souvik-mandal-phd


Last updated: 2026-07-02

Open in Colab Open in GitHub Codespaces

Most pre-trained computer vision models are optimized for general datasets (for example, people, vehicles, or everyday objects) and do not directly model domain-specific keypoints such as those on a Kendo sword or a baseball bat.
To track these reliably, we fine-tune a pre-trained model on a custom labeled dataset using transfer learning. This lets the model adapt existing visual features to your specific objects and keypoint definitions with far less data and compute than training from scratch.

Step 0: Environment Setup

Before we begin, I recommend creating a new Python virtual environment and install the necessary Python libraries. Think of this as setting up your digital laboratory.

  • To create a new Python virtual environment, go to your terminal, navigate to the folder where you want to save the environment (using cd your/chosen/path) and then type the following.

python3.12 - venv CV_VEnv
  • Activate the environment.

source path/to/CV_VEnv/bin/activate # For Mac/Linux

or

venv\Scripts\activate # For Windows CMD

  • Next, install ipykernel using pip install ipykernel on the terminal.

  • Register the Jupyter kernel

path/to/CV_VEnv/bin/python -m ipykernel install --user --name CV_VEnv --display-name "CV_VEnv"

  • Open your notebook from this virtual environment using jupyter notebook and select “CV_VEnv” Kernel.


Step 1: Extracting Training Data - Frames from videos

The first step is building a small “dataset” of images extracted from the videos and maually label them. About 100 to 200 images of the object in various positions, lighting, and states of motion blur should be sufficient to start training our model.

We will use OpenCV to automatically slice our training videos into a folder of still images. We have built two modes for this:

  1. Evenly Spaced (method="evenly"): Grabs a frame every few seconds. Great for getting a chronological sample of the whole video.

  2. Random (method="random"): Grabs frames at completely random times. This is the secret to a great AI, as it forces the model to learn what the object looks like in weird, unexpected, and chaotic situations!

For this notebook, we will be using:

  • ultralytics: The engine for YOLO26, which handles object detection and tracking.

  • opencv-python: The industry standard for video and image processing (reading frames, drawing skeletons, and saving videos).

  • pandas & numpy: The “bread and butter” of data science for handling our coordinate spreadsheets and math.

  • scipy: Used for the advanced “Hungarian Algorithm” math that helps us identify people across frames.

  • matplotlib: Our graphing library for visualizing strike velocities.

  • tqdm: A utility to give us those beautiful progress bars so we aren’t left guessing if the code is still running.

Note: In a Jupyter environment, we use the ! prefix to run terminal commands directly from a cell.

Step 2: Automated Multi-Video Frame Extraction

To make our pipeline scalable, we are automating the folder management. Instead of manually creating directories, this script will:

  1. Identify each video in your input folder.

  2. Create a unique sub-folder for each video using the name of the video file followed by the suffix extracted-frames_.

  3. Organize the frames directly into their respective folders.

This approach ensures that if you have 10 videos, all the images stay grouped logically, making it much easier to manage your training data.

We will continue to use the Random sampling method by default, as it provides the best data variety, generally preferred for training AI models.

EXECUTE THE FRAME-EXTRACTION PIPELINE

Provide the path that contains the video(s) to the variable target_path. Generally, a total of 100-200 farmes would be a good start. So, if you have 5 videos, frames_per_video could be between 20 to 40.

Step 3: Launching Label Studio (Locally)

Now that we have the extracted frames, we need to annotate the position of our intended objiects. We will use these annonated labels to train our custom model. We will use Label Studio for this.

Step 3.1.: Let’s first install it on our working virtual environment. For more information on instllation, visit Label Studio’s official installation guide.

Because Label Studio runs as a web server, launching it directly inside a notebook cell can block or destabilize the kernel. Instead, we launch it from the terminal (or as a background process) so the notebook remains responsive. This workflow will:

  1. Launch the server in the background.

  2. Wait for some time (could be a minute or two) for it to boot up.

  3. Automatically open a new tab in your web browser so you can start labeling.

Step 3.2. Before launching Label Studio as a local server process, terminate any existing Label Studio or related background process to avoid port conflicts and ensure a clean startup.

🛑 Cleaned up background processes.

Step 3.3.: Annotating the Dataset with Label Studio

Now that we have terminated any background processes, we will launch Label Studio directly from your Mac’s Terminal to ensure it runs stably and allows us to monitor the server logs.

Instructions:

  1. Launch the Server: Go to your Mac Terminal or Windows Command Prompt, type label-studio, and press return. This will start the server and automatically open a browser window to http://localhost:8080.

  2. Create an Account: In the Label Studio web interface, first create an account with random email id and password (nothing is happening over the web, EVERYTHING is on your local machine).

  3. Create the Project: Once you are logged in, click the “Create” button in the top right. Give an appropriate name to your project like Object_Kinematics. Do NOT save it yet.

  4. Import Your Images: Go to the “Data Import” tab. You can click on the “upload Files” or “Drag & Drop” the extracted image frames (generated in Step 1) into the browser. Keep the imported images under 100.

  5. Configure the Custom Keypoint Interface: * Nest, go to the “Labeling Setup” tab and choose “Keypoint Labeling”. Under the heading Add Label names", add appropriate names for each keypoints you want to annotate, like object1_tip, object1_base, object2_tip, object2_base, etc. Click on “add” after typing each label. Delete the in-built labels like “Face”, “Nose” by hovering over the names and click on the delete icon.

  6. Click “Save”.

  7. Annotate the Frames (The Workflow): Click your first image to begin.

    • Label Keypoint Click on the name of the keypoint on the top ribbon, and then click on the appropriate place on the image. repeat that for all the keypoints. Once you are done lableing all keypoints on a frame, click the “Submit” button in the bottom. Then click on the next image and repeat.

    • If a Keypoint is hidden (Occluded): If a keypoint is not visible, do not drop the keypoint. Only label what is visible to your eye.

    • If the object is missing: If the object is not in the frame at all, do not draw any points. Click “Submit” on the empty frame to “teach” the model what “No object” looks like.

  8. Export the Raw Data: Once all frames are labeled, return to the project dashboard by clicking on the name of the project on the top ribbon. Click the “Export” button. Because standard YOLO Pose isn’t directly available, select the standard JSON format (not JSON-MIN).

  9. Save the File: Download the exported file, rename it to labels.json, and place it in the same directory as this Jupyter Notebook.

(Note: When you are completely finished with this phase, you can go back to your Mac Terminal and press Ctrl + C to safely shut down the Label Studio server.)

Step 3.4 Parsing the Dataset & Auto-Generating Bounding Boxes

Because we only labeled the precise keypoints and skipped drawing bounding boxes, we need to adapt our data for YOLO.

YOLO Pose strictly requires a bounding box for every object. This script will:

  1. Parse the labels.json file to find object1_tip, object1_base, object2_tip, and object2_base.

  2. Mathematically calculate a bounding box that perfectly surrounds the keypoints for each object, adding a 5% “padding” so the edges of the objects aren’t cut off.

  3. Output the properly formatted .txt files into our CustomObject_Dataset/labels folder.

🔄 Starting Data Translation & Box Generation...
✅ Conversion Complete! Processed 16 frames into 'CustomObject_Dataset'.

Step 3.4. Generating the Configuration File (data.yaml)

We now generate the configuration file for YOLO. Because we labeled attacking and defending objects separately, we are telling the model to look for 2 Classes, each with 2 Keypoints.

✅ Configuration saved to: CustomObject_Dataset/data.yaml

Step 3.5. Training the Dual-Object Pose Model

The data is fully parsed and mapped! We will now initialize the YOLOv26 Nano Pose model and fine-tune it on our dataset. The model will learn to distinguish not only the tip from the base, but object 1 from object 2.

Step 5.5: Evaluating Model Health & Efficiency

Before we run our model on a new video, we need to check its “report card.” During training, YOLO automatically tracked several key metrics:

  1. Loss (Box & Pose): This measures how “wrong” the AI’s predictions were compared to our Label Studio annotations. We want this number to go DOWN over the 50 epochs.

  2. mAP (Mean Average Precision): This is the AI’s accuracy score. mAP50(P) specifically measures how accurately it found our Tip and Base keypoints (Pose). We want this number to go UP (closer to 1.0 or 100%).

Let’s visualize the training results!

Loading Training Metrics...

Here is the complete training overview generated by YOLO:
<IPython.core.display.Image object>
<Figure size 1200x500 with 2 Axes>

What to look for when you run this:

  • The Loss Curve (Left Graph): You want to see a steep drop in the first 10-20 epochs, followed by a gradual leveling off (a nice “L” shape). If the Validation Loss starts spiking back up at the end, it means the model is “overfitting” (memorizing the data instead of learning the concept).

  • The mAP Curve (Right Graph): You want to see this climb up toward 0.8 or 0.9 (80% to 90% accuracy). If the objects blur heavily during a fast swing, anything above 0.7 for a custom pose model on a small dataset is actually a massive victory!

Step 6: Extracting Kinematic Data from a New Video

Once our model is fully trained, we will use it to analyze a completely new video.

This script will feed the video into our custom YOLO model frame-by-frame. The model will locate the Object 1 and the Object 2, and extract the precise X and Y coordinates for the Tip and Base of each.

Instead of just drawing lines on a video, we are going to export this raw data into a CSV spreadsheet. This is the foundation of sports analytics—with this CSV, we can calculate swing speed, impact angles, and acceleration!

  • Dynamic Naming: The pipeline automatically names the output .csv file based on the file name of the input video so you never accidentally overwrite your data!

  • If the model is “young” (trained on limited data), it might have lower confidence. We have lowered the confidence threshold (conf=0.1) so the model doesn’t aggressively filter out its predictions. With more training, you can eventually push it to even 0.9.

Step 7: Drawing the Tracking Overlay

Now that we have successfully extracted our coordinate data into a .csv file, we can visualize it!

This script takes our raw dataset and overlays it back onto the original video. We will use OpenCV to draw physical lines connecting the Base and Tip of each object frame-by-frame.

  • Object 1: Drawn with a Red line.

  • Object 2: Drawn with a Blue line.

  • Tips: Marked with a bright Yellow dot so we can easily track the striking edge.