AI-Powered PCB Defect Detection

Train a YOLOv8 model to detect 6 PCB defect types and deploy it as a web app.

Introduction

30 Second Summary

Every electronic device you use has a printed circuit board inside. Before those boards leave the factory, inspectors check each one for tiny defects that could cause failures.

In this project, you will train a YOLOv8 object detection model to automatically identify PCB defects, build an inspection report generator, deploy it as a local web app, and package everything as a professional GitHub portfolio project.

What You'll Build

A complete AI-powered PCB defect detection pipeline that uses YOLOv8 to identify 6 common defect types in circuit board images, generate inspection reports, and serve results through a Gradio web interface.

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

  • A trained YOLOv8s model that detects opens, shorts, mousebites, spurs, spurious copper, and pinholes with bounding boxes and confidence scores.
  • An automated inspection report generator that outputs PASS/FAIL dispositions based on configurable acceptance criteria, mimicking industry First Article Inspection Reports.
  • A Gradio web interface for real-time PCB image analysis and a professional GitHub repository with README, documentation, sample data, and reproducible setup instructions.
  • Secret Mission: Add defect severity classification and export professional PDF inspection reports.

Are there any prerequisites?

You should have basic Python knowledge and some familiarity with PCB or electronics manufacturing concepts. No prior machine learning experience is required.

Before We Start

Before writing any detection code, you need a clean, professional project structure and the right machine learning packages installed.

A well-organized repository is what separates a portfolio project from a random script. In this step, you'll create the folder structure, set up a Python virtual environment, install the required packages, and verify that YOLOv8 loads correctly.

In this step, get ready to:
  • Create the project folder structure with directories for source code, documentation, and sample images.
  • Initialize git, create a virtual environment, and install the required packages.
  • Create project configuration files and verify YOLOv8 works.
Create the project folder structure

Every GitHub portfolio project needs a consistent directory layout so that recruiters and collaborators can navigate it immediately. You'll create folders for source code, documentation, configuration files, and sample images.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir pcb-defect-detection
cd pcb-defect-detection
  • Create the subdirectory structure by running this command:
mkdir -p src docs samples/results configs
  • Confirm the folders exist by running:
ls

You should see configs, docs, samples, and src listed in the output.

What are these folders for?

  • src/ holds all Python source code (training, inference, web app).
  • docs/ holds documentation like the methodology writeup.
  • samples/results/ stores example PCB images and detection results for the README.
  • configs/ holds dataset and training configuration YAML files.

Don't see the folders?

Make sure you ran cd pcb-defect-detection before running mkdir. If you're still in ~/Desktop, the folders were created inside the project directory but ls is showing Desktop contents instead.

help me create my project folder structure for pcb-defect-detection

Initialize git and install packages

Version control and an isolated Python environment are essential for reproducibility. A virtual environment keeps your project's packages separate from your system Python.

  • Initialize a git repository by running this command:
git init

You should see a message like Initialized empty Git repository confirming git is ready.

  • Create a virtual environment by running:
python3 -m venv venv
  • Activate the virtual environment by running:
source venv/bin/activate

Your terminal prompt should now show (venv) at the beginning, confirming the virtual environment is active.

What is a virtual environment?

A virtual environment is an isolated Python installation inside your project folder. Packages you install here won't affect your system Python, and other projects won't interfere with this one.

This isolation is critical for a portfolio project because it means anyone can clone your repo, create a fresh venv, and install from requirements.txt to get the exact same setup.

  • Install the required packages by running:
pip install ultralytics gradio opencv-python Pillow numpy

What are these packages?

  • ultralytics provides the YOLOv8 model and training framework.
  • gradio builds the interactive web interface for uploading PCB images.
  • opencv-python handles image processing operations.
  • Pillow and numpy support image manipulation and numerical operations.

The installation will download several packages and their dependencies. This may take a minute or two depending on your internet speed.

Seeing a pip error?

Make sure your virtual environment is active (you should see (venv) in your prompt). If you see a permission error, you likely forgot to activate the venv. Run source venv/bin/activate and try the install command again.

help me fix my pip install error for ultralytics and gradio packages

Create project files and verify YOLOv8

Now you'll create the two essential project files that make the repository reproducible. Then you'll run a quick test to confirm the Ultralytics package loaded correctly.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type PyCharm and press Enter to open it.
  • Click Open on the PyCharm welcome screen.
  • Navigate to Desktop, select the pcb-defect-detection folder, and click Open.

Why open the folder in PyCharm?

Opening the project folder directly tells PyCharm to treat it as your project root. This means the file tree, terminal, and run configurations all start from the right place.

  • Right-click the pcb-defect-detection root folder in PyCharm's Project panel on the left.
  • Select New, then File.
  • Name the file .gitignore and press Enter.
  • Paste the following content into the file:
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
venv/
.env

# IDE
.idea/
.vscode/
*.swp

# Dataset (too large for git)
DeepPCB/
pcb_dataset/

# Model weights (too large for git)
pcb_defect_detection/
*.pt

# OS files
.DS_Store
Thumbs.db

# Temporary files
*.tmp
inspection_report.json
inspection_result.jpg

What does this .gitignore do?

This file tells Git which files to exclude from version control. The dataset (hundreds of images) and model weights (large binary files) would bloat the repository, so they stay local.

The venv/ and .idea/ folders are machine-specific and should never be committed.

  • Right-click the pcb-defect-detection root folder again in the Project panel.
  • Select New, then File.
  • Name the file requirements.txt and press Enter.
  • Paste the following content into the file:
ultralytics>=8.4.0
gradio>=5.0.0
opencv-python>=4.8.0
Pillow>=10.0.0
numpy>=1.24.0

What is requirements.txt for?

This file documents the exact packages your project needs. Anyone who clones your repo can run pip install -r requirements.txt to install everything in one command.

  • Save both files.
  • Open PyCharm's built-in terminal by pressing Option+F12 (macOS) or Alt+F12 (Windows).

PyCharm should automatically detect and activate your virtual environment. Look for (venv) at the beginning of the terminal prompt.

  • If you do not see (venv) in the prompt, activate it manually by running:
source venv/bin/activate
  • Verify that YOLOv8 is installed and working by running:
python3 -c "from ultralytics import YOLO; model = YOLO('yolov8s.pt'); print('YOLOv8 loaded successfully!')"

This downloads the yolov8s.pt pretrained model weights (about 22 MB) and loads it into memory. You should see YOLOv8 loaded successfully! printed in the terminal.

Seeing a ModuleNotFoundError?

This means the packages aren't installed in the active environment. Confirm (venv) appears in your prompt. If it does not, run source venv/bin/activate first, then re-run the pip install command from the previous substep.

help me fix ModuleNotFoundError when importing ultralytics in my pcb-defect-detection project

✔️ Awesome, I've got everything!

Great. Double check you've saved both files, then move on.

ⓧ I'd like to double check the full code

Here is the complete content of both files for comparison:

# Python
__pycache__/
*.py[cod]
*$py.class
*.so
venv/
.env

# IDE
.idea/
.vscode/
*.swp

# Dataset (too large for git)
DeepPCB/
pcb_dataset/

# Model weights (too large for git)
pcb_defect_detection/
*.pt

# OS files
.DS_Store
Thumbs.db

# Temporary files
*.tmp
inspection_report.json
inspection_result.jpg
ultralytics>=8.4.0
gradio>=5.0.0
opencv-python>=4.8.0
Pillow>=10.0.0
numpy>=1.24.0

Your project structure is set up, packages are installed, and YOLOv8 is confirmed working. Next up, you'll download the DeepPCB dataset and convert its annotations into the format YOLOv8 needs for training.

Set Up Project Structure and Environment

Before you can train an AI model to detect PCB defects, you need a well-organized project that can live on GitHub. A clean folder structure makes your project reproducible and portfolio-ready from day one.

In this step, you will create the project skeleton, set up a Python virtual environment, install the machine learning packages, and confirm everything works with a quick test.

In this step, get ready to:
  • Create a GitHub-ready folder structure with dedicated directories for source code, documentation, and samples.
  • Initialize git, create a virtual environment, and install the required packages.
  • Create project configuration files and verify YOLOv8 works with a quick inference test.
Create the project folder structure

A portfolio project needs a professional layout. Recruiters and hiring managers judge a repo by its structure before reading any code.

  • In PyCharm, open the terminal by pressing Alt+F12 (macOS and Windows).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir pcb-defect-detection
cd pcb-defect-detection
  • Create the subdirectories for source code, documentation, sample images, and configuration by running this command:
mkdir -p src docs samples/results configs
  • Confirm the directories exist by running:
ls

You should see configs, docs, samples, and src listed in the output.

