Search…

Data Pipelines and Augmentation for Vision Models

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

The classifier in the last post started overfitting at epoch 10. The usual first response is augmentation — but a badly chosen augmentation makes things worse, and some quietly destroy your labels. This post covers which ones help, which ones lie, and how to keep the GPU fed.

Prerequisites: Your first image classifier.

What augmentation actually claims

augmentation Randomly transforming training images in ways that should not change their label, so the model sees more variety than you collected.

The definition contains the trap. Every augmentation is a claim: this change does not alter the correct answer. When the claim is true, you get free training data. When it is false, you are training the model on wrong labels.

# Augmentation The claim it makes True for recycling photos? target True for reading gauges?
1 Horizontal flip A mirrored object is the same object yes no — digits become mirrored
2 Vertical flip An upside-down object is the same object mostly no
3 Rotation ±15° A tilted object is the same object yes no — needle angle is the answer
4 Colour jitter Different lighting, same object yes depends
5 Hue shift Different colour, same object no — glass vs plastic uses tint no
6 Random crop 70–100% A partly visible object is the same object yes risky
7 Gaussian blur A slightly soft photo is the same object yes no if reading small text

The same augmentation is correct for one task and wrong for another. There is no universal list.

Look at the hue shift row. In the recycling classifier, the glass–plastic confusion was the dominant error, and the little colour tint each material gives is one of the few real cues. Shifting hue randomly deletes that cue and makes the worst confusion worse.

Watch a crop go wrong

RandomResizedCrop is the single most effective augmentation for classification and the easiest to misconfigure. Its scale parameter is the fraction of the original area kept.

PyTorch’s default is scale=(0.08, 1.0). On a 500×500 photo, 8% of the area is 0.08×250,000=20,0000.08 \times 250{,}000 = 20{,}000 px², a region of about 141×141 pixels, which is then upscaled to 224×224.

Original: bottle centred (8×8)
0
0
0
0
0
0
0
0
0
0
1
1
1
1
0
0
0
1
1
1
1
1
1
0
0
1
1
1
1
1
1
0
0
1
1
1
1
1
1
0
0
0
1
1
1
1
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
crop
scale 0.7: still clearly a bottle (4×4)
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
crop
scale 0.08: pure background, label says 'plastic' (2×2)
0
0
0
0

The third panel is a training example labelled “plastic” that contains no plastic. Feed the model enough of those and you teach it that empty background predicts plastic.

# scale range Minimum area kept Object usually visible? target Use for
1 (0.08, 1.0) 8% often not ImageNet-scale training, millions of images
2 (0.5, 1.0) 50% yes Small datasets — a safe default
3 (0.7, 1.0) 70% almost always Small objects, or fine detail matters
4 (0.9, 1.0) 90% always Barely augments — use with other transforms

Choosing scale. The aggressive default assumes you have millions of images to average the damage away.

The default works for ImageNet because with 1.2 million images, occasional garbage crops are diluted. With 840 training images they are a meaningful fraction of every epoch.

Measuring what each augmentation is worth

Do not guess. Add one at a time and measure against a fixed validation set.

# Configuration Train acc Val acc target Gap Change vs previous
1 No augmentation 99.8% 78.3% 21.5 baseline
2 + RandomResizedCrop(0.7, 1.0) 96.2% 84.1% 12.1 +5.8
3 + HorizontalFlip 95.4% 85.9% 9.5 +1.8
4 + ColorJitter(0.3, 0.3) 94.1% 88.2% 5.9 +2.3
5 + Rotation(±15°) 93.6% 88.7% 4.9 +0.5
6 + Hue shift(±0.1) 92.8% 86.4% 6.4 −2.3
7 + RandomErasing(p=0.25) 91.9% 89.4% 2.5 +0.7 (hue removed)

One augmentation at a time on the recycling dataset. Hue shift is the only one that hurts — and it hurts a lot.

Two things to read off that chart. Training accuracy should fall as you augment; if it stays at 99.8% your augmentation is doing nothing. And the hue-shift dip is real and reproducible, because it removes the tint cue the glass–plastic decision depends on.

Mixup and CutMix

These are stranger than they look, and they work.

mixup Blend two training images pixel by pixel with weight lambda, and blend their labels by the same lambda. The model learns that a 70/30 image mix should produce a 70/30 prediction.

With λ=0.7\lambda = 0.7, image A being cardboard and image B being glass:

x~=0.7xA+0.3xB\tilde{x} = 0.7 \, x_A + 0.3 \, x_B y~=0.7[1,0,0,0]+0.3[0,0,1,0]=[0.7,  0,  0.3,  0]\tilde{y} = 0.7 \, [1,0,0,0] + 0.3 \, [0,0,1,0] = [0.7,\; 0,\; 0.3,\; 0]

Trace one pixel. If A has red 200 and B has red 60 at the same position:

0.7(200)+0.3(60)=140+18=1580.7(200) + 0.3(60) = 140 + 18 = 158

