Face Mask Detector with YOLOv8

Train a YOLOv8 model to detect face masks in real time via webcam.

Introduction

30 Second Summary

Face masks became a part of daily life during the pandemic, and many workplaces, hospitals, and transit systems still require them today. Automated detection systems can help enforce compliance without relying on manual checks at every entrance.

In this project, you will train a custom object detection model to recognize masked and unmasked faces, then deploy it as a live webcam detector that draws colored bounding boxes around faces in real time.

What You'll Build

A real-time face mask detection system powered by YOLOv8 that identifies masked and unmasked faces through your webcam with colored bounding boxes and confidence scores.

By the end of this project, you'll have:

  • A custom-trained YOLOv8 model that distinguishes "Mask" from "No Mask" faces, trained on 2,500+ real-world images using GPU-accelerated training.
  • A real-time webcam application that draws green bounding boxes for masked faces and red bounding boxes for unmasked faces, with confidence scores and an FPS counter.
  • Hands-on experience with the full ML workflow: dataset preparation, model training, evaluation, and deployment.
  • Secret Mission: Add an alert system that plays a warning sound and saves timestamped screenshots when an unmasked face is detected.

Are there any prerequisites?

You need intermediate Python knowledge to follow along. No prior machine learning or computer vision experience is required.

This project uses an NVIDIA GPU for training. The instructions are written for a Windows machine with an NVIDIA GeForce RTX 2070 Super and CUDA installed.

Before We Start

Training a face mask detector requires a specific set of tools working together. You need a deep learning framework that can leverage your GPU for fast training, a labeled dataset of face images, and an isolated Python environment to keep everything organized.

In this step, you will create your project environment, install the Ultralytics library (which bundles PyTorch and OpenCV for you), verify your GPU is ready, and download a pre-labeled dataset of 2,500+ face images.

In this step, get ready to:
  • Create your project folder and Python virtual environment.
  • Install packages and verify GPU detection.
  • Clone the face mask dataset.
Create a project folder and Python virtual environment

A virtual environment gives your project its own isolated set of Python packages. This prevents conflicts between projects that might need different library versions.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Cursor and press Enter to open it.
  • Open the integrated terminal by pressing Ctrl+` (backtick key, usually above Tab).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir mask-detector
cd mask-detector
  • Create a Python virtual environment by running:
python -m venv venv
  • Activate the virtual environment by running:
venv\Scripts\activate

You should see (venv) appear at the beginning of your terminal prompt.

What does (venv) mean?

The (venv) prefix tells you the virtual environment is active. Any packages you install with pip will go into this environment only, keeping your system Python clean.

Not seeing (venv) in your prompt?

If you get a permissions error on Windows, PowerShell may be blocking scripts. Run Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned and then try the activate command again.

Make sure you are inside the mask-detector folder when you run the activate command.

Help me activate my Python virtual environment on Windows.

Install the required packages

The ultralytics package is all you need to install. It automatically brings in PyTorch (with CUDA support), OpenCV, and every other dependency for training and running YOLOv8 models.

  • Install the ultralytics package by running:
pip install ultralytics

What does this install?

This single command installs the full Ultralytics toolkit along with PyTorch (GPU-enabled), OpenCV, NumPy, and other dependencies. The install may take a few minutes as it downloads large GPU libraries.

  • Once the install finishes, verify your GPU is detected by running:
python -c "import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name(0))"

You should see True NVIDIA GeForce RTX 2070 SUPER printed in the terminal.

  • Verify your Ultralytics installation by running:
yolo checks

This prints a summary of your system setup, confirming that Python, PyTorch, and CUDA are all detected correctly.

torch.cuda.is_available() returns False?

Make sure your NVIDIA drivers are up to date. Open NVIDIA GeForce Experience and check for driver updates.

Confirm you are running the command inside the (venv) environment. If you opened a new terminal, activate the environment again first.

Help me get CUDA working with PyTorch on my NVIDIA GPU.

Clone the face mask dataset

You need a labeled dataset to train your model. You will use a publicly available dataset of 2,500+ face images already annotated in YOLO format with two classes: "Mask" and "No Mask".

  • Check that Git is installed by running:
git --version

✔️ I see a version number

Git is ready to go. Continue to the next step below.

ⓧ Command not found

You need to install Git before continuing.

  • Install Git using winget by running this command:
winget install --id Git.Git -e --source winget
  • Close and reopen your terminal (Ctrl+` twice in Cursor) so the new git command becomes available.
  • Activate your virtual environment again by running:
cd ~/Desktop/mask-detector
venv\Scripts\activate
  • Run git --version again to confirm it now works.

Alternatively, download the installer from git-scm.com/download/win and run it with default settings.

  • Clone the face mask dataset into your project folder by running:
git clone https://github.com/LforikC/face-mask-dataset.git

What did this download?

This clones a repository containing 2,500+ pre-annotated face images organized into train, validation, and test splits. Each image has a matching .txt label file in YOLO annotation format with two classes: "Mask" (class 0) and "No Mask" (class 1).

The repository also includes a dataset.yaml configuration file that tells YOLOv8 where to find the images and what class names to use.

  • Confirm the dataset downloaded by running:
ls face-mask-dataset

You should see folders and files including data, dataset.yaml, and a README.md.

Clone failed or taking too long?

If you see a network timeout, check your internet connection and try the clone command again. The dataset is around 200-300 MB so it may take a minute on slower connections.

If you see "repository not found", double-check the URL has no typos. Copy and paste it directly from the code block above.

Help me clone the face mask dataset from GitHub.

Your environment is ready with GPU acceleration and a labeled dataset. Next up, you will explore the dataset structure and prepare it for training.

Explore and Prepare the Dataset

Before training a model, you need to understand the data it will learn from. A model is only as good as its dataset, so inspecting the images, verifying the labels, and confirming the file paths are critical first steps.

In this step, you will explore the face mask dataset structure, fix the configuration file so YOLOv8 can find your images, and visualize sample annotations to confirm everything is correctly labeled.

In this step, get ready to:
  • Inspect the dataset folder layout and understand YOLO annotation format.
  • Update dataset.yaml paths so they resolve correctly on your machine.
  • Visualize sample training images with their bounding box annotations.
Inspect the dataset structure

Every object detection dataset has two components: images and labels. The labels tell the model where objects are located in each image and what class they belong to.

  • View the top-level contents of the dataset folder by running this command:
ls face-mask-dataset/

You should see an images/ folder, a labels/ folder, and a dataset.yaml file.

  • Explore the images directory to see the training and validation splits by running:
ls face-mask-dataset/images/

You should see train/ and valid/ subfolders containing the training and validation images.

  • View the current contents of dataset.yaml by running:
cat face-mask-dataset/dataset.yaml

What does this file contain?

The dataset.yaml file tells YOLOv8 where to find images, how many classes exist, and what those classes are called. You will see paths, a nc (number of classes) value, and a names list.

The paths in this file currently use Windows backslashes and assume the dataset is nested inside a parent yolov5 directory. You will fix these in the next substep.

  • Look at a sample label file to understand the annotation format by running:
cat face-mask-dataset/labels/train/$(ls face-mask-dataset/labels/train/ | head -1)

Each line in a label file represents one detected object in the corresponding image.

What is YOLO annotation format?

Each label .txt file has one line per object. The format is: <class_id> <x_center> <y_center> <width> <height> with all coordinates normalized to a 0-1 range.

For example, 0 0.45 0.62 0.30 0.40 means class 0 (Mask) centered at 45% from the left and 62% from the top, spanning 30% of the image width and 40% of the height.

Update dataset.yaml paths for your project

The dataset was originally configured to work inside a yolov5 directory with Windows-style paths. You need to update the paths so YOLOv8 can locate your images from the mask-detector project folder.

  • In Cursor's Explorer sidebar, expand the face-mask-dataset folder and click on dataset.yaml to open it.
  • Replace the entire contents of the file with this corrected configuration:
# Paths relative to this yaml file's location
train: images/train
val: images/valid

# Number of classes
nc: 2

# Class names (class 0 = Mask, class 1 = No Mask)
names: ['Mask', 'No Mask']

What did you change?

You updated the train and val paths to use forward slashes relative to the yaml file's own location. YOLOv8 resolves dataset paths relative to where the dataset.yaml file sits.

The names list defines what each numeric ID means in the label files. Class 0 is "Mask" and class 1 is "No Mask". The nc: 2 confirms there are exactly two classes.

  • Save dataset.yaml by pressing Cmd+S (macOS) or Ctrl+S (Windows).

Not sure if paths are correct?

Run ls face-mask-dataset/images/train in your terminal. If you see image files listed, your train path is correct. Do the same for face-mask-dataset/images/valid to verify the validation path.

Help me debug my dataset.yaml paths for YOLOv8 training.

Visualize sample images with their annotations

Seeing the bounding boxes drawn on actual images is the best way to confirm your labels are correct. You will write a short script that loads a training image and draws its annotation boxes using OpenCV.

  • Create a new file called visualize.py in your mask-detector folder by right-clicking in the Explorer sidebar and selecting New File.
  • Add the following code to visualize.py:
import cv2
import os

# Path to training images and labels
images_dir = "face-mask-dataset/images/train"
labels_dir = "face-mask-dataset/labels/train"

# Class names and colors (green for Mask, red for No Mask)
class_names = ["Mask", "No Mask"]
class_colors = [(0, 255, 0), (0, 0, 255)]

# Get a sample image filename
image_files = os.listdir(images_dir)
sample_image = image_files[0]

# Load the image
img_path = os.path.join(images_dir, sample_image)
img = cv2.imread(img_path)
height, width = img.shape[:2]

# Load corresponding label file
label_file = os.path.splitext(sample_image)[0] + ".txt"
label_path = os.path.join(labels_dir, label_file)

# Draw bounding boxes from YOLO annotations
with open(label_path, "r") as f:
    for line in f.readlines():
        parts = line.strip().split()
        class_id = int(parts[0])
        x_center = float(parts[1]) * width
        y_center = float(parts[2]) * height
        box_width = float(parts[3]) * width
        box_height = float(parts[4]) * height

        # Convert from center format to corner format
        x1 = int(x_center - box_width / 2)
        y1 = int(y_center - box_height / 2)
        x2 = int(x_center + box_width / 2)
        y2 = int(y_center + box_height / 2)

        # Draw rectangle and class label
        color = class_colors[class_id]
        cv2.rectangle(img, (x1, y1), (x2, y2), color, 2)
        cv2.putText(img, class_names[class_id], (x1, y1 - 10),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)

# Display the annotated image
cv2.imshow("Sample Annotation", img)
print(f"Showing: {sample_image} with annotations from {label_file}")
print("Press any key to close the window.")
cv2.waitKey(0)
cv2.destroyAllWindows()

What does this script do?

The script loads one training image and reads its matching .txt label file. For each line in the label file, it converts the normalized YOLO coordinates back to pixel values and draws a colored rectangle on the image.

Green boxes represent class 0 (Mask) and red boxes represent class 1 (No Mask). This color scheme matches what your final webcam detector will use.

  • Save visualize.py.
  • Run the visualization script by running this command:
python visualize.py

A window opens showing an image with colored bounding boxes drawn around faces. Press any key to close the window.

Window not appearing?

If you see an error about NoneType or the image not loading, check that your terminal's working directory is mask-detector (not a subfolder). Run cd ~/Desktop/mask-detector to return to the project root and try again.

If the window appears but has no bounding boxes, the label file may be empty for that particular image. Try changing image_files[0] to image_files[1] or image_files[2] to pick a different sample.

Help me fix my visualization script that is not showing bounding boxes on face mask images.

✔️ Awesome, I've got everything!

Great. Double check you have saved dataset.yaml and visualize.py.

ⓧ I'd like to double check the full code

Here is the full dataset.yaml you should have:

# Paths relative to this yaml file's location
train: images/train
val: images/valid

# Number of classes
nc: 2

# Class names (class 0 = Mask, class 1 = No Mask)
names: ['Mask', 'No Mask']

Here is the full visualize.py you should have:

import cv2
import os

# Path to training images and labels
images_dir = "face-mask-dataset/images/train"
labels_dir = "face-mask-dataset/labels/train"

# Class names and colors (green for Mask, red for No Mask)
class_names = ["Mask", "No Mask"]
class_colors = [(0, 255, 0), (0, 0, 255)]

# Get a sample image filename
image_files = os.listdir(images_dir)
sample_image = image_files[0]

# Load the image
img_path = os.path.join(images_dir, sample_image)
img = cv2.imread(img_path)
height, width = img.shape[:2]

# Load corresponding label file
label_file = os.path.splitext(sample_image)[0] + ".txt"
label_path = os.path.join(labels_dir, label_file)

# Draw bounding boxes from YOLO annotations
with open(label_path, "r") as f:
    for line in f.readlines():
        parts = line.strip().split()
        class_id = int(parts[0])
        x_center = float(parts[1]) * width
        y_center = float(parts[2]) * height
        box_width = float(parts[3]) * width
        box_height = float(parts[4]) * height

        # Convert from center format to corner format
        x1 = int(x_center - box_width / 2)
        y1 = int(y_center - box_height / 2)
        x2 = int(x_center + box_width / 2)
        y2 = int(y_center + box_height / 2)

        # Draw rectangle and class label
        color = class_colors[class_id]
        cv2.rectangle(img, (x1, y1), (x2, y2), color, 2)
        cv2.putText(img, class_names[class_id], (x1, y1 - 10),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)