What does each folder do?

  • src/ holds all Python scripts (training, inference, web app).
  • docs/ stores methodology documentation and architecture notes.
  • samples/results/ contains example PCB images and detection outputs for your README.
  • configs/ holds dataset configuration YAML files for training.
Initialize git and install packages

Version control from the start means every change is tracked. The virtual environment keeps your project's dependencies isolated from your system Python.

  • Initialize a git repository in your project folder by running:
git init

You should see a message saying the repository has been initialized.

  • Create and activate a virtual environment by running these commands:
python3 -m venv venv
source venv/bin/activate

Your terminal prompt should now show (venv) at the beginning, confirming the virtual environment is active.

What is a virtual environment?

A virtual environment is an isolated Python installation inside your project folder. Packages you install here stay separate from your system Python. This prevents version conflicts between projects.

  • Install the required machine learning and web framework packages by running:
pip install ultralytics>=8.4.0 gradio>=5.0.0 opencv-python>=4.8.0 Pillow>=10.0.0 numpy>=1.24.0

What are these packages?

  • ultralytics provides the YOLOv8 object detection framework you will use to detect PCB defects.
  • gradio is a web UI library for building machine learning demos with minimal code.
  • opencv-python handles image processing and computer vision operations.
  • Pillow and numpy provide image manipulation and numerical computation support.

Installation taking a long time?

The ultralytics package downloads PyTorch and other large dependencies. This can take 2-5 minutes on a typical connection. If you see timeout errors, try running the install command again.

Help me troubleshoot my pip installation

Create configuration files and verify YOLOv8

Your project needs a .gitignore to keep large files out of version control and a requirements.txt so anyone can reproduce your environment.

  • Create the .gitignore file by running:
cat > .gitignore << 'EOF'
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
venv/
.env

# IDE
.idea/
.vscode/
*.swp

# Dataset (too large for git)
DeepPCB/
pcb_dataset/

# Model weights (too large for git)
pcb_defect_detection/
*.pt

# OS files
.DS_Store
Thumbs.db

# Temporary files
*.tmp
inspection_report.json
inspection_result.jpg
EOF

Why exclude these files?

The DeepPCB dataset and trained model weights are too large for GitHub (hundreds of megabytes). The .gitignore ensures only your source code and documentation get committed.

The venv/ directory is excluded because anyone can recreate it from requirements.txt.

  • Create the requirements.txt file by running:
cat > requirements.txt << 'EOF'
ultralytics>=8.4.0
gradio>=5.0.0
opencv-python>=4.8.0
Pillow>=10.0.0
numpy>=1.24.0
EOF
  • Verify that ultralytics is installed correctly by running a quick inference test:
python3 -c "from ultralytics import YOLO; model = YOLO('yolov8s.pt'); print('YOLOv8 loaded successfully!')"

You should see YOLOv8 loaded successfully! printed in your terminal. This confirms the package is working and downloads the pretrained weights.

Seeing an import error?

Make sure your virtual environment is active. Look for (venv) at the start of your terminal prompt. If it is missing, run source venv/bin/activate again.

Help me fix this import error

  • Make your initial git commit to save this project skeleton by running:
git add .gitignore requirements.txt
git commit -m "Initial commit: project structure and requirements"

You should see a commit confirmation message listing the two files added.

✔️ Awesome, I've got everything!

Great. Double check you have (venv) in your prompt, the folders visible in PyCharm's project panel, and the YOLOv8 test passed.

ⓧ I'd like to double check the full code

Here is what your .gitignore should contain:

# Python
__pycache__/
*.py[cod]
*$py.class
*.so
venv/
.env

# IDE
.idea/
.vscode/
*.swp

# Dataset (too large for git)
DeepPCB/
pcb_dataset/

# Model weights (too large for git)
pcb_defect_detection/
*.pt

# OS files
.DS_Store
Thumbs.db

# Temporary files
*.tmp
inspection_report.json
inspection_result.jpg

Here is what your requirements.txt should contain:

ultralytics>=8.4.0
gradio>=5.0.0
opencv-python>=4.8.0
Pillow>=10.0.0
numpy>=1.24.0

Your project skeleton is ready and version-controlled. Next up, you will download the DeepPCB dataset and convert its annotations into the format YOLOv8 needs for training.

Prepare the DeepPCB Dataset

Your environment is set up and verified. Now you need training data.

Object detection models like YOLOv8 learn by studying thousands of labeled examples. The DeepPCB dataset gives us exactly that: 1,500 PCB images with bounding boxes drawn around 6 types of defects. The catch is that DeepPCB stores annotations in its own format, and YOLO expects something different. You will write a conversion script to bridge the gap.

In this step, get ready to:
  • Clone the DeepPCB dataset and explore its annotation format.
  • Write and run a conversion script that transforms annotations into YOLO format with a train/val split.
  • Create the dataset YAML configuration file for training.
Clone the DeepPCB dataset

DeepPCB is an open-source dataset published under the MIT License. It contains 1,500 image pairs captured with a linear scan CCD at approximately 48 pixels per millimeter. Each 640x640 image has a companion annotation file listing defect bounding boxes.

  • Switch back to the terminal in PyCharm (your virtual environment should still be active).
  • Clone the DeepPCB repository into your project by running this command:
git clone https://github.com/tangsanli5201/DeepPCB.git

What is the DeepPCB annotation format?

Each annotation file uses the format x1, y1, x2, y2, class_id where (x1, y1) is the top-left corner and (x2, y2) is the bottom-right corner of the defect bounding box. The class IDs map to 6 defect types:

  • 0 = background (not used)
  • 1 = open (broken trace)
  • 2 = short (unintended connection)
  • 3 = mousebite (irregular copper edge)
  • 4 = spur (unwanted protrusion)
  • 5 = spurious copper (residual copper)
  • 6 = pinhole (small hole in copper)

YOLO expects a different format: class_id center_x center_y width height with all values normalized to 0-1 relative to image dimensions. Your conversion script will bridge this gap.

  • Confirm the clone succeeded by running:
ls DeepPCB/PCBData

You should see directories like group00041, group12000, and others listed in the output.

Clone taking too long or failing?

The DeepPCB repository is around 600MB because it includes all the image data. If the clone is slow, check your internet connection and wait for it to finish. If you see a permission error, make sure you are inside your pcb-defect-detection project folder.

Help me troubleshoot cloning the DeepPCB dataset

Write and run the conversion script

Now you will write a Python script that walks through every DeepPCB annotation file, converts coordinates to YOLO format, and splits the data into training and validation sets.

  • In PyCharm, right-click the src/ folder in the Project panel and select New then Python File.
  • Name the file convert_deeppcb and press Enter.
  • Add the module docstring, imports, and class constants by pasting this code:
"""\nDeepPCB Dataset Converter\n=========================\nConverts the DeepPCB dataset annotation format to YOLO format\nfor training object detection models.\n\nDeepPCB format: x1, y1, x2, y2, class_id (absolute pixel coordinates)\nYOLO format: class_id, center_x, center_y, width, height (normalized 0-1)\n\nDataset source: https://github.com/tangsanli5201/DeepPCB (MIT License)\n"""\n\nimport os\nimport shutil\nimport random\n\n# DeepPCB defect classes (0-indexed for YOLO)\n# Original: 1-open, 2-short, 3-mousebite, 4-spur, 5-copper, 6-pin-hole\n# YOLO: 0-open, 1-short, 2-mousebite, 3-spur, 4-copper, 5-pinhole\nCLASS_NAMES = ["open", "short", "mousebite", "spur", "copper", "pinhole"]\n\nIMG_SIZE = 640  # DeepPCB images are 640x640

What does this code do?

  • The docstring documents what the script does and what formats it converts between.
  • CLASS_NAMES maps YOLO's 0-indexed classes to the 6 defect types. DeepPCB uses 1-6, but YOLO expects 0-5.
  • IMG_SIZE is set to 640 because all DeepPCB images are 640x640 pixels. This constant is used to normalize pixel coordinates into the 0-1 range YOLO expects.
  • Below the constants, add the convert_annotation function that converts a single annotation file:
\ndef convert_annotation(annotation_path):\n    """\n    Convert a single DeepPCB annotation file to YOLO format.\n\n    Args:\n        annotation_path: Path to the DeepPCB .txt annotation file.\n\n    Returns:\n        List of YOLO-formatted annotation strings.\n    """\n    yolo_lines = []\n    with open(annotation_path, "r") as f:\n        for line in f:\n            parts = line.strip().split()\n            if len(parts) != 5:\n                continue\n            x1, y1, x2, y2, class_id = (\n                int(parts[0]),\n                int(parts[1]),\n                int(parts[2]),\n                int(parts[3]),\n                int(parts[4]),\n            )\n\n            # Skip background class (0)\n            if class_id == 0:\n                continue\n\n            # Convert to YOLO format (0-indexed class, normalized coordinates)\n            yolo_class = class_id - 1\n            center_x = ((x1 + x2) / 2) / IMG_SIZE\n            center_y = ((y1 + y2) / 2) / IMG_SIZE\n            width = (x2 - x1) / IMG_SIZE\n            height = (y2 - y1) / IMG_SIZE\n\n            yolo_lines.append(\n                f\"{yolo_class} {center_x:.6f} {center_y:.6f} {width:.6f} {height:.6f}\"\n            )\n    return yolo_lines

