Search…

CV Project Workflow: Dataset, Baseline, Error Iteration

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

Most computer vision projects do not fail on the model. They fail because nobody wrote down what “working” means, the split leaked, or three months went into tuning a model when the real problem was 31 night-time photos. This post is the loop that avoids all three.

Prerequisites: Vision metrics, because every step here depends on being able to measure honestly.

The project we will run through

A scooter-sharing company has a problem. Riders park scooters badly: blocking pavements, lying on their side, or left on private property. The app asks every rider to photograph the scooter at the end of the trip. The company wants those photos checked automatically.

That is the whole brief. It is not enough to start on, and turning it into something buildable is step one.

The loop between steps 4, 5, and 6 is the project. Everything else happens once.


Step 0: write the spec

Before any code, answer five questions in writing. If you cannot, you do not have a project yet.

# Question Answer for the scooter project target
1 What exact decision does this make? Flag the photo for human review, or accept it
2 What does a false positive cost? $0.40 of reviewer time, plus rider annoyance
3 What does a false negative cost? $12 average — complaints, fines, retrieval trips
4 What must it beat to be worth shipping? Current manual review of 100% of photos, at $0.40 each
5 What is the target? Review under 20% of photos while catching 85% of bad parks

The five questions. Answer them in a document, not in your head.

That last row is the actual goal, and notice it is two numbers, not one. “High accuracy” would have been useless. “Recall ≥ 0.85 at a review rate ≤ 0.20” is something you can test against.

The cost asymmetry is already telling you something. A miss costs 30 times a false alarm, so this project should be tuned for recall. If someone later proposes optimising F1, you have the numbers to explain why not.


Step 1: look at 50 images by hand

Open fifty photos, one at a time, and write down what you see. Not a script. Your eyes. This takes forty minutes and it changes the project.

Here is what that audit produced:

# Observation Count in 50 target Consequence
1 Photo taken at night or in poor light 14 A colour-based rule will not survive
2 Scooter partly hidden by a car or a bin 9 Whole-object rules will fail here
3 Scooter smaller than 8% of the frame 7 Small-object handling matters
4 Photo does not contain a scooter at all 4 Needs a 'no scooter' outcome
5 Motion blur or a finger over the lens 3 Needs a quality gate before anything else
6 Correctly parked but on a slope, looks tilted 5 The 'tilted' rule is ambiguous
7 Clean, well-lit, obvious 8 Only 16% of photos are the easy case

Manual audit of 50 randomly sampled photos

Two findings change the plan immediately.

Only 8 of 50 photos are the clean case. Any demo built on hand-picked good images will mislead everyone about how hard this is.

Five photos are genuinely ambiguous. A scooter on a slope leans, but it is parked correctly. Until you decide how to label those, two labellers will disagree and your ground truth will be noise. Write the rule down: “tilt is measured relative to the ground plane, not the image frame; a scooter upright on its stand is correct regardless of slope.”

labelling guideline A written document giving the exact rule for every ambiguous case, with example images. Without one, different labellers produce contradictory ground truth and no model can fit it. is the most undervalued artifact in vision projects. Write it during this audit, while the ambiguous cases are in front of you.


Step 2: split the data without leaking

data leakage When information from the validation or test set influences training, making the reported score better than what production will deliver. is the most common reason a model that scored 0.94 in testing scores 0.71 in production.

The obvious split is wrong here:

# WRONG for this project
train, temp = train_test_split(all_photos, test_size=0.3, random_state=42)
val, test = train_test_split(temp, test_size=0.5, random_state=42)

Riders often photograph the same scooter at the same docking spot several times in a week. A random split puts near-identical photos in both train and validation. The model memorises that background and reports a great score for recognising a location, not a parking violation.

Split by something the model cannot memorise:

import pandas as pd
import numpy as np

df = pd.read_csv("photos.csv")     # columns: path, label, location_id, date

locations = df["location_id"].unique()
rng = np.random.default_rng(42)
rng.shuffle(locations)

n = len(locations)
train_loc = set(locations[: int(0.70 * n)])
val_loc   = set(locations[int(0.70 * n) : int(0.85 * n)])
test_loc  = set(locations[int(0.85 * n) :])

train = df[df.location_id.isin(train_loc)]
val   = df[df.location_id.isin(val_loc)]
test  = df[df.location_id.isin(test_loc)]

assert not (set(train.location_id) & set(val.location_id))
assert not (set(train.location_id) & set(test.location_id))
print(len(train), len(val), len(test))

The two assert lines are worth keeping permanently. Leakage is silent, and an assertion is the only thing that makes it loud.

# Split by Leaks when Use for
1 Random image Near-duplicate images exist Almost never in vision
2 Capture session One session spans many conditions Video frames, burst photos
3 Location or camera Locations share backgrounds Fixed-camera and geo-tagged data
4 Date Conditions drift over time Anything deployed over months
5 Patient / subject / vehicle The same subject appears repeatedly Medical, biometric, fleet data

Choose the split key by asking what the model could memorise instead of learning

Watch leakage happen