# Display the annotated image
cv2.imshow("Sample Annotation", img)
print(f"Showing: {sample_image} with annotations from {label_file}")
print("Press any key to close the window.")
cv2.waitKey(0)
cv2.destroyAllWindows()

Your dataset is verified and ready to train. Next up, you will load a pretrained YOLOv8 model and train it on these 2,500+ images using your GPU.

Train the YOLOv8 Model on GPU

Your dataset is prepared and verified. Now it is time for the most exciting part of any machine learning project: training the model.

You will fine-tune a pretrained YOLOv8 model on your face mask images using transfer learning. Instead of teaching a model to see from scratch, you start with one that already understands shapes, edges, and patterns from millions of general images. Your job is to teach it the specific difference between a masked face and an unmasked face.

In this step, get ready to:
  • Write a training script that loads a pretrained model and trains it on your dataset.
  • Run GPU-accelerated training for 50 epochs and monitor progress.
  • Review training results including metrics and generated plots.
Load the pretrained model and configure training

The YOLOv8 "small" model (yolov8s.pt) comes pretrained on the COCO dataset with knowledge of general object detection. Your RTX 2070 Super has enough VRAM to train the small variant comfortably, giving you significantly better accuracy than the nano model.

  • In the Cursor terminal, make sure you are in the mask-detector folder with your virtual environment activated.
  • Create a new file called train.py and add the following training code:
from ultralytics import YOLO

# Load pretrained YOLOv8 small model
model = YOLO("yolov8s.pt")

# Train on the face mask dataset with GPU
results = model.train(
    data="face-mask-dataset/dataset.yaml",
    epochs=50,
    imgsz=640,
    batch=16,
    device=0,
    name="mask_detector"
)

What does this code do?

YOLO("yolov8s.pt") downloads and loads a pretrained small model. Ultralytics handles the download automatically on first run.

The model.train() call configures and launches training with these key parameters:

  • data points to the dataset config you verified in the last step.
  • epochs=50 means the model sees every training image 50 times.
  • imgsz=640 resizes images to 640x640 pixels for consistent input.
  • batch=16 processes 16 images at a time (fits well within 8 GB VRAM).
  • device=0 tells PyTorch to use your first GPU (the RTX 2070 Super).
  • name="mask_detector" sets the output folder name for results.
Add model evaluation after training

After training finishes, you want the script to automatically evaluate how well the model performs on the validation set. This gives you concrete accuracy numbers.

  • Add the following evaluation code to the bottom of train.py, directly below the training block:
# Evaluate the model
metrics = model.val()
print(f"mAP50: {metrics.box.map50:.3f}")
print(f"mAP50-95: {metrics.box.map:.3f}")

What does this code do?

model.val() runs the trained model against your validation images and calculates detection accuracy metrics.

  • mAP50 (mean Average Precision at 50% IoU) measures how accurately the model detects objects when the predicted box overlaps at least 50% with the true box.
  • mAP50-95 averages accuracy across stricter overlap thresholds (50% to 95%), giving a tougher overall score.
  • Save train.py.

Seeing a red underline on the import?

If Cursor shows an error under from ultralytics import YOLO, make sure your virtual environment is activated and selected as the Python interpreter. Press Ctrl+Shift+P, type Python: Select Interpreter, and choose the interpreter from your venv folder.

help me fix my Python interpreter in Cursor

✔️ Awesome, I've got everything!

Great. Double check you have saved train.py before running it in the next substep.

ⓧ I'd like to double check the full code

from ultralytics import YOLO

# Load pretrained YOLOv8 small model
model = YOLO("yolov8s.pt")

# Train on the face mask dataset with GPU
results = model.train(
    data="face-mask-dataset/dataset.yaml",
    epochs=50,
    imgsz=640,
    batch=16,
    device=0,
    name="mask_detector"
)

# Evaluate the model
metrics = model.val()
print(f"mAP50: {metrics.box.map50:.3f}")
print(f"mAP50-95: {metrics.box.map:.3f}")
Launch training and monitor progress

With your script ready, it is time to kick off training. The RTX 2070 Super will process all 2,500+ images across 50 epochs in approximately 5-10 minutes.

  • Run the training script by executing this command:
python train.py

What should I see during training?

On the first run, Ultralytics will automatically download the yolov8s.pt pretrained weights. This takes a few seconds.