What does this function do?

This function reads one DeepPCB annotation file and converts each defect entry to YOLO format. The key transformation is:

  • Subtract 1 from the class ID (DeepPCB uses 1-6, YOLO uses 0-5).
  • Convert corner coordinates (x1, y1, x2, y2) to center coordinates (center_x, center_y) plus width and height.
  • Divide all coordinate values by 640 to normalize them into the 0-1 range.
  • Below convert_annotation, add the prepare_dataset function and the main execution block:
\ndef prepare_dataset(deeppcb_path, output_path, train_ratio=0.8):\n    """\n    Convert entire DeepPCB dataset to YOLO format with train/val split.\n\n    Args:\n        deeppcb_path: Path to cloned DeepPCB repository.\n        output_path: Output directory for YOLO-formatted dataset.\n        train_ratio: Fraction of data used for training (default 0.8).\n\n    Returns:\n        Tuple of (train_count, val_count).\n    """\n    # Create output directories\n    for split in ["train", "val"]:\n        os.makedirs(os.path.join(output_path, "images", split), exist_ok=True)\n        os.makedirs(os.path.join(output_path, "labels", split), exist_ok=True)\n\n    # Collect all image-annotation pairs\n    pairs = []\n    pcb_data_path = os.path.join(deeppcb_path, "PCBData")\n\n    for group_dir in sorted(os.listdir(pcb_data_path)):\n        group_path = os.path.join(pcb_data_path, group_dir)\n        if not os.path.isdir(group_path):\n            continue\n\n        for sub_dir in os.listdir(group_path):\n            sub_path = os.path.join(group_path, sub_dir)\n            if not os.path.isdir(sub_path):\n                continue\n            # Skip annotation directories (ending with _not)\n            if sub_dir.endswith("_not"):\n                continue\n\n            # Find test images and their annotations\n            for filename in os.listdir(sub_path):\n                if filename.endswith("_test.jpg"):\n                    base_name = filename.replace("_test.jpg", "")\n                    annotation_dir = sub_path.replace(sub_dir, sub_dir + "_not")\n                    annotation_file = os.path.join(annotation_dir, base_name + ".txt")\n\n                    if os.path.exists(annotation_file):\n                        image_path = os.path.join(sub_path, filename)\n                        pairs.append((image_path, annotation_file, base_name))\n\n    # Shuffle and split\n    random.seed(42)\n    random.shuffle(pairs)\n    split_idx = int(len(pairs) * train_ratio)\n    train_pairs = pairs[:split_idx]\n    val_pairs = pairs[split_idx:]\n\n    # Process pairs\n    for split_name, split_pairs in [("train", train_pairs), ("val", val_pairs)]:\n        for image_path, annotation_path, base_name in split_pairs:\n            # Copy image\n            dst_image = os.path.join(output_path, "images", split_name, base_name + ".jpg")\n            shutil.copy2(image_path, dst_image)\n\n            # Convert and save annotation\n            yolo_lines = convert_annotation(annotation_path)\n            dst_label = os.path.join(output_path, "labels", split_name, base_name + ".txt")\n            with open(dst_label, "w") as f:\n                f.write("\\n".join(yolo_lines))\n\n    # Copy sample images for the GitHub repo\n    os.makedirs("samples/results", exist_ok=True)\n    if val_pairs:\n        sample_image_path = val_pairs[0][0]\n        shutil.copy2(sample_image_path, "samples/sample_pcb_01.jpg")\n    if len(val_pairs) > 1:\n        shutil.copy2(val_pairs[1][0], "samples/sample_pcb_02.jpg")\n\n    print(f"Dataset prepared: {len(train_pairs)} training, {len(val_pairs)} validation images")\n    print(f"Sample images copied to samples/ directory")\n    return len(train_pairs), len(val_pairs)\n\n\nif __name__ == "__main__":\n    deeppcb_path = "DeepPCB"\n    output_path = "pcb_dataset"\n    prepare_dataset(deeppcb_path, output_path)

What does this function do?

The prepare_dataset function orchestrates the entire conversion pipeline:

  • Creates the YOLO-expected folder structure (images/train, images/val, labels/train, labels/val).
  • Walks through the DeepPCB directory tree to find test images and their matching annotation files.
  • Shuffles with a fixed seed (42) for reproducibility, then splits 80% training and 20% validation.
  • Copies two validation images into samples/ so your GitHub README can show real detection examples.
  • Save src/convert_deeppcb.py.
  • Run the conversion script from the terminal:
python src/convert_deeppcb.py

You should see output similar to: Dataset prepared: 1200 training, 300 validation images.

  • Verify the output structure by running:
ls pcb_dataset/images/train | head -5

You should see .jpg files listed. You should also see samples/sample_pcb_01.jpg and samples/sample_pcb_02.jpg in your project.

Seeing a FileNotFoundError?

Make sure you are running the command from your pcb-defect-detection project root folder (not from inside src/). The script expects to find the DeepPCB/ directory at the project root.

Help me fix a FileNotFoundError in my conversion script

✔️ Awesome, I've got everything!

Great. Double check you saved src/convert_deeppcb.py and that the script ran without errors.

ⓧ I'd like to double check the full code

"""
DeepPCB Dataset Converter
=========================
Converts the DeepPCB dataset annotation format to YOLO format
for training object detection models.

DeepPCB format: x1, y1, x2, y2, class_id (absolute pixel coordinates)
YOLO format: class_id, center_x, center_y, width, height (normalized 0-1)

Dataset source: https://github.com/tangsanli5201/DeepPCB (MIT License)
"""

import os
import shutil
import random

# DeepPCB defect classes (0-indexed for YOLO)
# Original: 1-open, 2-short, 3-mousebite, 4-spur, 5-copper, 6-pin-hole
# YOLO: 0-open, 1-short, 2-mousebite, 3-spur, 4-copper, 5-pinhole
CLASS_NAMES = ["open", "short", "mousebite", "spur", "copper", "pinhole"]

IMG_SIZE = 640  # DeepPCB images are 640x640


def convert_annotation(annotation_path):
    """
    Convert a single DeepPCB annotation file to YOLO format.

    Args:
        annotation_path: Path to the DeepPCB .txt annotation file.

    Returns:
        List of YOLO-formatted annotation strings.
    """
    yolo_lines = []
    with open(annotation_path, "r") as f:
        for line in f:
            parts = line.strip().split()
            if len(parts) != 5:
                continue
            x1, y1, x2, y2, class_id = (
                int(parts[0]),
                int(parts[1]),
                int(parts[2]),
                int(parts[3]),
                int(parts[4]),
            )

            # Skip background class (0)
            if class_id == 0:
                continue

            # Convert to YOLO format (0-indexed class, normalized coordinates)
            yolo_class = class_id - 1
            center_x = ((x1 + x2) / 2) / IMG_SIZE
            center_y = ((y1 + y2) / 2) / IMG_SIZE
            width = (x2 - x1) / IMG_SIZE
            height = (y2 - y1) / IMG_SIZE

            yolo_lines.append(
                f"{yolo_class} {center_x:.6f} {center_y:.6f} {width:.6f} {height:.6f}"
            )
    return yolo_lines