Toggle between the correct and leaky pipeline. The leaky version computes normalization statistics before splitting, so validation information reaches the training data. It is the same class of mistake as splitting randomly across sessions: a small ordering error that inflates every number downstream.


Step 3: build a stupid baseline

Before any model, build the cheapest thing that produces an answer. Three levels, in order:

Level 0 — the constant. Always predict the majority class. If 22% of photos are bad parks, always predicting “fine” gives 78% accuracy and 0 recall. This is the number every future result must beat, and it is one line of code.

Level 1 — one hand-made feature. Bad parks are often scooters lying down, which are wider than they are tall. Find the largest contour, measure its aspect ratio, threshold it. An hour of work using thresholding and contours.

import cv2, numpy as np

def looks_fallen(path, ratio_thresh=1.6):
    img = cv2.imread(path)
    if img is None:
        return None
    img = cv2.resize(img, (640, 480))
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    gray = cv2.GaussianBlur(gray, (0, 0), 1.5)
    _, mask = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
    cnts, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    if not cnts:
        return False
    x, y, w, h = cv2.boundingRect(max(cnts, key=cv2.contourArea))
    return (w / h) > ratio_thresh

Level 2 — a small pretrained model. Fine-tune a ResNet-18 for twenty minutes, covered in your first image classifier.

Measured on the validation split:

# Baseline Time spent Recall target Review rate Meets target?
1 Always 'fine' 1 min 0 0 no
2 Always 'flag' 1 min 1 1 no, reviews everything
3 Aspect-ratio rule 1 hour 0.41 0.29 no
4 ResNet-18 fine-tuned 3 hours 0.62 0.24 no, but closest

Baselines against the target of recall ≥ 0.85 at review rate ≤ 0.20

Nothing is inside the target region yet. But the baselines were cheap, they gave a real number to improve on, and the aspect-ratio rule at 0.41 recall told you something important: a third of bad parks really are just fallen scooters, and simple geometry finds them.


Step 4 and 5: measure, then bucket the errors

The ResNet-18 baseline got recall 0.62 on 200 validation photos containing bad parks. That means 76 misses. The instinct is to train longer or try a bigger model. Do neither yet. Open all 76 and sort them into buckets.

# Error bucket Count target Share of errors Recall if fixed completely
1 Night or very low light 31 41% 0.775
2 Scooter occluded by a car or bin 18 24% 0.71
3 Scooter is tiny in the frame 12 16% 0.68
4 Ground-truth label is wrong 9 12% 0.665
5 Motion blur 4 5% 0.64
6 Genuinely unclear even to a human 2 3% 0.63

All 76 validation misses, bucketed by cause

Work out that last column for the night bucket. There are 200 positives and the model currently gets 0.62×200=1240.62 \times 200 = 124 of them. Fixing all 31 night errors gives:

recall=124+31200=155200=0.775\text{recall} = \frac{124 + 31}{200} = \frac{155}{200} = 0.775

Error counts per bucket:

Now the picture is completely different from “the model needs to be better”. Night photos alone are 41% of every failure. And the audit in step 1 already told you 14 of 50 photos are taken at night, which is 28% of all traffic. The model was never given enough night data to learn from.

Two buckets deserve special attention

The 9 wrong labels. These are cases where the model was right and the ground truth was wrong. That is 4.5% of the validation positives, which caps your measurable recall at roughly 0.955 no matter what you build. It also means the training set has similar noise, so the model is being actively taught wrong answers. Fixing labels is usually the cheapest available improvement.

The 2 genuinely unclear cases. These define the ceiling. If a careful human cannot decide, no model will. Take them out of the metric or accept them as permanent loss, but do not spend a week on them.

Choose the fix by gain per unit of effort

# Fix Effort Recall gain Gain per day of work target
1 Collect + label 600 night photos 3 days +0.11 0.037
2 Re-label the 9 wrong + audit the training set 1 day +0.04 0.04
3 Add brightness/gamma augmentation 2 hours +0.05 0.2
4 Swap ResNet-18 for ResNet-50 1 day +0.02 0.02
5 Train 3x longer 1 day of compute +0.01 0.01
6 Build an occlusion-aware detector 2 weeks +0.06 0.006

Ranking fixes by return, not by how interesting they are

The two-hour augmentation change returns ten times more per day than the bigger model, and twenty times more than the two-week detector rebuild. It is also the least intellectually exciting item on the list, which is exactly why teams skip it. Do it first. Augmentation strategy is covered properly in vision data pipelines and augmentation.


Step 6: iterate, and keep a log

Run the loop again after each change. Record every experiment, including the failures, in one table.

# # Change Recall target Review rate Kept?
1 1 ResNet-18 baseline 0.62 0.24 baseline
2 2 + brightness and gamma augmentation 0.67 0.25 yes
3 3 + re-labelled 40 wrong ground truths 0.71 0.23 yes
4 4 + 600 night photos added to training 0.82 0.22 yes
5 5 ResNet-50 instead of ResNet-18 0.83 0.22 no, not worth 4x latency
6 6 + random occlusion augmentation 0.86 0.21 yes
7 7 threshold lowered 0.50 to 0.44 0.89 0.19 yes, target met