Once training begins, you will see a progress bar for each epoch showing:

  • Epoch number (e.g., 1/50, 2/50).
  • GPU memory usage.
  • Loss values (box_loss, cls_loss, dfl_loss) that should decrease over time.
  • Validation metrics (mAP50, mAP50-95, precision, recall) reported after each epoch.

Decreasing loss values mean the model is learning. Increasing mAP values mean the model is getting better at detecting masks correctly.

Wait for training to complete. After all 50 epochs finish, the evaluation metrics will print at the bottom of the terminal output.

Training seems stuck or very slow?

If training is taking much longer than expected, make sure device=0 is set in your script. Without it, training falls back to CPU which is dramatically slower.

If you see a CUDA out-of-memory error, reduce batch=16 to batch=8 in your train.py file and run again.

help me debug my YOLOv8 GPU training issue

Review training results

After training and evaluation complete, Ultralytics saves a full set of results to a dedicated output folder. These files show you exactly how well your model learned.

  • In the Cursor file explorer, navigate to runs/detect/mask_detector/.
  • Open the confusion_matrix.png image. This shows how often the model correctly classified Mask vs No Mask detections.
  • Open the results.png image. This plots loss curves and mAP over all 50 epochs.
  • Confirm that runs/detect/mask_detector/weights/best.pt exists. This is your trained model file.

What is best.pt?

Ultralytics automatically saves the model weights from the epoch that achieved the highest mAP score. This is best.pt. It also saves last.pt from the final epoch. You will use best.pt for all detection tasks going forward because it represents your model at peak performance.

What is a confusion matrix?

A confusion matrix is a grid that shows the model's predictions versus the actual labels. The diagonal cells (top-left, bottom-right) represent correct predictions. Off-diagonal cells show mistakes. A strong model has high numbers on the diagonal and low numbers everywhere else.

Don't see the runs folder?

The runs/ folder is created inside whatever directory you ran python train.py from. Make sure you ran the script while inside the mask-detector folder.

If training was interrupted before finishing, no best.pt will be saved. Re-run python train.py and let it complete all 50 epochs.

help me find my YOLOv8 training output files

Your model is trained and evaluated. Next up, you will test it on individual images to see the bounding boxes and confidence scores in action.

Evaluate and Test the Model

Your model has completed 50 epochs of training on 2,500+ face mask images. Training metrics look promising, but the real test is how well the model performs on images it has never seen before.

In this step, you will run formal validation to measure your model's accuracy, then test it on individual images to see real detections with bounding boxes and confidence scores.

In this step, get ready to:
  • Review validation metrics (mAP50, precision, recall) from training.
  • Run the model on test images and inspect detection results.
Run validation on the trained model

When train.py finished, it ran model.val() and printed two key metrics to your terminal. Scroll up in your terminal to find the validation output.

  • Locate the validation summary table in your terminal output from the previous step.
  • Find the row labeled all which shows the combined metrics across both classes.
  • Confirm your mAP50 value is above 0.7.

What do these metrics mean?

mAP50 (mean Average Precision at 50% IoU) measures how well your model's bounding boxes overlap with the real labels. A score above 0.7 means your model correctly locates and classifies most faces.

mAP50-95 averages precision across stricter overlap thresholds. This number is always lower than mAP50 and rewards very tight bounding boxes.

Precision tells you how many of the model's detections were correct. Recall tells you how many real objects the model actually found.

  • Open the runs/detect/mask_detector/ folder in the Cursor file explorer sidebar.
  • Open confusion_matrix.png to see how the model performed on each class.
  • Open results.png to see training loss curves and metric trends across all 50 epochs.

How to read the confusion matrix

The diagonal cells show correct predictions. High numbers on the diagonal mean the model reliably tells "Mask" apart from "No Mask". Off-diagonal cells show misclassifications.

mAP50 below 0.7?

If your mAP50 is significantly below 0.7, the most common cause is incorrect paths in dataset.yaml. Go back to Step 2 and verify that the train, val, and test paths point to existing folders with images.

Help me debug my low mAP50 score for YOLOv8 face mask detection training

Test the model on individual images

Validation gives you aggregate metrics, but you also want to see the model in action on specific images. You will create a script that runs predictions on the test split and prints each detection.

  • Create a new file called test_images.py in your mask-detector folder.
  • Add the following code to load your trained model and run predictions on the test images:
from ultralytics import YOLO

# Load trained model
model = YOLO("runs/detect/mask_detector/weights/best.pt")

# Run prediction on test images
results = model("face-mask-dataset/test/images", save=True, conf=0.5)

What does this code do?

YOLO("runs/detect/mask_detector/weights/best.pt") loads the best weights your training produced. These are the weights from the epoch that had the highest validation accuracy.

