Manufacturing defect detection is one of the highest-ROI applications of computer vision. A single missed scratch or crack on an assembly line costs real money. YOLOv8 makes it straightforward to train a detector that spots defects in real time, and wrapping it in a FastAPI service means any system on your factory floor can call it over HTTP.

Here’s the full pipeline: dataset prep, training, inference with OpenCV annotations, and a production-ready API.

Prepare Your Dataset

YOLOv8 expects images and labels in a specific directory layout, plus a YAML config that describes the dataset. Each label file has one row per defect bounding box in normalized class x_center y_center width height format.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
defect_dataset/
├── images/
│   ├── train/
│   │   ├── img_001.jpg
│   │   ├── img_002.jpg
│   │   └── ...
│   └── val/
│       ├── img_100.jpg
│       └── ...
├── labels/
│   ├── train/
│   │   ├── img_001.txt
│   │   ├── img_002.txt
│   │   └── ...
│   └── val/
│       ├── img_100.txt
│       └── ...
└── dataset.yaml

Create the dataset.yaml that tells Ultralytics where everything lives and what your classes are:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# dataset.yaml
path: /home/user/defect_dataset
train: images/train
val: images/val

names:
  0: scratch
  1: dent
  2: crack
  3: stain

Adjust the class names to match your actual defect types. Label files use the same integer IDs. A label line like 2 0.45 0.30 0.10 0.08 means class crack centered at 45% across, 30% down, spanning 10% width and 8% height of the image.

If you’re labeling from scratch, tools like CVAT or Label Studio export directly to YOLO format. Aim for at least 200 images per class for a usable baseline.

Train YOLOv8 on Your Defect Dataset

Training takes a few lines. Start from a pretrained checkpoint so the model already understands edges, textures, and shapes – it just needs to learn what your specific defects look like.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from ultralytics import YOLO

# Load a pretrained YOLOv8 model (nano for fast iteration, large for production)
model = YOLO("yolov8n.pt")

# Train on your defect dataset
results = model.train(
    data="/home/user/defect_dataset/dataset.yaml",
    epochs=100,
    imgsz=640,
    batch=16,
    name="defect_detector",
    patience=15,       # early stopping after 15 epochs without improvement
    augment=True,      # built-in mosaic, flip, scale augmentations
)

The patience parameter enables early stopping, which prevents overfitting on small datasets. Training outputs land in runs/detect/defect_detector/, including best.pt (the checkpoint with the lowest validation loss) and metrics like mAP@50.

For a production model, swap yolov8n.pt with yolov8m.pt or yolov8l.pt. The nano variant trains fast and is great for prototyping, but medium or large models catch subtle defects more reliably.

Run Inference and Annotate with OpenCV

Once you have a trained model, load it and run predictions on new images. YOLOv8’s predict method returns structured results you can iterate over. Use OpenCV to draw bounding boxes and save annotated images for review.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import cv2
from ultralytics import YOLO

model = YOLO("runs/detect/defect_detector/weights/best.pt")

image_path = "test_images/sample_part.jpg"
results = model.predict(source=image_path, conf=0.5)

# Load the original image with OpenCV
img = cv2.imread(image_path)

for result in results:
    for box in result.boxes:
        x1, y1, x2, y2 = map(int, box.xyxy[0])
        confidence = float(box.conf[0])
        class_id = int(box.cls[0])
        label = result.names[class_id]

        # Draw bounding box
        cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2)

        # Add label with confidence
        text = f"{label} {confidence:.2f}"
        cv2.putText(img, text, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)

cv2.imwrite("annotated_output.jpg", img)
print(f"Saved annotated image with {len(results[0].boxes)} defects detected")

The conf=0.5 threshold filters out low-confidence predictions. Lower it to 0.3 if you’d rather catch more defects at the cost of some false positives – for quality control, false positives are usually cheaper than misses.

Deploy as a FastAPI Endpoint

Wrapping the model in a REST API lets any system on your network request inspections. Use FastAPI’s lifespan context manager to load the model once at startup and share it across requests.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
from contextlib import asynccontextmanager
from io import BytesIO

import cv2
import numpy as np
from fastapi import FastAPI, File, UploadFile
from fastapi.responses import JSONResponse
from ultralytics import YOLO

ml_models = {}

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Load model at startup
    ml_models["defect_detector"] = YOLO("runs/detect/defect_detector/weights/best.pt")
    yield
    # Cleanup on shutdown
    ml_models.clear()

app = FastAPI(lifespan=lifespan)

@app.post("/detect")
async def detect_defects(file: UploadFile = File(...)):
    contents = await file.read()
    np_arr = np.frombuffer(contents, np.uint8)
    img = cv2.imdecode(np_arr, cv2.IMREAD_COLOR)

    model = ml_models["defect_detector"]
    results = model.predict(source=img, conf=0.5)

    detections = []
    for box in results[0].boxes:
        x1, y1, x2, y2 = map(int, box.xyxy[0])
        detections.append({
            "class": results[0].names[int(box.cls[0])],
            "confidence": round(float(box.conf[0]), 3),
            "bbox": [x1, y1, x2, y2],
        })

    return JSONResponse(content={
        "defects_found": len(detections),
        "detections": detections,
    })

Run it with uvicorn main:app --host 0.0.0.0 --port 8000. Test with curl:

1
2
curl -X POST "http://localhost:8000/detect" \
  -F "file=@test_images/sample_part.jpg"

The response returns a JSON array of detected defects with class names, confidence scores, and pixel-coordinate bounding boxes. From here you can trigger alerts, log defects to a database, or feed results into a factory dashboard.

Common Errors and Fixes

RuntimeError: Dataset not found – Double-check that the path in your dataset.yaml is an absolute path, and that train and val paths are relative to it. A trailing slash or typo in the directory name is the usual culprit.

Low mAP after training – If your mAP@50 is below 0.5, you likely need more labeled data or your defects are too small relative to the image size. Try increasing imgsz to 1280 or cropping images tighter around the region of interest before labeling.

cv2.error: (-215:Assertion failed) !_src.empty() – OpenCV could not read the image. Verify the file path exists and the image is not corrupted. When receiving uploads via FastAPI, make sure you’re decoding the bytes with cv2.imdecode as shown above, not passing a file path.

Slow inference on CPU – YOLOv8 nano runs at roughly 20-40 FPS on a modern CPU. If that’s not enough, export to ONNX for faster CPU inference: model.export(format="onnx"), then load with YOLO("best.onnx").

ModuleNotFoundError: No module named 'ultralytics' – Install the package with pip install ultralytics. It pulls in PyTorch and OpenCV as dependencies automatically.