def prepare_dataset(deeppcb_path, output_path, train_ratio=0.8):
    """
    Convert entire DeepPCB dataset to YOLO format with train/val split.

    Args:
        deeppcb_path: Path to cloned DeepPCB repository.
        output_path: Output directory for YOLO-formatted dataset.
        train_ratio: Fraction of data used for training (default 0.8).

    Returns:
        Tuple of (train_count, val_count).
    """
    # Create output directories
    for split in ["train", "val"]:
        os.makedirs(os.path.join(output_path, "images", split), exist_ok=True)
        os.makedirs(os.path.join(output_path, "labels", split), exist_ok=True)

    # Collect all image-annotation pairs
    pairs = []
    pcb_data_path = os.path.join(deeppcb_path, "PCBData")

    for group_dir in sorted(os.listdir(pcb_data_path)):
        group_path = os.path.join(pcb_data_path, group_dir)
        if not os.path.isdir(group_path):
            continue

        for sub_dir in os.listdir(group_path):
            sub_path = os.path.join(group_path, sub_dir)
            if not os.path.isdir(sub_path):
                continue
            # Skip annotation directories (ending with _not)
            if sub_dir.endswith("_not"):
                continue

            # Find test images and their annotations
            for filename in os.listdir(sub_path):
                if filename.endswith("_test.jpg"):
                    base_name = filename.replace("_test.jpg", "")
                    annotation_dir = sub_path.replace(sub_dir, sub_dir + "_not")
                    annotation_file = os.path.join(annotation_dir, base_name + ".txt")

                    if os.path.exists(annotation_file):
                        image_path = os.path.join(sub_path, filename)
                        pairs.append((image_path, annotation_file, base_name))

    # Shuffle and split
    random.seed(42)
    random.shuffle(pairs)
    split_idx = int(len(pairs) * train_ratio)
    train_pairs = pairs[:split_idx]
    val_pairs = pairs[split_idx:]

    # Process pairs
    for split_name, split_pairs in [("train", train_pairs), ("val", val_pairs)]:
        for image_path, annotation_path, base_name in split_pairs:
            # Copy image
            dst_image = os.path.join(output_path, "images", split_name, base_name + ".jpg")
            shutil.copy2(image_path, dst_image)

            # Convert and save annotation
            yolo_lines = convert_annotation(annotation_path)
            dst_label = os.path.join(output_path, "labels", split_name, base_name + ".txt")
            with open(dst_label, "w") as f:
                f.write("\n".join(yolo_lines))

    # Copy sample images for the GitHub repo
    os.makedirs("samples/results", exist_ok=True)
    if val_pairs:
        sample_image_path = val_pairs[0][0]
        shutil.copy2(sample_image_path, "samples/sample_pcb_01.jpg")
    if len(val_pairs) > 1:
        shutil.copy2(val_pairs[1][0], "samples/sample_pcb_02.jpg")

    print(f"Dataset prepared: {len(train_pairs)} training, {len(val_pairs)} validation images")
    print(f"Sample images copied to samples/ directory")
    return len(train_pairs), len(val_pairs)


if __name__ == "__main__":
    deeppcb_path = "DeepPCB"
    output_path = "pcb_dataset"
    prepare_dataset(deeppcb_path, output_path)
Create the dataset YAML configuration

Ultralytics YOLOv8 needs a YAML configuration file that tells it where to find images and what classes exist. This file is the bridge between your prepared dataset and the training process.

  • In PyCharm, right-click the configs/ folder in the Project panel and select New then File.
  • Name the file pcb_dataset.yaml and press Enter.
  • Paste the following configuration:
# PCB Defect Detection Dataset Configuration
# Dataset: DeepPCB (https://github.com/tangsanli5201/DeepPCB)
# License: MIT

path: pcb_dataset
train: images/train
val: images/val

names:
  0: open
  1: short
  2: mousebite
  3: spur
  4: copper
  5: pinhole

What does this YAML file do?

  • path tells YOLO where the dataset root folder is (relative to where you run training).
  • train and val point to the image subdirectories. YOLO automatically looks for matching label files in a parallel labels/ folder.
  • names maps each class ID to a human-readable name. These must match the class IDs your conversion script produced.
  • Save configs/pcb_dataset.yaml.
  • Verify the file exists by running:
cat configs/pcb_dataset.yaml

You should see the YAML content printed back to the terminal with all 6 class names listed.

YAML not found or empty?

Make sure the file is saved inside the configs/ directory at your project root, not inside src/ or the project root itself. The filename must be exactly pcb_dataset.yaml.

Help me fix my YAML dataset configuration file

Your data pipeline is complete. You have 6 defect classes converted to YOLO format, split into training and validation sets, and configured for training. Next up, you will train YOLOv8 on this prepared dataset.

Train YOLOv8 on PCB Defects

Your dataset is prepped, converted, and split into training and validation sets. Now comes the exciting part: teaching the model to see defects.

You'll fine-tune a pretrained YOLOv8s model on your PCB dataset using transfer learning. The model starts with general object detection knowledge from millions of images and adapts specifically to detecting PCB defects.

In this step, get ready to:
  • Create and run the training script to fine-tune YOLOv8s on PCB defects.
  • Review training metrics like mAP, precision, and recall.
  • Validate the model and save detection results for the README.
Configure and launch YOLOv8s training

Training a YOLO model from scratch on only 1,500 images would produce poor results. Transfer learning solves this by starting from a model already trained on COCO (a massive general-purpose dataset) and fine-tuning it for your specific task.

  • In PyCharm, right-click the src/ folder in the Project panel and select New then Python File.
  • Name the file train and press Enter.
  • Paste the following training script into src/train.py:
"""\nPCB Defect Detection - Model Training\n======================================\nFine-tunes YOLOv8s on the DeepPCB dataset for PCB defect detection.\n\nUsage:\n    python src/train.py\n\nOutput:\n    Trained model weights saved to pcb_defect_detection/yolov8s_pcb/weights/best.pt\n\nNote:\n    Training uses CPU device for stability. Apple Silicon MPS has known\n    issues with loss calculation in Ultralytics as of May 2026.\n"""\n\nfrom ultralytics import YOLO\n\n\ndef train_model():\n    """Train YOLOv8s on PCB defect dataset."""\n    # Load pretrained YOLOv8s model (transfer learning from COCO)\n    model = YOLO("yolov8s.pt")\n\n    # Train on PCB defect dataset\n    results = model.train(\n        data="configs/pcb_dataset.yaml",\n        epochs=50,\n        imgsz=640,\n        batch=16,\n        device="cpu",\n        project="pcb_defect_detection",\n        name="yolov8s_pcb",\n        patience=10,\n    )\n\n    print("\\nTraining complete!")\n    print(f"Best model saved at: pcb_defect_detection/yolov8s_pcb/weights/best.pt")\n    print(f"\\nTo run inference:")\n    print(f"  python src/inspect_pcb.py samples/sample_pcb_01.jpg")\n\n    return results\n\n\nif __name__ == "__main__":\n    train_model()

What does this code do?

YOLO("yolov8s.pt") loads the pretrained YOLOv8s ("small") model. The "s" variant balances speed and accuracy for a dataset this size.

model.train(...) launches fine-tuning with these key parameters:

  • data points to your dataset YAML config from the previous step.
  • epochs=50 runs 50 full passes through the training data.
  • device="cpu" forces CPU training for stability (see callout below).
  • patience=10 enables early stopping if performance stops improving for 10 epochs.

Why CPU instead of MPS on Apple Silicon?

Apple Silicon Macs have an MPS (Metal Performance Shaders) GPU that can accelerate inference. However, Ultralytics has known training bugs on MPS as of May 2026.

Reported issues include incorrect loss calculations (box_loss and dfl_loss showing as 0) and significant mAP discrepancies between training-time validation and independent validation. CPU training is reliable and fast enough for the 1,500-image DeepPCB dataset.

  • Open the terminal in PyCharm (View > Tool Windows > Terminal).
  • Launch training by running this command:
python src/train.py

What should I see?

Ultralytics will first download the yolov8s.pt pretrained weights (about 22 MB). Then training begins, printing a progress table for each epoch showing metrics like box_loss, cls_loss, and dfl_loss.

Training on CPU with this dataset takes roughly 30-60 minutes depending on your machine. Let it run to completion.

Training not starting?

Make sure your virtual environment is activated. You should see (venv) in your terminal prompt.

If you see an error about configs/pcb_dataset.yaml not being found, make sure you are running the command from your project root folder (the one containing the configs/ directory).

help me debug why my YOLOv8 training script isn't starting

Monitor training progress and understand metrics

As training runs, YOLO prints metrics after each epoch. Understanding these numbers tells you whether the model is actually learning to detect defects.

What do the training metrics mean?

  • mAP50 (mean Average Precision at IoU 0.5): the primary detection accuracy metric. Higher is better. This measures how well the model's predicted boxes overlap with the ground truth across all 6 defect classes.
  • Precision: of all the defects the model claims to find, how many are actually real defects. High precision means few false alarms.
  • Recall: of all the real defects in the images, how many did the model find. High recall means few missed defects.
  • box_loss: how accurately the model draws bounding boxes around defects. This should decrease over time.
  • cls_loss: how well the model classifies which type of defect it found. This should also decrease.
  • Watch the terminal output as training progresses.
  • At the end of training, note the final mAP50 value printed in the results summary.

When training completes, YOLO saves the best model weights to pcb_defect_detection/yolov8s_pcb/weights/best.pt. This is the model checkpoint with the highest validation performance across all 50 epochs.

Validate the model and save detection results

Now that training is done, you'll run the trained model on your sample images to generate annotated detection results. These images go into samples/results/ for your GitHub README.

  • Generate sample detection images by running this command in the terminal:
python -c "from ultralytics import YOLO; model = YOLO('pcb_defect_detection/yolov8s_pcb/weights/best.pt'); model.predict(source='samples/sample_pcb_01.jpg', save=True, project='samples', name='results')"