The prediction call passes the entire test images folder. save=True tells Ultralytics to save annotated copies of each image with bounding boxes drawn. conf=0.5 filters out any detections below 50% confidence.

  • Below the prediction call, add the code to iterate through results and print each detection:
# Print detection results
for result in results:
    for box in result.boxes:
        class_id = int(box.cls.item())
        confidence = box.conf.item()
        class_name = result.names[class_id]
        print(f"Detected: {class_name} ({confidence:.2f})")

What does this code do?

Each result corresponds to one image. Each box in result.boxes is a single detected face.

box.cls holds the class ID (0 for Mask, 1 for No Mask). box.conf holds the confidence score. result.names maps IDs to human-readable class names.

  • Save test_images.py.

✔️ Awesome, I've got everything!

Great. Double check you have saved test_images.py before running it.

ⓧ I'd like to double check the full code

from ultralytics import YOLO

# Load trained model
model = YOLO("runs/detect/mask_detector/weights/best.pt")

# Run prediction on test images
results = model("face-mask-dataset/test/images", save=True, conf=0.5)

# Print detection results
for result in results:
    for box in result.boxes:
        class_id = int(box.cls.item())
        confidence = box.conf.item()
        class_name = result.names[class_id]
        print(f"Detected: {class_name} ({confidence:.2f})")
  • Run the script in your terminal:
python test_images.py
  • Watch the terminal output. You should see lines like Detected: Mask (0.87) and Detected: No Mask (0.72) printed for each face found in the test images.
  • Open the runs/detect/predict/ folder in Cursor's file explorer. You should see annotated copies of the test images with colored bounding boxes drawn around detected faces.

Where did the annotated images go?

Ultralytics automatically saves annotated images to runs/detect/predict/ when you pass save=True. Each image gets bounding boxes, class labels, and confidence scores drawn directly onto it.

No detections printed?

If you see zero detections, check that the path face-mask-dataset/test/images exists and contains .jpg or .png files. Also confirm that runs/detect/mask_detector/weights/best.pt exists from your training run.

Help me troubleshoot why my YOLOv8 model detects nothing on test images

Your model is trained, validated, and tested. Next up, you will bring it to life with a real-time webcam detector that draws colored bounding boxes around faces as you move.

Build the Real-Time Webcam Detector

Your model is trained, evaluated, and proven accurate on test images. Now it is time to deploy it where it matters most: running live on your webcam feed.

In this step, you will build a Python script that captures video from your webcam, runs your trained model on every frame using GPU acceleration, and draws colored bounding boxes around detected faces in real time.

In this step, get ready to:
  • Write a webcam detection script that loads your model and opens a video feed.
  • Add a real-time FPS counter and a keyboard shortcut to quit.
  • Test the live detector with and without a mask.
Write the webcam detection script

The detector script needs to do three things: load your trained model, open a webcam feed with OpenCV, and run inference on each frame. You will define a color map so "Mask" detections appear in green and "No Mask" detections appear in red.

  • Create a new file called detect_webcam.py in your mask-detector folder.
  • Add the following setup code to load the model, define detection colors, and open your webcam:
import cv2
import time
from ultralytics import YOLO

# Load the trained model
model = YOLO("runs/detect/mask_detector/weights/best.pt")

# Define colors for each class
COLORS = {
    "Mask": (0, 255, 0),      # Green
    "No Mask": (0, 0, 255)    # Red
}

# Open webcam
cap = cv2.VideoCapture(0)

if not cap.isOpened():
    print("Error: Could not open webcam")
    exit()

print("Face Mask Detector running. Press 'q' to quit.")

prev_time = time.time()

What does this code do?

  • YOLO("runs/detect/mask_detector/weights/best.pt") loads your trained model weights from the training output folder.
  • COLORS maps each class name to a BGR color tuple. OpenCV uses BGR order, so (0, 255, 0) is green and (0, 0, 255) is red.
  • cv2.VideoCapture(0) opens the default webcam. The error check ensures the script exits gracefully if no webcam is available.
  • prev_time stores the timestamp of the last frame, used later to calculate FPS.

You will not see a change yet. The next chunk adds the detection loop that processes each video frame.

Now you need the main loop that reads frames from the webcam, runs your model on each one, and draws colored bounding boxes with labels. This is the same detection logic you used in test_images.py, but applied to live video frames.

  • Add the detection loop below your setup code in detect_webcam.py:
while True:
    ret, frame = cap.read()
    if not ret:
        print("Error: Could not read frame")
        break

    # Run detection on GPU
    results = model(frame, conf=0.5, verbose=False)

    # Draw bounding boxes
    for result in results:
        for box in result.boxes:
            # Get box coordinates
            x1, y1, x2, y2 = map(int, box.xyxy[0].tolist())
            confidence = box.conf.item()
            class_id = int(box.cls.item())
            class_name = result.names[class_id]

            # Get color based on class
            color = COLORS.get(class_name, (255, 255, 255))

            # Draw bounding box
            cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2)

            # Draw label background
            label = f"{class_name} {confidence:.2f}"
            label_size = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2)[0]
            cv2.rectangle(frame, (x1, y1 - label_size[1] - 10), (x1 + label_size[0], y1), color, -1)

            # Draw label text
            cv2.putText(frame, label, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2)

What does this code do?

  • cap.read() captures a single frame from the webcam. If the read fails, the loop breaks gracefully.
  • model(frame, conf=0.5, verbose=False) runs inference on the frame using your GPU. The conf=0.5 threshold filters out weak detections. Setting verbose=False suppresses per-frame log output.
  • For each detection, the code extracts the bounding box coordinates, class name, and confidence score. It then draws a colored rectangle and a filled label background with white text above the box.
  • COLORS.get(class_name, (255, 255, 255)) returns green for "Mask" and red for "No Mask". The white fallback handles any unexpected class names.

You will not see a change yet. The next chunk adds the FPS display and quit mechanism that completes the script.

Add real-time display with FPS counter

