Search…

YOLO Detection Pipeline: Data to Inference

In this series (30 parts)
  1. Computer Vision Roadmap: From Basics to Real Projects
  2. What is Computer Vision? From Pixels to Decisions
  3. A Short History of Computer Vision: 1545 to Now
  4. Math for CV Beginners: Vectors, Matrices, Convolutions
  5. Image Fundamentals: Color, Histograms, Noise, Filtering
  6. How Images and Video Are Stored: Colour, JPEG, Frames
  7. OpenCV Setup + First 10 Tasks in Python
  8. Vision Metrics: Accuracy, Precision, Recall, mAP, IoU
  9. CV Project Workflow: Dataset, Baseline, Error Iteration
  10. Intensity Transforms and Frequency-Domain Filtering
  11. Edge Detection and Thresholding That Actually Work
  12. Morphology, Contours, and Shape Analysis for Real Images
  13. Feature Matching (SIFT/ORB) and Image Stitching
  14. Camera Calibration and Perspective Correction
  15. Epipolar Geometry, Stereo Vision, and Depth Estimation
  16. Optical Flow and Motion Tracking in Video
  17. HOG, HOF and MBH: Descriptors Before Deep Learning
  18. Your First Image Classifier (PyTorch + Transfer Learning)
  19. Data Pipelines and Augmentation for Vision Models
  20. CNN Architectures Explained: From LeNet to ResNet
  21. YOLO Detection Pipeline: Data to Inference
  22. Semantic and Instance Segmentation: U-Net to Mask R-CNN
  23. Vision Transformers (ViT) and When to Use Them
  24. CV Explainability: Grad-CAM, Failures, Bias Checks
  25. 3D Vision Basics: SfM, Point Clouds, and Pose Estimation
  26. Multimodal Vision: CLIP, Embeddings, and Retrieval Systems
  27. Video Understanding: Flow Networks, Interpolation, Stabilisation
  28. Real-Time CV Systems: Tracking, Latency, Streaming
  29. Deploying CV Models: ONNX, TensorRT, Edge, and APIs
  30. Responsible CV: Privacy, Fairness, Security, Governance

Classification answers “what is in this image”. Detection answers “what, where, and how many”. The jump is bigger than it sounds: a single forward pass produces 8,400 candidate boxes, and turning those into a clean answer is most of the work.

Prerequisites: CNN architectures and IoU and mAP.

The problem: hard hats on a construction site

A site safety officer wants an alert when someone walks into the excavation zone without a hard hat. A fixed camera watches the entrance. Two classes: hardhat and no_hardhat.

Classification cannot do this. There are usually several people in frame, some wearing hats and some not, and the answer needs to identify which person, so someone can be spoken to.

# Task Output Fits this problem? target
1 Classification one label for the whole image no — several people at once
2 Detection a box and class per object yes
3 Segmentation a class per pixel yes, but more than needed
4 Keypoints joint positions per person useful later, not now

Detection is the right tool: it counts and locates.

How one forward pass produces 8,400 boxes

Modern YOLO feeds a 640×640 image into a backbone, then makes predictions at three different strides.

# Stride Grid size Cells target Each cell covers Best for objects around
1 8 80 × 80 6,400 8 × 8 px 8–64 px — distant people
2 16 40 × 40 1,600 16 × 16 px 32–128 px — mid-range
3 32 20 × 20 400 32 × 32 px 96–640 px — close-up
4 **Total** **8,400**

Three prediction scales. 6400 + 1600 + 400 = 8,400 candidate boxes per image.

Each of those 8,400 cells outputs 4 box numbers, 1 objectness score, and one score per class. With 2 classes that is 7 numbers × 8,400 = 58,800 values from a single forward pass.

Decoding one cell’s prediction

Each cell predicts an offset from its own position rather than an absolute coordinate. That keeps the numbers small and easy to learn.