What does this command do?

This one-liner loads your trained model from the saved weights, runs inference on sample_pcb_01.jpg, and saves the annotated result (with bounding boxes drawn on detected defects) into samples/results/.

The project and name parameters control the output directory structure, placing results at samples/results/.

  • Verify the detection image was saved by running:
ls samples/results/

You should see an image file (like sample_pcb_01.jpg) with bounding boxes drawn around any detected defects.

Not seeing results in samples/results/?

If Ultralytics creates a folder like samples/results2/ instead, it means a samples/results/ folder already existed from the dataset conversion step. Move the file manually or delete the empty samples/results/ folder and re-run the command.

help me figure out where my YOLO prediction results were saved

✔️ Awesome, I've got everything!

Great. Double check you've saved src/train.py and that your samples/results/ folder contains at least one detection image.

ⓧ I'd like to double check the full code

"""\nPCB Defect Detection - Model Training\n======================================\nFine-tunes YOLOv8s on the DeepPCB dataset for PCB defect detection.\n\nUsage:\n    python src/train.py\n\nOutput:\n    Trained model weights saved to pcb_defect_detection/yolov8s_pcb/weights/best.pt\n\nNote:\n    Training uses CPU device for stability. Apple Silicon MPS has known\n    issues with loss calculation in Ultralytics as of May 2026.\n"""\n\nfrom ultralytics import YOLO\n\n\ndef train_model():\n    """Train YOLOv8s on PCB defect dataset."""\n    # Load pretrained YOLOv8s model (transfer learning from COCO)\n    model = YOLO("yolov8s.pt")\n\n    # Train on PCB defect dataset\n    results = model.train(\n        data="configs/pcb_dataset.yaml",\n        epochs=50,\n        imgsz=640,\n        batch=16,\n        device="cpu",\n        project="pcb_defect_detection",\n        name="yolov8s_pcb",\n        patience=10,\n    )\n\n    print("\\nTraining complete!")\n    print(f"Best model saved at: pcb_defect_detection/yolov8s_pcb/weights/best.pt")\n    print(f"\\nTo run inference:")\n    print(f"  python src/inspect_pcb.py samples/sample_pcb_01.jpg")\n\n    return results\n\n\nif __name__ == "__main__":\n    train_model()

Your model is trained and you have visual proof of its detection capabilities saved in your repo. Next up, you'll build an inspection report generator that turns these raw detections into structured pass/fail reports.

Build the Inspection Report Generator

You now have a trained YOLOv8 model that can detect PCB defects. A trained model sitting in a weights file is useful, but it does not tell an inspector whether a board passes or fails.

In electronics manufacturing, every inspected board gets a structured report documenting what was found, where, and whether the board meets acceptance criteria. This is the First Article Inspection Report (FAIR) workflow. In this step, you will build an inspection engine that loads your trained model, runs inference on a PCB image, classifies each defect by severity, and outputs a PASS/FAIL report.

In this step, get ready to:
  • Create the inspection module with severity configuration constants.
  • Write the core inspection function with PASS/FAIL disposition logic.
  • Add terminal report formatting and JSON file output.
Set up the inspection module with configuration constants

Before writing any detection logic, you need to define the acceptance criteria that determine whether a PCB passes or fails inspection. In real manufacturing, these thresholds come from customer specifications and industry standards like IPC.

  • In PyCharm, create a new file called inspect_pcb.py inside your src/ folder.
  • Add the module docstring, imports, and configuration constants by pasting this code:
"""
PCB Defect Detection - Inspection Engine
==========================================
Runs inference on PCB images and generates structured inspection reports
with PASS/FAIL disposition based on configurable acceptance criteria.

Usage:
    python src/inspect_pcb.py <image_path>
    python src/inspect_pcb.py samples/sample_pcb_01.jpg

Output:
    - Terminal-formatted inspection report
    - JSON report file (inspection_report.json)
    - Annotated image with detection bounding boxes (inspection_result.jpg)
"""

import json
import os
import sys
from datetime import datetime
from ultralytics import YOLO

# Defect class names (matches training configuration)
CLASS_NAMES = ["open", "short", "mousebite", "spur", "copper", "pinhole"]

# Acceptance criteria (configurable per customer/standard)
MAX_CRITICAL_DEFECTS = 0  # open, short - zero tolerance
MAX_MAJOR_DEFECTS = 1     # mousebite, spur
MAX_MINOR_DEFECTS = 3     # copper, pinhole
CONFIDENCE_THRESHOLD = 0.25

# Severity classification by defect type
CRITICAL_CLASSES = [0, 1]   # open, short
MAJOR_CLASSES = [2, 3]      # mousebite, spur
MINOR_CLASSES = [4, 5]      # copper, pinhole

What does this code do?

The constants at the top define your inspection policy. MAX_CRITICAL_DEFECTS = 0 means any open circuit or short is an automatic fail. This mirrors how real manufacturing lines handle critical defects.

The CRITICAL_CLASSES, MAJOR_CLASSES, and MINOR_CLASSES lists map defect type indices to severity tiers. Opens and shorts are critical because they mean the circuit will not function. Mousebites and spurs are major because they compromise reliability. Spurious copper and pinholes are minor cosmetic issues.

Why these specific thresholds?

In production, acceptance criteria come from customer specs or IPC standards. Zero tolerance for critical defects is standard across the industry. The major and minor thresholds here are configurable. A stricter customer might set MAX_MAJOR_DEFECTS = 0 as well.

Write the inspection function with PASS/FAIL determination

The core of your inspection engine is a function that loads the trained model, runs inference on an image, parses every detected defect, classifies it by severity, and returns a structured report with a final disposition.

  • Below the constants you just added, paste the run_inspection() function:

def run_inspection(image_path, model_path="pcb_defect_detection/yolov8s_pcb/weights/best.pt"):
    """
    Run PCB inspection and generate structured report.

    Args:
        image_path: Path to PCB image to inspect.
        model_path: Path to trained YOLOv8 model weights.

    Returns:
        Tuple of (report_dict, ultralytics_result).
    """
    # Load model
    model = YOLO(model_path)

    # Run inference
    results = model(image_path, conf=CONFIDENCE_THRESHOLD)
    result = results[0]

    # Parse detections
    defects = []
    for box in result.boxes:
        class_id = int(box.cls[0])
        confidence = float(box.conf[0])
        x1, y1, x2, y2 = box.xyxy[0].tolist()

        defects.append({
            "class_id": class_id,
            "class_name": CLASS_NAMES[class_id],
            "confidence": round(confidence, 4),
            "bbox": {
                "x1": round(x1, 1),
                "y1": round(y1, 1),
                "x2": round(x2, 1),
                "y2": round(y2, 1),
            },
            "area_px": round((x2 - x1) * (y2 - y1), 1),
        })

    # Classify by severity
    critical_count = sum(1 for d in defects if d["class_id"] in CRITICAL_CLASSES)
    major_count = sum(1 for d in defects if d["class_id"] in MAJOR_CLASSES)
    minor_count = sum(1 for d in defects if d["class_id"] in MINOR_CLASSES)

    # Determine pass/fail disposition
    if critical_count > MAX_CRITICAL_DEFECTS:
        disposition = "FAIL"
        reason = f"Critical defects detected: {critical_count} (max allowed: {MAX_CRITICAL_DEFECTS})"
    elif major_count > MAX_MAJOR_DEFECTS:
        disposition = "FAIL"
        reason = f"Major defects exceeded: {major_count} (max allowed: {MAX_MAJOR_DEFECTS})"
    elif minor_count > MAX_MINOR_DEFECTS:
        disposition = "FAIL"
        reason = f"Minor defects exceeded: {minor_count} (max allowed: {MAX_MINOR_DEFECTS})"
    else:
        disposition = "PASS"
        reason = "All defect counts within acceptance criteria"

    # Build report
    report = {
        "inspection_report": {
            "report_id": f"PCB-INS-{datetime.now().strftime('%Y%m%d-%H%M%S')}",
            "timestamp": datetime.now().isoformat(),
            "image_file": os.path.basename(image_path),
            "model_version": "YOLOv8s-PCB-v1",
            "confidence_threshold": CONFIDENCE_THRESHOLD,
        },
        "results": {
            "disposition": disposition,
            "reason": reason,
            "total_defects": len(defects),
            "critical_defects": critical_count,
            "major_defects": major_count,
            "minor_defects": minor_count,
        },
        "defect_details": defects,
        "acceptance_criteria": {
            "max_critical": MAX_CRITICAL_DEFECTS,
            "max_major": MAX_MAJOR_DEFECTS,
            "max_minor": MAX_MINOR_DEFECTS,
        },
    }

    return report, result