Image A (cardboard) (1×4)
200
180
160
140
λ=0.7
Image B (glass) (1×4)
60
80
100
120
mix
0.7A + 0.3B → label [0.7, 0, 0.3, 0] (1×4)
158
150
142
134

Check the second column: 0.7(180)+0.3(80)=126+24=1500.7(180) + 0.3(80) = 126 + 24 = 150. ✓

CutMix does the same with a rectangle instead of a blend: paste a patch of B into A, and set λ\lambda to the fraction of area that stayed A. If the pasted patch covers 30% of the image, the label is 70% A and 30% B — the same arithmetic, but the result looks like a real image rather than a ghostly double exposure.

import numpy as np, torch

def mixup(x, y, alpha=0.4):
    lam = np.random.beta(alpha, alpha)
    idx = torch.randperm(x.size(0), device=x.device)
    return lam * x + (1 - lam) * x[idx], y, y[idx], lam

# in the training loop
xm, ya, yb, lam = mixup(x, y)
out = model(xm)
loss = lam * criterion(out, ya) + (1 - lam) * criterion(out, yb)
PropertyMixupCutMixRandomErasing
What it doesBlends two whole imagesPastes a patch of one into anotherBlanks a random rectangle
LabelsBlended by λBlended by patch areaUnchanged
Looks realisticNo — ghostlyYesYes
Helps most withOverconfidence, calibrationClassification and detectionOcclusion robustness
Epochs neededMore — converges slowerMoreSame
Small dataset (<2k)MarginalMarginalHelps
Large dataset (>50k)Clear gainClear gainModest gain
Mixup, CutMix and RandomErasing compared

The last two rows matter for small projects. Mixup and CutMix are strong regularisers that need extra epochs to pay off, and on a 1,200-image dataset the extra training time is usually better spent collecting more images.

Keeping the GPU fed

An augmented pipeline is CPU work: decode the JPEG, resize, transform, normalise. Do too little of it in parallel and the GPU sits waiting.

Measure the per-image CPU cost:

# Step Time per image target Runs on
1 JPEG decode (1500×1500) 4.0 ms CPU worker
2 Resize to 224 1.5 ms CPU worker
3 Augment (crop, flip, jitter) 1.6 ms CPU worker
4 ToTensor + Normalize 0.4 ms CPU worker
5 **Total per image** **7.5 ms**

Per-image CPU cost. One worker sustains 1000/7.5 = 133 images per second.

One worker delivers 133 img/s. The GPU can consume ResNet-18 forward and backward at roughly 380 img/s. So:

# num_workers Data supply (img/s) GPU capacity (img/s) Bottleneck GPU utilisation target
1 0 (main process) 133 380 data 35%
2 2 266 380 data 70%
3 3 399 380 GPU 100%
4 8 1064 380 GPU 100%
5 16 1064 380 GPU + memory pressure 100%, more RAM

Worker count versus GPU utilisation. Three workers saturate this GPU; sixteen just waste memory.

train_dl = DataLoader(
    train_ds,
    batch_size=32,
    shuffle=True,
    num_workers=8,
    pin_memory=True,           # faster host-to-GPU copies
    persistent_workers=True,   # don't respawn workers every epoch
    prefetch_factor=4,         # batches queued per worker
    drop_last=True,            # keeps batch shapes constant
)

persistent_workers=True is the cheapest win here. Without it, PyTorch tears down and restarts every worker process at the end of each epoch, which on a small dataset can cost more time than the epoch itself.

The visualization above contrasts a correct pipeline with a leaky one. The distinction it draws — fit preprocessing on training data only — is exactly the trap in the next section.

Statistics must come from training data only

If you compute your own normalization statistics instead of using ImageNet’s, compute them on the training split alone.

# Correct: training split only
mean, std, n = 0.0, 0.0, 0
for x, _ in DataLoader(train_ds_no_aug, batch_size=64):
    b = x.size(0)
    x = x.view(b, x.size(1), -1)
    mean += x.mean(2).sum(0)
    std  += x.std(2).sum(0)
    n    += b
mean, std = mean / n, std / n

Computing them over the full dataset leaks information about the test set into training. The effect is small for normalization statistics specifically, but the habit is what matters: anything fitted to data must be fitted to training data only, and that applies to class weights, PCA, and any learned resize or crop policy too.

# Fitted on the full dataset Leak severity target Correct approach
1 Normalization mean/std small but real Fit on train split
2 Class weights moderate Count train split only
3 PCA or whitening large Fit on train, apply to all
4 Selecting which images to keep severe Decide before splitting
5 Choosing augmentations by test score severe Use validation, touch test once

Common leaks in preprocessing, ranked

Test-time augmentation

At inference, run several augmented versions of each image and average the predictions.

@torch.no_grad()
def predict_tta(model, img):
    views = [img, torch.flip(img, dims=[3])]           # original + horizontal flip
    probs = [torch.softmax(model(v), dim=1) for v in views]
    return torch.stack(probs).mean(0)