Take cell (12,7)(12, 7) on the 80×80 grid, which has stride 8. Suppose it predicts offsets tx=0.35t_x = 0.35, ty=0.62t_y = 0.62 and sizes w=68w = 68, h=154h = 154 px:

cx=(12+0.35)×8=12.35×8=98.8c_x = (12 + 0.35) \times 8 = 12.35 \times 8 = 98.8 cy=(7+0.62)×8=7.62×8=60.96c_y = (7 + 0.62) \times 8 = 7.62 \times 8 = 60.96

So the box centre is at pixel (98.8,61.0)(98.8, 61.0), and the corners are:

x1=98.834=64.8,y1=61.077=16.00x_1 = 98.8 - 34 = 64.8, \quad y_1 = 61.0 - 77 = -16.0 \rightarrow 0 x2=98.8+34=132.8,y2=61.0+77=138.0x_2 = 98.8 + 34 = 132.8, \quad y_2 = 61.0 + 77 = 138.0

The negative y1y_1 is clamped to 0 — a person whose head is cut off by the top of the frame.

Getting the labels right

This is where most projects fail silently. YOLO wants one .txt per image, one line per object:

class_id  cx  cy  w  h

All four numbers normalised to 0–1 by image width and height, and the centre, not the corner.

Convert a box drawn at pixel corners (98,61)(98, 61) to (210,305)(210, 305) in a 640×640 image:

cx=98+2102=154    154640=0.2406c_x = \frac{98 + 210}{2} = 154 \;\rightarrow\; \frac{154}{640} = 0.2406 cy=61+3052=183    183640=0.2859c_y = \frac{61 + 305}{2} = 183 \;\rightarrow\; \frac{183}{640} = 0.2859 w=21098=112    112640=0.1750w = 210 - 98 = 112 \;\rightarrow\; \frac{112}{640} = 0.1750 h=30561=244    244640=0.3813h = 305 - 61 = 244 \;\rightarrow\; \frac{244}{640} = 0.3813

The label line is:

1 0.240625 0.285938 0.175000 0.381250
# Format target Order Origin Normalised?
1 YOLO cx cy w h box centre yes, 0–1
2 COCO x y w h top-left corner no, pixels
3 Pascal VOC x1 y1 x2 y2 two corners no, pixels
4 Albumentations x1 y1 x2 y2 two corners yes, 0–1

Four box formats. Mixing them up is the most common detection bug and it produces no error message.

def voc_to_yolo(x1, y1, x2, y2, W, H):
    return ((x1 + x2) / 2 / W, (y1 + y2) / 2 / H,
            (x2 - x1) / W,     (y2 - y1) / H)

# Always verify by drawing the boxes back onto the images
import cv2
def draw_labels(img_path, label_path):
    img = cv2.imread(img_path); H, W = img.shape[:2]
    for line in open(label_path):
        c, cx, cy, w, h = map(float, line.split())
        x1 = int((cx - w/2) * W); y1 = int((cy - h/2) * H)
        x2 = int((cx + w/2) * W); y2 = int((cy + h/2) * H)
        cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2)
    return img

Non-maximum suppression, worked out

After confidence filtering you still have several boxes on the same person. non-maximum suppression Keep the highest-scoring box, remove every remaining box that overlaps it beyond an IoU threshold, and repeat with what is left. cleans that up.

Four surviving detections:

# Box Coordinates (x1,y1,x2,y2)(x_1,y_1,x_2,y_2) Area Score target
1 A (100, 100, 200, 200) 10,000 0.92
2 B (110, 105, 205, 210) 9,975 0.85
3 C (400, 300, 480, 400) 8,000 0.78
4 D (105, 98, 198, 195) 9,021 0.61

Four detections before NMS. A, B and D are all on the same person.

Sort by score: A (0.92), B (0.85), C (0.78), D (0.61). Keep A. Now compute IoU against each remaining box.

A versus B. The intersection rectangle is the overlap of the two:

x1=max(100,110)=110,y1=max(100,105)=105x_1 = \max(100, 110) = 110, \quad y_1 = \max(100, 105) = 105 x2=min(200,205)=200,y2=min(200,210)=200x_2 = \min(200, 205) = 200, \quad y_2 = \min(200, 210) = 200 intersection=(200110)(200105)=90×95=8,550\text{intersection} = (200-110)(200-105) = 90 \times 95 = 8{,}550 union=10,000+9,9758,550=11,425\text{union} = 10{,}000 + 9{,}975 - 8{,}550 = 11{,}425 IoU=8,55011,425=0.748\text{IoU} = \frac{8{,}550}{11{,}425} = 0.748

0.748 is above the 0.5 threshold, so B is suppressed.

A versus C. max(100,400)=400\max(100,400) = 400 but min(200,480)=200\min(200,480) = 200, so x2<x1x_2 < x_1 and the intersection is empty. IoU = 0, C is kept.

A versus D.

intersection=(198105)(195100)=93×95=8,835\text{intersection} = (198-105)(195-100) = 93 \times 95 = 8{,}835 union=10,000+9,0218,835=10,186\text{union} = 10{,}000 + 9{,}021 - 8{,}835 = 10{,}186 IoU=8,83510,186=0.867    suppressed\text{IoU} = \frac{8{,}835}{10{,}186} = 0.867 \;\rightarrow\; \textbf{suppressed}

# Comparison Intersection Union IoU target Result
1 A vs B 8,550 11,425 0.748 suppress B
2 A vs C 0 18,000 0.000 keep C
3 A vs D 8,835 10,186 0.867 suppress D

NMS at IoU threshold 0.5. Final output: A and C — two people, correctly.

Set the NMS threshold too low and two workers standing shoulder to shoulder get merged into one detection. Set it too high and each worker keeps three boxes. For a crowded entrance, 0.6 to 0.7 is often better than the 0.45 default.

Training

from ultralytics import YOLO

model = YOLO("yolov8n.pt")          # nano, pretrained on COCO

model.train(
    data="site.yaml",
    epochs=100,
    imgsz=640,
    batch=16,
    patience=20,
    hsv_h=0.015, hsv_s=0.7, hsv_v=0.4,
    degrees=0.0,           # people are upright — do not rotate
    translate=0.1,
    scale=0.5,
    fliplr=0.5,
    mosaic=1.0,
    close_mosaic=10,       # turn mosaic off for the last 10 epochs
)

site.yaml:

path: /data/site
train: images/train
val: images/val
names:
  0: hardhat
  1: no_hardhat

Two settings deserve explanation. degrees=0.0 because people and hard hats are always upright, so rotation would create training images that cannot occur. And close_mosaic=10 disables mosaic augmentation for the final ten epochs — mosaic stitches four images together, which is great for variety but produces layouts unlike anything at inference, so the model finishes on realistic images.

mosaic augmentation Combining four training images into one, with boxes remapped. It multiplies object variety per batch and teaches the model to handle objects at frame edges.

Reading the results

# Class Images Instances Precision Recall mAP@50 target mAP@50-95
1 all 340 1204 0.874 0.812 0.869 0.591
2 hardhat 340 981 0.912 0.883 0.928 0.647
3 no_hardhat 340 223 0.836 0.741 0.810 0.535

Validation results. no_hardhat is worse on every metric — and it is the class that matters.

The class that triggers the alert is the weaker one, for a familiar reason: 223 instances against 981. It is also the class where a miss is expensive.

Adjust the threshold to see how it changes the confusion matrix and all derived metrics. Lower threshold → more positives (higher recall, lower precision).
Confusion Matrix
Metrics

Use the threshold slider above to watch precision and recall trade against each other. That trade-off is the next decision, and it should be made by cost.

Choosing the confidence threshold by cost