What does this function do?

  • model(image_path, conf=CONFIDENCE_THRESHOLD) runs YOLOv8 inference on the image, only keeping detections above 25% confidence.
  • The for box in result.boxes loop extracts each detected defect's class, confidence score, bounding box coordinates, and pixel area.
  • The severity classification counts how many defects fall into each tier (Critical, Major, Minor) using the class lists you defined earlier.
  • The cascading if/elif/else checks each severity tier against its threshold. Critical defects are checked first because they are the most serious.
  • The report dictionary mimics a structured FAIR document with metadata, results summary, defect details, and the criteria used for the disposition.
  • Save src/inspect_pcb.py.

You will not be able to run this yet because the file has no entry point. The next chunk adds that.

Add the terminal report display and file output

The inspection function returns structured data, but inspectors need a readable terminal summary they can scan at a glance. You also need a JSON file for integration with other systems and an annotated image showing where defects were found.

  • Below the run_inspection() function, add the print_report() function:

def print_report(report):
    """Print formatted inspection report to terminal."""
    print("\n" + "=" * 60)
    print("       PCB QUALITY INSPECTION REPORT")
    print("=" * 60)
    info = report["inspection_report"]
    print(f"  Report ID:    {info['report_id']}")
    print(f"  Timestamp:    {info['timestamp']}")
    print(f"  Image:        {info['image_file']}")
    print(f"  Model:        {info['model_version']}")
    print("-" * 60)

    results = report["results"]
    disposition = results["disposition"]
    status_marker = "PASS \u2713" if disposition == "PASS" else "FAIL \u2717"
    print(f"\n  DISPOSITION:  [{status_marker}]")
    print(f"  Reason:       {results['reason']}")
    print(f"\n  Total Defects Found: {results['total_defects']}")
    print(f"    Critical: {results['critical_defects']}")
    print(f"    Major:    {results['major_defects']}")
    print(f"    Minor:    {results['minor_defects']}")

    if report["defect_details"]:
        print("\n  DEFECT DETAILS:")
        print("  " + "-" * 56)
        for i, defect in enumerate(report["defect_details"], 1):
            print(f"    {i}. {defect['class_name'].upper()} "
                  f"(conf: {defect['confidence']:.2f}, "
                  f"area: {defect['area_px']:.0f}px)")

    print("\n" + "=" * 60)

What does this function do?

This formats the report dictionary into a human-readable terminal display. The layout mirrors what you would see on a paper FAIR: header with metadata, a clear PASS/FAIL stamp, severity breakdown, and a detailed defect list with confidence scores and sizes.

  • Below print_report(), add the CLI entry point that ties everything together:

if __name__ == "__main__":
    image_path = sys.argv[1] if len(sys.argv) > 1 else "samples/sample_pcb_01.jpg"

    report, result = run_inspection(image_path)
    print_report(report)

    # Save JSON report
    report_path = "inspection_report.json"
    with open(report_path, "w") as f:
        json.dump(report, f, indent=2)
    print(f"\n  JSON report saved to: {report_path}")

    # Save annotated image
    annotated_path = "inspection_result.jpg"
    result.save(annotated_path)
    print(f"  Annotated image saved to: {annotated_path}")

What does this code do?

  • sys.argv[1] reads the image path from the command line. If none is provided, it defaults to samples/sample_pcb_01.jpg.
  • json.dump(report, f, indent=2) writes the full inspection report as a formatted JSON file that other systems could consume.
  • result.save(annotated_path) uses Ultralytics' built-in plotting to save the image with detection bounding boxes drawn on it.
  • Save src/inspect_pcb.py.
  • Run the inspection on your sample image by running this command in your terminal:
python src/inspect_pcb.py samples/sample_pcb_01.jpg

You should see a formatted inspection report in your terminal with a DISPOSITION line showing either PASS or FAIL, followed by defect counts and details.

Seeing a FileNotFoundError?

  • If the error mentions best.pt, make sure your training from Step 3 completed successfully. The model weights should be at pcb_defect_detection/yolov8s_pcb/weights/best.pt.
  • If the error mentions sample_pcb_01.jpg, make sure your dataset conversion from Step 2 completed and copied sample images into the samples/ folder.
  • Make sure you are running the command from your project root directory (the pcb-defect-detection folder), not from inside src/.

Why is my inspect_pcb.py script not finding the model or sample image?

  • Confirm that inspection_report.json and inspection_result.jpg were created in your project root by running:
ls inspection_report.json inspection_result.jpg

You should see both files listed in the output.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file and that both output files exist.

ⓧ I'd like to double check the full code

Here is the complete src/inspect_pcb.py file:

"""
PCB Defect Detection - Inspection Engine
==========================================
Runs inference on PCB images and generates structured inspection reports
with PASS/FAIL disposition based on configurable acceptance criteria.

Usage:
    python src/inspect_pcb.py <image_path>
    python src/inspect_pcb.py samples/sample_pcb_01.jpg

Output:
    - Terminal-formatted inspection report
    - JSON report file (inspection_report.json)
    - Annotated image with detection bounding boxes (inspection_result.jpg)
"""

import json
import os
import sys
from datetime import datetime
from ultralytics import YOLO

# Defect class names (matches training configuration)
CLASS_NAMES = ["open", "short", "mousebite", "spur", "copper", "pinhole"]

# Acceptance criteria (configurable per customer/standard)
MAX_CRITICAL_DEFECTS = 0  # open, short - zero tolerance
MAX_MAJOR_DEFECTS = 1     # mousebite, spur
MAX_MINOR_DEFECTS = 3     # copper, pinhole
CONFIDENCE_THRESHOLD = 0.25

# Severity classification by defect type
CRITICAL_CLASSES = [0, 1]   # open, short
MAJOR_CLASSES = [2, 3]      # mousebite, spur
MINOR_CLASSES = [4, 5]      # copper, pinhole


def run_inspection(image_path, model_path="pcb_defect_detection/yolov8s_pcb/weights/best.pt"):
    """
    Run PCB inspection and generate structured report.

    Args:
        image_path: Path to PCB image to inspect.
        model_path: Path to trained YOLOv8 model weights.

    Returns:
        Tuple of (report_dict, ultralytics_result).
    """
    # Load model
    model = YOLO(model_path)

    # Run inference
    results = model(image_path, conf=CONFIDENCE_THRESHOLD)
    result = results[0]

    # Parse detections
    defects = []
    for box in result.boxes:
        class_id = int(box.cls[0])
        confidence = float(box.conf[0])
        x1, y1, x2, y2 = box.xyxy[0].tolist()

        defects.append({
            "class_id": class_id,
            "class_name": CLASS_NAMES[class_id],
            "confidence": round(confidence, 4),
            "bbox": {
                "x1": round(x1, 1),
                "y1": round(y1, 1),
                "x2": round(x2, 1),
                "y2": round(y2, 1),
            },
            "area_px": round((x2 - x1) * (y2 - y1), 1),
        })

    # Classify by severity
    critical_count = sum(1 for d in defects if d["class_id"] in CRITICAL_CLASSES)
    major_count = sum(1 for d in defects if d["class_id"] in MAJOR_CLASSES)
    minor_count = sum(1 for d in defects if d["class_id"] in MINOR_CLASSES)

    # Determine pass/fail disposition
    if critical_count > MAX_CRITICAL_DEFECTS:
        disposition = "FAIL"
        reason = f"Critical defects detected: {critical_count} (max allowed: {MAX_CRITICAL_DEFECTS})"
    elif major_count > MAX_MAJOR_DEFECTS:
        disposition = "FAIL"
        reason = f"Major defects exceeded: {major_count} (max allowed: {MAX_MAJOR_DEFECTS})"
    elif minor_count > MAX_MINOR_DEFECTS:
        disposition = "FAIL"
        reason = f"Minor defects exceeded: {minor_count} (max allowed: {MAX_MINOR_DEFECTS})"
    else:
        disposition = "PASS"
        reason = "All defect counts within acceptance criteria"

    # Build report
    report = {
        "inspection_report": {
            "report_id": f"PCB-INS-{datetime.now().strftime('%Y%m%d-%H%M%S')}",
            "timestamp": datetime.now().isoformat(),
            "image_file": os.path.basename(image_path),
            "model_version": "YOLOv8s-PCB-v1",
            "confidence_threshold": CONFIDENCE_THRESHOLD,
        },
        "results": {
            "disposition": disposition,
            "reason": reason,
            "total_defects": len(defects),
            "critical_defects": critical_count,
            "major_defects": major_count,
            "minor_defects": minor_count,
        },
        "defect_details": defects,
        "acceptance_criteria": {
            "max_critical": MAX_CRITICAL_DEFECTS,
            "max_major": MAX_MAJOR_DEFECTS,
            "max_minor": MAX_MINOR_DEFECTS,
        },
    }

    return report, result