# TTA setup Accuracy target Inference cost Worth it?
1 Single centre crop 89.4% baseline
2 + horizontal flip 90.1% usually yes
3 + 3 scales 90.6% offline batch only
4 + 5 crops × 2 flips 90.8% 10× competitions only

Test-time augmentation on the recycling model. Diminishing returns arrive fast.

Flip-only TTA is the sweet spot: 0.7 points for double the cost, which is fine for a batch job and usually fine for a phone app. Beyond that you pay 5× more compute for another 0.7 points.

Only use TTA transforms that are valid for your task. If horizontal flip would change the label — reading text, arrows, or gauges — flip TTA actively hurts.

Practice task

Use the classifier from the previous post, or any small dataset.

  1. Train with no augmentation. Record train and validation accuracy and the gap.
  2. Add RandomResizedCrop(224, scale=(0.7, 1.0)). Record the gap.
  3. Add horizontal flip. Then colour jitter. Then rotation. One at a time.
  4. Plot train and validation accuracy across all five runs.
  5. Now set scale=(0.08, 1.0) and save 20 augmented images to disk. Look at them. How many contain no object?
  6. Time one epoch at num_workers of 0, 2, 4, 8, and 16. Plot epoch time against workers.
  7. Add flip TTA at inference and measure the accuracy change.
  8. Add an augmentation you believe is wrong for your task — hue shift, or vertical flip if orientation matters. Measure how much it costs.

Step 5 is the one that changes behaviour permanently. Seeing your own training images cropped down to blank background, still carrying a confident class label, makes the scale parameter stop being an abstraction.

Summary

Every augmentation asserts that a change does not alter the label. Horizontal flip and mild crops are usually safe; hue shift cost 2.3 points on the recycling dataset because material tint was one of the few real cues separating glass from plastic.

RandomResizedCrop’s default scale=(0.08, 1.0) keeps as little as 8% of the area — about 141×141 pixels from a 500×500 photo — often producing a blank crop that still carries a class label. Use (0.5, 1.0) or (0.7, 1.0) on small datasets.

Add augmentations one at a time and watch the train–validation gap, which fell from 21.5 to 2.5 points here. Training accuracy falling is the intended effect, not a problem.

Mixup blends images and labels by the same λ\lambda: at λ=0.7\lambda = 0.7, pixel values 200 and 60 become 158, and the label becomes [0.7,0,0.3,0][0.7, 0, 0.3, 0]. Both mixup and CutMix need larger datasets and more epochs to pay off.

Finally, size the DataLoader from measurement. At 7.5 ms of CPU work per image, one worker supplies 133 img/s against a GPU that consumes 380, so three workers saturate it and sixteen only cost RAM.

What comes next

You have now trained and regularised a model while treating ResNet-18 as a black box. CNN architectures from LeNet to ResNet opens it up: what a convolution layer actually computes, why depth stalled at around 20 layers before residual connections, and how to read an architecture diagram well enough to choose between backbones on purpose.

Test your understanding
You train a model to read seven-segment digits from a meter. Validation accuracy sits at 71% despite a heavy augmentation pipeline copied from an ImageNet recipe. What is the most likely cause?
Test your understanding
Your GPU shows 35% utilisation during training and each epoch takes 6 minutes. num_workers is 0. What should you do first?

Frequently asked questions

How much augmentation is too much?
Watch the train-validation gap and the training accuracy together. A large gap with high training accuracy means you need more augmentation. Training accuracy that has fallen close to validation accuracy, with both mediocre, means you have gone too far and the model can no longer fit even the real signal. The sweet spot on most small datasets leaves a gap of two to five points.
Should I augment the validation set?
No. Validation must be deterministic so that a change in the score reflects a change in the model rather than random augmentation. Random validation augmentation makes the metric jump around and makes early stopping and checkpoint selection unreliable. The one exception is deliberate test-time augmentation at inference, which is a separate technique applied identically every time.
Is mixup worth using on a small dataset?
Usually not below about two thousand images. Mixup is a strong regulariser that needs extra epochs to converge, and on a small dataset the equivalent training time is better spent on collecting more images or on tuning simpler augmentations. It becomes clearly worthwhile in the tens of thousands of images, and it also improves probability calibration, which matters when you act on confidence rather than just the top class.
Should I augment on GPU instead of CPU?
Consider it when the CPU is the bottleneck and adding workers is not an option, for instance in a container with limited cores. Libraries such as Kornia and NVIDIA DALI run transforms on GPU, which removes the data-loading bottleneck but consumes GPU time that would otherwise go to training. Measure both ways. For most single-GPU setups, more CPU workers is simpler and equally effective.
How do I know if an augmentation is destroying my labels?
Save fifty augmented images to disk with their labels and look at them. If you cannot confidently assign the stated label yourself, neither can the model. This one habit catches the aggressive-crop problem, the flipped-digit problem, and the hue-shift problem in about five minutes, and it catches them before you have spent a week tuning around the damage.
Start typing to search across all content
navigate Enter open Esc close