# Confidence no_hardhat recall no_hardhat precision Misses per 100 target False alarms per 100
1 0.15 0.94 0.52 6 87
2 0.25 0.89 0.68 11 42
3 0.40 0.81 0.79 19 22
4 0.55 0.71 0.88 29 10
5 0.70 0.58 0.94 42 4

Threshold sweep for no_hardhat. Pick by consequence, not by which row looks tidiest.

Now assign costs. A missed unprotected worker risks a head injury; a false alarm costs a supervisor thirty seconds to glance at a screenshot. Suppose a miss is worth 100 units and a false alarm 1 unit:

# Confidence Misses × 100 False alarms × 1 Total cost target
1 0.15 600 87 **687**
2 0.25 1100 42 1142
3 0.40 1900 22 1922
4 0.55 2900 10 2910
5 0.70 4200 4 4204

Expected cost at 100:1. The lowest threshold wins by a wide margin.

At that cost ratio, 0.15 is correct even though its precision is only 0.52 — half the alerts will be wrong, and that is the right trade. Change the ratio to 5:1 and the answer moves to 0.40. The threshold is a business decision that happens to be implemented in code.

Small objects are the hard part

A hard hat 30 metres from the camera might be 12 pixels across. The finest grid has stride 8, so the object spans one and a half cells and its features have been pooled away several times over.

Hard hat at 5 m: ~90 px (8×8)
0
0
0
0
0
0
0
0
0
0
1
1
0
0
0
0
0
1
1
1
1
0
0
0
0
1
1
1
1
0
0
0
0
0
1
1
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
Hard hat at 30 m: ~12 px (8×8)
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
1
0
0
0
0
0
0
0
1
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
# Object size Typical mAP@50 target Why
1 > 96 px (large) 0.91 plenty of detail at every scale
2 32–96 px (medium) 0.78 fine at strides 8 and 16
3 16–32 px (small) 0.54 only stride 8 helps
4 < 16 px (tiny) 0.21 below the useful resolution

Detection accuracy by object size. The drop below 32 pixels is steep and largely irreducible.

# Fix for small objects Cost Typical gain target
1 Raise imgsz from 640 to 1280 4× compute +0.12 mAP on small
2 Add a stride-4 detection head +30% compute +0.08
3 Tile the image and run on each tile 4–9× compute +0.18
4 Move the camera closer, or zoom in free if possible +0.30 or more
5 Train longer time +0.01

Small-object fixes ranked. The best one is not a software change.

The last two rows carry the lesson. Repositioning the camera so the far zone is 15 metres instead of 30 doubles every object’s pixel size and beats every algorithmic fix combined. Check the physical setup before optimising the model.

PropertyYOLOv8nYOLOv8mYOLOv8xRT-DETR-L
Parameters3.2M25.9M68.2M32M
COCO mAP@50-9537.350.253.953.0
GPU latency (T4)1.2 ms5.9 ms16.9 ms9.3 ms
CPU latency80 ms230 ms800 ms380 ms
Needs NMSyesyesyesno
Good forEdge, high FPSBalanced defaultOffline accuracyWhen NMS tuning hurts
Detector options. Start with the nano model to validate the pipeline, then scale up.

RT-DETR is worth knowing about: it predicts a fixed set of boxes directly and needs no NMS, which removes a whole class of tuning problems in crowded scenes.

Practice task

Use a public hard-hat dataset, or label 300 of your own images.

  1. Convert labels to YOLO format and draw them back onto 30 images. Fix whatever is wrong.
  2. Train YOLOv8n for 50 epochs. Record per-class precision, recall, and mAP@50.
  3. Sweep the confidence threshold over 0.1 to 0.8 and tabulate misses and false alarms per 100 objects.
  4. Assign a cost to each error type and compute expected cost per threshold. Which wins?
  5. Sweep the NMS IoU threshold over 0.3 to 0.8 on a crowded image. Where do people start merging?
  6. Split validation results by object size using COCO’s small/medium/large bands.
  7. Retrain at imgsz=1280 and compare small-object mAP against the compute cost.
  8. Pick 30 false negatives and look at them. Group them by cause.