def print_report(report):
    """Print formatted inspection report to terminal."""
    print("\n" + "=" * 60)
    print("       PCB QUALITY INSPECTION REPORT")
    print("=" * 60)
    info = report["inspection_report"]
    print(f"  Report ID:    {info['report_id']}")
    print(f"  Timestamp:    {info['timestamp']}")
    print(f"  Image:        {info['image_file']}")
    print(f"  Model:        {info['model_version']}")
    print("-" * 60)

    results = report["results"]
    disposition = results["disposition"]
    status_marker = "PASS ✓" if disposition == "PASS" else "FAIL ✗"
    print(f"\n  DISPOSITION:  [{status_marker}]")
    print(f"  Reason:       {results['reason']}")
    print(f"\n  Total Defects Found: {results['total_defects']}")
    print(f"    Critical: {results['critical_defects']}")
    print(f"    Major:    {results['major_defects']}")
    print(f"    Minor:    {results['minor_defects']}")

    if report["defect_details"]:
        print("\n  DEFECT DETAILS:")
        print("  " + "-" * 56)
        for i, defect in enumerate(report["defect_details"], 1):
            print(f"    {i}. {defect['class_name'].upper()} "
                  f"(conf: {defect['confidence']:.2f}, "
                  f"area: {defect['area_px']:.0f}px)")

    print("\n" + "=" * 60)


if __name__ == "__main__":
    image_path = sys.argv[1] if len(sys.argv) > 1 else "samples/sample_pcb_01.jpg"

    report, result = run_inspection(image_path)
    print_report(report)

    # Save JSON report
    report_path = "inspection_report.json"
    with open(report_path, "w") as f:
        json.dump(report, f, indent=2)
    print(f"\n  JSON report saved to: {report_path}")

    # Save annotated image
    annotated_path = "inspection_result.jpg"
    result.save(annotated_path)
    print(f"  Annotated image saved to: {annotated_path}")

Your inspection engine is working. It loads the trained model, detects defects, classifies them by severity, and generates both human-readable and machine-readable outputs. Next up, you will wrap this into a Gradio web interface so anyone can upload a PCB image and get an instant inspection report in their browser.

Deploy as a Gradio Web App and Write Documentation

Your inspection engine works from the command line, but a real portfolio project needs a polished front end that anyone can interact with.

In this step, you will build a Gradio web interface for your defect detector, then write professional documentation that makes your project GitHub-ready.

In this step, get ready to:
  • Build a Gradio web app that accepts PCB image uploads and displays annotated detection results with an inspection report.
  • Write a professional README.md with project overview, quick start guide, and architecture explanation.
  • Write a docs/METHODOLOGY.md explaining the ML pipeline, dataset, and quality inspection context.
Build the Gradio web interface

Gradio lets you wrap any Python function in a browser-based UI with just a few lines of code. You define an input component, an output component, and the function that connects them.

  • Create a new file called src/app.py in PyCharm.
  • Add the imports, model setup, and the detection function by pasting this code:
"""
PCB Defect Detection - Gradio Web Application
================================================
Interactive web interface for real-time PCB defect detection and
automated quality inspection reporting.

Usage:
    python src/app.py

Then open http://127.0.0.1:7860 in your browser.
"""

import os
import tempfile

import gradio as gr
import numpy as np
from PIL import Image
from ultralytics import YOLO

from inspect_pcb import run_inspection, CLASS_NAMES

MODEL_PATH = "pcb_defect_detection/yolov8s_pcb/weights/best.pt"
model = YOLO(MODEL_PATH)


def detect_defects(image):
    """
    Run defect detection on uploaded image and return annotated results.

    Args:
        image: Input image as numpy array (from Gradio).

    Returns:
        Tuple of (annotated_image, report_markdown).
    """
    if image is None:
        return None, "Please upload a PCB image."

    # Save temporary image for inference
    temp_path = tempfile.mktemp(suffix=".jpg")
    Image.fromarray(image).save(temp_path)

    # Run inspection
    report, result = run_inspection(temp_path, MODEL_PATH)

    # Get annotated image (BGR to RGB)
    annotated = result.plot()
    annotated_rgb = annotated[..., ::-1]

    # Format report as markdown
    results_data = report["results"]
    disposition = results_data["disposition"]
    status_emoji = "\u2705" if disposition == "PASS" else "\u274c"

    report_text = f"""## {status_emoji} Disposition: {disposition}

**Reason:** {results_data['reason']}

### Defect Summary
| Category | Count | Max Allowed |
|----------|-------|-------------|
| Critical (open, short) | {results_data['critical_defects']} | {report['acceptance_criteria']['max_critical']} |
| Major (mousebite, spur) | {results_data['major_defects']} | {report['acceptance_criteria']['max_major']} |
| Minor (copper, pinhole) | {results_data['minor_defects']} | {report['acceptance_criteria']['max_minor']} |

### Defect Details
"""
    for i, defect in enumerate(report["defect_details"], 1):
        report_text += (
            f"{i}. **{defect['class_name'].upper()}** - "
            f"Confidence: {defect['confidence']:.2f}, "
            f"Area: {defect['area_px']:.0f}px\n"
        )

    if not report["defect_details"]:
        report_text += "No defects detected.\n"

    report_text += f"\n---\n*Report ID: {report['inspection_report']['report_id']}*"

    # Clean up temp file
    os.remove(temp_path)

    return annotated_rgb, report_text

What does this code do?

  • The imports pull in Gradio for the UI, PIL for image handling, and the run_inspection function you already built.
  • MODEL_PATH points to your trained weights, and the model loads once at module level so it stays in memory between requests.
  • detect_defects() saves the uploaded image to a temp file, runs your inspection engine, converts the annotated image from BGR to RGB, and formats the report as a Markdown table.

You will not see a change yet. The next chunk adds the UI definition that calls this function.

  • Below the detect_defects function, add the Gradio interface definition and launch block:
# Build Gradio interface
demo = gr.Interface(
    fn=detect_defects,
    inputs=gr.Image(label="Upload PCB Image", type="numpy"),
    outputs=[
        gr.Image(label="Detection Results"),
        gr.Markdown(label="Inspection Report"),
    ],
    title="PCB Defect Detection - Quality Inspection System",
    description=(
        "Upload a PCB image to detect defects. The system identifies 6 defect types: "
        "Open, Short, Mousebite, Spur, Spurious Copper, and Pinhole. "
        "Results include a pass/fail disposition based on configurable acceptance criteria."
    ),
    examples=[
        ["samples/sample_pcb_01.jpg"],
    ],
)

if __name__ == "__main__":
    demo.launch()

What does the Interface definition do?

  • gr.Interface wraps your detect_defects function with an image upload input and two outputs (annotated image and Markdown report).
  • The examples parameter pre-loads your sample image so visitors can try the app instantly without uploading anything.
  • demo.launch() starts a local web server on port 7860.
  • Save src/app.py.
  • Launch the web app by running:
python src/app.py
  • Open http://127.0.0.1:7860 in your browser.
  • Upload one of your sample images (samples/sample_pcb_01.jpg) or click the pre-loaded example.

You should see the annotated image with bounding boxes on the left and a Markdown inspection report on the right.

App not launching?

  • Make sure you are running the command from your project root directory (the pcb-defect-detection folder), not from inside src/.
  • If you see a ModuleNotFoundError for inspect_pcb, confirm that src/inspect_pcb.py exists. Python adds the script's directory to the path automatically.
  • If port 7860 is already in use, stop any other Gradio apps first, then re-run.

help me debug my Gradio app not launching

  • Press Ctrl+C in the terminal to stop the Gradio server before continuing.

✔️ Awesome, I've got everything!

Great. Make sure you have saved src/app.py before moving on.

ⓧ I'd like to double check the full code

"""
PCB Defect Detection - Gradio Web Application
================================================
Interactive web interface for real-time PCB defect detection and
automated quality inspection reporting.

Usage:
    python src/app.py

Then open http://127.0.0.1:7860 in your browser.
"""

import os
import tempfile

import gradio as gr
import numpy as np
from PIL import Image
from ultralytics import YOLO

from inspect_pcb import run_inspection, CLASS_NAMES

MODEL_PATH = "pcb_defect_detection/yolov8s_pcb/weights/best.pt"
model = YOLO(MODEL_PATH)


