YOLO Detection Pipeline: Data to Inference
In this series (30 parts)
- Computer Vision Roadmap: From Basics to Real Projects
- What is Computer Vision? From Pixels to Decisions
- A Short History of Computer Vision: 1545 to Now
- Math for CV Beginners: Vectors, Matrices, Convolutions
- Image Fundamentals: Color, Histograms, Noise, Filtering
- How Images and Video Are Stored: Colour, JPEG, Frames
- OpenCV Setup + First 10 Tasks in Python
- Vision Metrics: Accuracy, Precision, Recall, mAP, IoU
- CV Project Workflow: Dataset, Baseline, Error Iteration
- Intensity Transforms and Frequency-Domain Filtering
- Edge Detection and Thresholding That Actually Work
- Morphology, Contours, and Shape Analysis for Real Images
- Feature Matching (SIFT/ORB) and Image Stitching
- Camera Calibration and Perspective Correction
- Epipolar Geometry, Stereo Vision, and Depth Estimation
- Optical Flow and Motion Tracking in Video
- HOG, HOF and MBH: Descriptors Before Deep Learning
- Your First Image Classifier (PyTorch + Transfer Learning)
- Data Pipelines and Augmentation for Vision Models
- CNN Architectures Explained: From LeNet to ResNet
- YOLO Detection Pipeline: Data to Inference
- Semantic and Instance Segmentation: U-Net to Mask R-CNN
- Vision Transformers (ViT) and When to Use Them
- CV Explainability: Grad-CAM, Failures, Bias Checks
- 3D Vision Basics: SfM, Point Clouds, and Pose Estimation
- Multimodal Vision: CLIP, Embeddings, and Retrieval Systems
- Video Understanding: Flow Networks, Interpolation, Stabilisation
- Real-Time CV Systems: Tracking, Latency, Streaming
- Deploying CV Models: ONNX, TensorRT, Edge, and APIs
- 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.
graph TD A["640×640 image"] --> B["Backbone (CSPDarknet)"] B --> C["80×80 features<br/>stride 8"] B --> D["40×40 features<br/>stride 16"] B --> E["20×20 features<br/>stride 32"] C --> F["Neck: PANet<br/>fuses the three scales"] D --> F E --> F F --> G["Head: 8,400 predictions<br/>box + objectness + classes"] G --> H["Filter by confidence"] H --> I["Non-maximum suppression"] I --> J["Final boxes"]
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 on the 80×80 grid, which has stride 8. Suppose it predicts offsets , and sizes , px:
So the box centre is at pixel , and the corners are:
The negative 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 to in a 640×640 image:
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 | 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:
0.748 is above the 0.5 threshold, so B is suppressed.
A versus C. but , so and the intersection is empty. IoU = 0, C is kept.
A versus D.
| # | 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.
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.
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.
| # | 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.
| Property | YOLOv8n | YOLOv8m | YOLOv8x | RT-DETR-L |
|---|---|---|---|---|
| Parameters | 3.2M | 25.9M | 68.2M | 32M |
| COCO mAP@50-95 | 37.3 | 50.2 | 53.9 | 53.0 |
| GPU latency (T4) | 1.2 ms | 5.9 ms | 16.9 ms | 9.3 ms |
| CPU latency | 80 ms | 230 ms | 800 ms | 380 ms |
| Needs NMS | yes | yes | yes | no |
| Good for | Edge, high FPS | Balanced default | Offline accuracy | When NMS tuning hurts |
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.
- You need to count objects or know where they are
- Several objects appear per image
- A bounding box is precise enough — you do not need exact outlines
- Objects are larger than roughly 20 pixels in your frames
- One object per image and position is irrelevant — classification is simpler
- You need exact pixel outlines or area — that is segmentation
- Objects are under about 15 pixels — fix the camera first
- You have fewer than a few hundred labelled boxes per class
Practice task
Use a public hard-hat dataset, or label 300 of your own images.
- Convert labels to YOLO format and draw them back onto 30 images. Fix whatever is wrong.
- Train YOLOv8n for 50 epochs. Record per-class precision, recall, and mAP@50.
- Sweep the confidence threshold over 0.1 to 0.8 and tabulate misses and false alarms per 100 objects.
- Assign a cost to each error type and compute expected cost per threshold. Which wins?
- Sweep the NMS IoU threshold over 0.3 to 0.8 on a crowded image. Where do people start merging?
- Split validation results by object size using COCO’s small/medium/large bands.
- Retrain at
imgsz=1280and compare small-object mAP against the compute cost. - 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 at stride 8 with offsets decodes to centre .
Labels are normalised class cx cy w h. A box from to 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.
- Three strides give 6400 + 1600 + 400 = 8,400 candidate boxes per image.
- Cells predict offsets from themselves: (12 + 0.35) × 8 = 98.8.
- YOLO labels are normalised centre-based cx cy w h — not corners, not pixels.
- Always draw converted labels back onto images. Format bugs produce no error message.
- NMS: keep the top box, suppress anything overlapping it above the IoU threshold, repeat.
- Raise the NMS threshold in crowded scenes or adjacent people get merged into one box.
- Disable mosaic for the final 10 to 15 epochs so the model calibrates to real images.
- Set the confidence threshold from the cost of each error, never from the default.
- Objects under 16 pixels are around 0.21 mAP — the fix is camera placement, not software.
- Group your false negatives by cause; one group usually dominates and points at the real 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.