Step 8 is where the real information is. You will usually find one dominant cause — heavy occlusion, motion blur, or one particular hat colour — and that group is worth more than any hyperparameter change.

Summary

A YOLO forward pass predicts at three strides — 80×80, 40×40 and 20×20 — for 8,400 candidate boxes. Each cell predicts an offset from itself: cell (12,7)(12,7) at stride 8 with offsets (0.35,0.62)(0.35, 0.62) decodes to centre (98.8,61.0)(98.8, 61.0).

Labels are normalised class cx cy w h. A box from (98,61)(98,61) to (210,305)(210,305) in a 640×640 image becomes 0.2406 0.2859 0.1750 0.3813. Always draw them back and look.

NMS keeps the top box and removes overlaps. You computed IoU 0.748 for A–B and 0.867 for A–D, both suppressed, and 0.0 for A–C, kept — two people from four boxes.

The confidence threshold is a cost decision. At 100:1 cost for a missed unprotected worker versus a false alarm, 0.15 beat 0.40 by 1,235 units of expected cost despite 52% precision.

And objects under 16 pixels sit around 0.21 mAP no matter what you do in software. Moving the camera closer beats every algorithmic fix.

What comes next

A box says roughly where an object is. Sometimes you need its exact outline — to measure area, to separate touching objects precisely, or to composite. Semantic and instance segmentation covers U-Net and Mask R-CNN, where the output is a class per pixel rather than a rectangle, and where the metrics shift from IoU on boxes to Dice and IoU on masks.

Test your understanding
Your detector trains without errors but mAP stays near 0.05 after 100 epochs. Loss decreases normally. What should you check first?
Test your understanding
Two workers standing shoulder to shoulder are consistently detected as a single box. Raising the confidence threshold does not help. What is the actual fix?

Frequently asked questions

How many labelled images do I need for detection?
Roughly 300 to 500 instances per class for a workable model when fine-tuning from COCO weights, and 1,500 or more per class for a solid one. Instances matter more than images, since one photo can contain ten labelled objects. Variety matters most of all: cover the lighting, distances, angles and occlusions you will actually see, because a detector generalises poorly to conditions absent from training.
What is the difference between mAP@50 and mAP@50-95?
mAP@50 counts a detection as correct at IoU 0.5, which means roughly the right place. mAP@50-95 averages over IoU thresholds from 0.5 to 0.95 in steps of 0.05, so it also rewards tight, accurate boxes. Use mAP@50 when approximate localisation is enough, as in counting or alerting, and mAP@50-95 when box precision matters, such as measurement or robotic grasping.
Should I use one model for everything or separate models per class?
One multi-class model, almost always. Classes share low-level features, so training them together is more data-efficient and gives you one thing to deploy, monitor and update. Split only when classes need genuinely different input resolutions, or when one class is so rare that it needs its own sampling strategy — and even then, a two-stage decomposition like detect-person-then-classify-hat usually works better than two separate detectors.
Why is my model good at detecting objects but the boxes are loose?
Loose boxes show up as decent mAP@50 with poor mAP@50-95. Common causes are inconsistent labelling — different annotators including different amounts of margin — and scale augmentation so aggressive that the model never learns precise extents. Check annotation consistency first by having two people label the same twenty images and comparing, since the model cannot be more precise than its labels.
How do I detect very small objects?
Fix the camera first: moving closer or zooming so objects exceed 30 pixels beats every software change combined. If the setup is fixed, raise the input resolution to 1280, add a stride-4 detection head, or tile the image and run detection on each tile before merging. All three cost significant compute, and none of them recovers detail that the sensor never captured.
Start typing to search across all content
navigate Enter open Esc close