The experiment log. Row 5 is a failure, and recording it stops someone repeating it.

The biggest single jump is experiment 4: adding night photos, worth +0.11. That was the top error bucket. The model architecture change in experiment 5 was worth +0.01 and quadrupled inference cost, so it was dropped.

Touch the test set exactly once

The validation set was used seven times. It is now partly tuned-to, and its 0.89 is slightly optimistic. That is fine and expected — that is what a validation set is for.

The test set is different. Run it once, at the end, and report that number. If it comes out at 0.84 against a 0.85 target, resist the urge to make one more change and re-run. The moment you do, the test set becomes a second validation set and you no longer have an honest estimate of anything.

Training setValidation setTest set
Used forFitting weightsChoosing between optionsOne final honest number
How often you lookConstantlyAfter every experimentOnce, at the end
Can you tune on it?YesYes, that is its jobNo, ever
Typical share70%15%15%
If you overuse itOverfitting, visible in valVal score drifts optimisticYou lose your only honest measure
Three splits, three different jobs

What this looks like in production

Shipping is not the end of the loop. It is the loop running with new inputs.

Logging the raw score is the key habit, because it needs no labels. Accuracy needs ground truth you rarely have in production, but the distribution of scores is free. When it shifts, you know something changed before any complaint reaches you.


Practice task

Run the loop on something small enough to finish in a weekend.

  1. Collect 150 photos of two categories with your phone, deliberately including hard cases: bad light, partial occlusion, odd angles.
  2. Write a one-page labelling guideline covering your three most ambiguous cases.
  3. Split by capture session, not randomly. Add the assertion lines.
  4. Build all three baselines: constant, one hand-made feature, and a fine-tuned model.
  5. Open every validation error and bucket them. You will need four to eight buckets.
  6. Compute the recall you would reach if each bucket were fully fixed.
  7. Do the highest gain-per-effort fix, re-measure, and log it.
  8. Repeat twice more, then run the test set exactly once.

The step people skip is 5. It is also the only one that tells you what to do next, and it takes about twenty minutes.

Summary

The workflow is a loop with a fixed order and one rule: never guess what to fix.

Write the spec first, including the cost of each mistake and a numeric target with two numbers in it. Look at fifty images by hand before writing code, and write the labelling guideline while the ambiguous cases are in front of you. Split by session, location, or date so the model cannot memorise a background, and assert that the splits do not overlap. Build the cheapest baseline that produces an answer, so every later result has something to beat.

Then measure, open every error, bucket them, and count. In the example, one bucket held 41% of all failures and a two-hour augmentation change returned ten times more per day of effort than swapping in a bigger model. Log every experiment, including the ones that failed. Touch the test set once.

What comes next

You have the loop. The next posts fill in the techniques it calls for. Edge detection and thresholding and morphology and contours give you strong classical baselines. Your first image classifier and vision data pipelines and augmentation cover the learned side, including the augmentation that gave the best return in the example above.

Test your understanding
You are building a defect detector using video from four factory cameras, and you split frames randomly into train and validation. Validation accuracy is 0.97, but the first day in production it drops to 0.74. What is the most likely cause?
Test your understanding
Your model has recall 0.62. Bucketing 76 errors shows 31 are night photos, 18 are occlusions, and 9 are wrong ground-truth labels. You have three days. What gives the best return?

Frequently asked questions

How many images do I need to start a computer vision project?
For a fine-tuned classifier on a clear two-class problem, 200 to 500 well-chosen images per class is often enough to get a useful baseline. The composition matters far more than the count: 300 images covering night, occlusion, and odd angles will beat 3,000 clean daylight photos. Start small, bucket the errors, and let the error analysis tell you exactly which images to collect next.
Should I always split randomly?
No, and in vision the answer is usually no. Ask what the model could memorise instead of learning. If images come from the same camera, session, location, patient, or vehicle, split on that key. A random split across near-duplicates is the single most common reason a validation score fails to reproduce in production.
When is a baseline good enough to ship?
When it meets the numeric target you wrote in step 0 and its remaining errors cost less to live with than to fix. That is a business decision with numbers behind it, not a modelling one. Plenty of shipped systems run on a threshold rule because the classical baseline hit the target in an afternoon.
How do I know whether to fix the data or the model?
Bucket the errors. If the top bucket is a condition that is rare or absent in your training data, such as night photos or a new product variant, it is a data problem and no architecture change will fix it. If the model performs poorly on conditions that are well represented in training, that points at capacity, optimisation, or the loss function. In practice the top bucket is a data problem far more often than not.
How often should I retrain after deploying?
Let the data decide rather than the calendar. Log raw scores continuously and watch the distribution; a shift means conditions changed. Also hand-label a fresh random sample of about 100 production images each month and measure against it. Retrain when that measurement drops below your target, or when the audit reveals a new condition your training set never covered.
Start typing to search across all content
navigate Enter open Esc close