Monitoring frames per second tells you whether your GPU is keeping up with real-time inference. You also need a way to display the processed frames and quit the application cleanly.

  • Add the following code at the end of your while True loop (after the bounding box drawing section, still inside the loop), followed by the cleanup code outside the loop:
    # Calculate and display FPS
    current_time = time.time()
    fps = 1 / (current_time - prev_time)
    prev_time = current_time
    cv2.putText(frame, f"FPS: {fps:.1f}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2)

    # Show frame
    cv2.imshow("Face Mask Detector", frame)

    # Quit on 'q' key press
    if cv2.waitKey(1) & 0xFF == ord("q"):
        break

cap.release()
cv2.destroyAllWindows()
print("Detector stopped.")

What does this code do?

  • The FPS calculation divides 1 by the time elapsed since the previous frame. This gives you the instantaneous frame rate, displayed in yellow text at the top-left corner.
  • cv2.imshow() opens a window showing the processed frame with all bounding boxes and labels drawn on it.
  • cv2.waitKey(1) waits 1 millisecond for a keypress. If the learner presses 'q', the loop breaks.
  • cap.release() and cv2.destroyAllWindows() free the webcam and close all OpenCV windows when the loop ends.
  • Save detect_webcam.py.

Seeing a syntax error when you save?

  • Make sure the FPS and display code is indented at the same level as the results = model(frame, ...) line (one level inside the while True loop).
  • The cap.release() and cv2.destroyAllWindows() lines must be outside the loop, at the top indentation level.
  • Check for mismatched quotes in the f-string f"FPS: {fps:.1f}".

Why is my detect_webcam.py showing a syntax error?

✔️ Awesome, I've got everything!

Great. Double check you have saved detect_webcam.py before running it in the next substep.

ⓧ I'd like to double check the full code

Here is the complete detect_webcam.py file for comparison:

import cv2
import time
from ultralytics import YOLO

# Load the trained model
model = YOLO("runs/detect/mask_detector/weights/best.pt")

# Define colors for each class
COLORS = {
    "Mask": (0, 255, 0),      # Green
    "No Mask": (0, 0, 255)    # Red
}

# Open webcam
cap = cv2.VideoCapture(0)

if not cap.isOpened():
    print("Error: Could not open webcam")
    exit()

print("Face Mask Detector running. Press 'q' to quit.")

prev_time = time.time()

while True:
    ret, frame = cap.read()
    if not ret:
        print("Error: Could not read frame")
        break

    # Run detection on GPU
    results = model(frame, conf=0.5, verbose=False)

    # Draw bounding boxes
    for result in results:
        for box in result.boxes:
            # Get box coordinates
            x1, y1, x2, y2 = map(int, box.xyxy[0].tolist())
            confidence = box.conf.item()
            class_id = int(box.cls.item())
            class_name = result.names[class_id]

            # Get color based on class
            color = COLORS.get(class_name, (255, 255, 255))

            # Draw bounding box
            cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2)

            # Draw label background
            label = f"{class_name} {confidence:.2f}"
            label_size = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2)[0]
            cv2.rectangle(frame, (x1, y1 - label_size[1] - 10), (x1 + label_size[0], y1), color, -1)

            # Draw label text
            cv2.putText(frame, label, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2)

    # Calculate and display FPS
    current_time = time.time()
    fps = 1 / (current_time - prev_time)
    prev_time = current_time
    cv2.putText(frame, f"FPS: {fps:.1f}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2)

    # Show frame
    cv2.imshow("Face Mask Detector", frame)

    # Quit on 'q' key press
    if cv2.waitKey(1) & 0xFF == ord("q"):
        break

cap.release()
cv2.destroyAllWindows()
print("Detector stopped.")
Test the live detector

Your script is complete. Time to see your trained model in action on a live webcam feed.

  • Run the detector by executing this command in your terminal:
python detect_webcam.py

A window titled "Face Mask Detector" opens showing your webcam feed. You should see an FPS counter in yellow text at the top-left corner.

  • Look directly at your webcam without a mask. You should see a red bounding box appear around your face with a "No Mask" label and a confidence score.
  • Put on a face mask and look at the webcam again. The bounding box should turn green with a "Mask" label.
  • Check the FPS counter. With your RTX 2070 Super, you should see 30+ FPS.
  • Press q on your keyboard to quit. Your terminal should print "Detector stopped." and the window closes.

What should I see?

Green boxes with "Mask" labels appear around faces wearing masks. Red boxes with "No Mask" labels appear around unmasked faces. Each label includes a confidence score (e.g., 0.87). The FPS counter in the top-left corner should stay above 30 with GPU inference.

Webcam window not opening?

  • If you see "Error: Could not open webcam", make sure no other application (Zoom, Teams, another browser tab) is using your webcam. Close those apps and try again.
  • If the window opens but shows no detections, try moving closer to the camera or improving your lighting. The model needs a clear view of your face.
  • If FPS is low (under 10), verify your GPU is being used by checking that torch.cuda.is_available() returns True in a Python shell.

Why isn't my webcam detector working?

Your real-time face mask detector is live. You have gone from raw training data to a working application that draws colored bounding boxes on your webcam feed at 30+ FPS. Next up, the Secret Mission challenges you to add an alert system that sounds an alarm when it spots an unmasked face.

Secret mission

Add an Alert System

Your detector spots unmasked faces, but what if you need it to actively warn people? Build an alert system that plays a sound, saves a timestamped screenshot, and displays a visual warning whenever an unmasked face is detected.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.

Resources you used:

  • mask-detector project folder (virtual environment, Python scripts, cloned dataset, trained model weights, and alert screenshots).

Keep everything running

No action needed. Choose this if you're still actively building or experimenting with your mask detector.

  • Your virtual environment, trained model, dataset, and scripts all stay in the mask-detector folder on your Desktop.
  • To pick up where you left off, open Cursor, navigate to the mask-detector folder, and activate the virtual environment again.

Pause - I'll come back to this later

Shut down running processes to free up memory. Your files stay on disk for next time.

  • Make sure the webcam detector script is stopped (press q in the OpenCV window if it's still running).
  • Deactivate the virtual environment by running:
deactivate
  • Close Cursor. Your project files remain on the Desktop and you can return any time.

Delete - I don't want to use this again

Remove all project resources and start fresh. This permanently deletes your trained model, dataset, scripts, and alert screenshots.

  • Make sure the webcam detector script is stopped (press q in the OpenCV window if it's still running).
  • Deactivate the virtual environment by running:
deactivate
  • Navigate to your Desktop by running:
cd ~/Desktop
  • Delete the entire project folder. On macOS/Linux, run:
rm -rf mask-detector
  • On Windows (Command Prompt), run:
rd /s /q mask-detector
  • Confirm the folder is gone by running:
ls
  • You should no longer see mask-detector in the output.

Nice Work!

Nice Work!

You just built a real-time face mask detection system from scratch. From raw images to a live webcam application, you have experienced the full machine learning workflow that powers real-world computer vision products.

Here's what you accomplished:

  • Trained a custom YOLOv8 object detection model on 2,500+ images using GPU-accelerated transfer learning, evaluating performance with metrics like mAP, precision, and recall.
  • Built a real-time webcam detector that draws colored bounding boxes around faces at 30+ FPS, distinguishing masked from unmasked faces with confidence scores.
  • Mastered the end-to-end ML pipeline: dataset preparation, model training, evaluation, and deployment to a live application.
  • Secret Mission: Added an intelligent alert system with sound warnings, timestamped screenshots, cooldown logic, and visual overlays.

Ready to quiz yourself?