def detect_defects(image):
    """
    Run defect detection on uploaded image and return annotated results.

    Args:
        image: Input image as numpy array (from Gradio).

    Returns:
        Tuple of (annotated_image, report_markdown).
    """
    if image is None:
        return None, "Please upload a PCB image."

    # Save temporary image for inference
    temp_path = tempfile.mktemp(suffix=".jpg")
    Image.fromarray(image).save(temp_path)

    # Run inspection
    report, result = run_inspection(temp_path, MODEL_PATH)

    # Get annotated image (BGR to RGB)
    annotated = result.plot()
    annotated_rgb = annotated[..., ::-1]

    # Format report as markdown
    results_data = report["results"]
    disposition = results_data["disposition"]
    status_emoji = "\u2705" if disposition == "PASS" else "\u274c"

    report_text = f"""## {status_emoji} Disposition: {disposition}

**Reason:** {results_data['reason']}

### Defect Summary
| Category | Count | Max Allowed |
|----------|-------|-------------|
| Critical (open, short) | {results_data['critical_defects']} | {report['acceptance_criteria']['max_critical']} |
| Major (mousebite, spur) | {results_data['major_defects']} | {report['acceptance_criteria']['max_major']} |
| Minor (copper, pinhole) | {results_data['minor_defects']} | {report['acceptance_criteria']['max_minor']} |

### Defect Details
"""
    for i, defect in enumerate(report["defect_details"], 1):
        report_text += (
            f"{i}. **{defect['class_name'].upper()}** - "
            f"Confidence: {defect['confidence']:.2f}, "
            f"Area: {defect['area_px']:.0f}px\n"
        )

    if not report["defect_details"]:
        report_text += "No defects detected.\n"

    report_text += f"\n---\n*Report ID: {report['inspection_report']['report_id']}*"

    # Clean up temp file
    os.remove(temp_path)

    return annotated_rgb, report_text


# Build Gradio interface
demo = gr.Interface(
    fn=detect_defects,
    inputs=gr.Image(label="Upload PCB Image", type="numpy"),
    outputs=[
        gr.Image(label="Detection Results"),
        gr.Markdown(label="Inspection Report"),
    ],
    title="PCB Defect Detection - Quality Inspection System",
    description=(
        "Upload a PCB image to detect defects. The system identifies 6 defect types: "
        "Open, Short, Mousebite, Spur, Spurious Copper, and Pinhole. "
        "Results include a pass/fail disposition based on configurable acceptance criteria."
    ),
    examples=[
        ["samples/sample_pcb_01.jpg"],
    ],
)

if __name__ == "__main__":
    demo.launch()
Write the project README

The README is the first thing anyone sees when they visit your GitHub repository. A strong README communicates what the project does, how to run it, and why it matters.

  • Create a new file called README.md in your project root (the pcb-defect-detection folder).
  • Paste the following content:
# PCB Defect Detection System

An AI-powered quality inspection tool that uses YOLOv8 object detection to identify defects in Printed Circuit Boards (PCBs).

## Overview

This project implements an automated visual inspection system for PCBs, detecting 6 common defect types:

| Defect Type | Description | Severity |
|-------------|-------------|----------|
| Open | Broken trace/connection | Critical |
| Short | Unintended connection between traces | Critical |
| Mousebite | Irregular edges on copper traces | Major |
| Spur | Unwanted copper protrusion | Major |
| Spurious Copper | Residual copper in unintended areas | Minor |
| Pinhole | Small holes in copper traces | Minor |

## Quick Start

```bash
git clone https://github.com/YOUR_USERNAME/pcb-defect-detection.git
cd pcb-defect-detection
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
git clone https://github.com/tangsanli5201/DeepPCB.git
python src/convert_deeppcb.py
python src/train.py
python src/inspect_pcb.py samples/sample_pcb_01.jpg
python src/app.py
```

What makes this README effective?

  • The defect table gives visitors immediate context about what the system detects and how it classifies severity.
  • The Quick Start section lets a reviewer clone and run the project end-to-end with copy-paste commands.
  • Anyone evaluating your portfolio can understand the project scope in under 30 seconds.
  • Save README.md.
  • Confirm the file appears in your project root alongside requirements.txt and .gitignore.

README not rendering in PyCharm?

PyCharm shows a split preview for Markdown files. If you only see raw text, click the split-view icon in the top-right corner of the editor tab. The rendered version appears on GitHub automatically after you push.

help me with README formatting in PyCharm

Write the methodology documentation

A methodology document explains the technical decisions behind your project. It shows reviewers that you understand the engineering tradeoffs, not just the code.

  • Create a docs/ directory inside your project root by running:
mkdir docs
  • Create a new file called docs/METHODOLOGY.md and paste the following content:
# Methodology: PCB Defect Detection System

## Problem Statement

Manual visual inspection of PCBs is time-consuming and subject to human fatigue. This project applies deep learning-based object detection to automate the process.

## Dataset: DeepPCB

- Source: tangsanli5201/DeepPCB (MIT License)
- Size: 1,500 image pairs (640x640 pixels)
- Defect Types: 6 classes annotated with bounding boxes

## Model Selection: YOLOv8s

YOLO provides real-time inference, single-stage detection, and strong performance on small objects. The 'small' variant balances accuracy and speed for this dataset size.

## Training Configuration

| Parameter | Value |
|-----------|-------|
| Epochs | 50 |
| Batch Size | 16 |
| Image Size | 640x640 |
| Device | CPU |
| Patience | 10 |
| Base Model | yolov8s.pt |

Why include a methodology document?

Hiring managers at companies like TTM Technologies want to see that you understand why you made certain technical choices. This document explains the reasoning behind your model selection, dataset, and training configuration in one place.

It also shows that you can communicate technical work to non-ML stakeholders like quality engineers and manufacturing managers.

  • Save docs/METHODOLOGY.md.
  • Commit all your new files to git by running:
git add .
git commit -m "Add Gradio web app and project documentation"
  • Confirm the commit succeeded. You should see a summary listing src/app.py, README.md, and docs/METHODOLOGY.md in the output.

Git commit failed?

If you see an error about user identity, configure your git name and email first:

git config user.email "you@example.com"
git config user.name "Your Name"

Then re-run the commit command.

help me fix my git commit error

Your project is now complete and GitHub-ready. You have a trained model, an inspection engine, a web interface, and professional documentation. Next up, the Secret Mission adds severity classification and PDF report export to take your project even further.

Secret mission

Add Severity Classification and PDF Reports

Your current inspection system treats all defects equally. In real electronics manufacturing, a short circuit is far more dangerous than a cosmetic pinhole. Can you add severity classification with size-based escalation, and export professional PDF inspection reports that a quality manager could actually sign off on?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs or cloud resources to worry about.

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • pcb-defect-detection project folder on your Desktop (source code, configs, documentation).
  • venv/ virtual environment with installed Python packages (ultralytics, gradio, reportlab, opencv-python).
  • DeepPCB/ cloned dataset repository (1,500 image pairs).
  • pcb_dataset/ converted YOLO-format training data.
  • pcb_defect_detection/ trained model weights.
  • Generated output files (inspection_report.json, inspection_result.jpg, inspection_report.pdf).

Keep everything running

No action needed. Choose this if you're still actively building or plan to extend the project further.

  • Your project folder stays on your Desktop at ~/Desktop/pcb-defect-detection.
  • The trained model weights remain in pcb_defect_detection/yolov8s_pcb/weights/best.pt for future inference.
  • You can reactivate your virtual environment any time by running source venv/bin/activate from inside the project folder.

Pause - I'll come back to this later

Shut down any running processes to free up memory. Your files stay intact for when you return.

  • If the Gradio web app is still running in your terminal, press Ctrl+C to stop it.
  • Deactivate your virtual environment by running:
deactivate
  • Close PyCharm if you're done for now.
  • When you're ready to return, open the project in PyCharm and reactivate the virtual environment with source venv/bin/activate.

Delete - I don't want to use this again

Remove all project resources and start fresh. This deletes everything including your trained model, dataset, and source code.

  • If the Gradio web app is still running, press Ctrl+C to stop it.
  • Deactivate the virtual environment if it is still active:
deactivate
  • Navigate to your Desktop and delete the entire project folder by running these commands:
cd ~/Desktop
rm -rf pcb-defect-detection
  • On Windows, use this command instead:
cd ~\Desktop
rmdir /s /q pcb-defect-detection
  • Confirm the folder is gone by running:
ls
  • You should no longer see pcb-defect-detection in the output. This removes all source code, the virtual environment, the cloned DeepPCB dataset, converted training data, trained model weights, and generated report files.

Nice Work!

Nice Work!

Nice work! You just built a complete AI-powered PCB quality inspection system from scratch.

You've learned how to:

  • Trained a YOLOv8 object detection model on the DeepPCB dataset to identify 6 types of PCB defects with bounding boxes and confidence scores.
  • Built a full data pipeline that converts raw annotations to YOLO format and an automated inspection report generator with PASS/FAIL disposition mimicking industry First Article Inspection Reports.
  • Deployed a real-time detection web app using Gradio and packaged the entire project as a professional GitHub-ready portfolio with README, methodology docs, and sample outputs.
  • Secret Mission: Added defect severity classification with size-based escalation and professional PDF inspection report generation.

Ready to quiz yourself?