Vision Metrics: Accuracy, Precision, Recall, mAP, IoU
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
A model that is 98% accurate can be completely useless. This post shows exactly how that happens, then works out every metric you need by hand on real numbers, so you know what each one is hiding.
Prerequisites: OpenCV setup and first 10 tasks. The general ideas here overlap with evaluation metrics in machine learning, and this post adds the vision-specific ones: IoU, mAP, and Dice.
The 98% model that gets someone fired
A factory inspects circuit boards. Out of 1,000 boards, 30 have a defect. A model is trained to flag defects.
Here is a model that scores 97% accuracy without doing anything at all:
def predict(board_image):
return "OK" # always
It is right on all 970 good boards and wrong on all 30 defective ones. Accuracy is . It catches zero defects. Every faulty board ships.
Now here is a real model’s output on the same 1,000 boards:
| Predicted: defect | Predicted: OK | |
|---|---|---|
| Actually defect | TP = 18 | FN = 12 |
| Actually OK | FP = 7 | TN = 963 |
Confusion matrix, 1,000 boards
This table is where every metric comes from. Read the four cells:
- TP = 18: defects the model caught.
- FN = 12: defects it missed. These ship to customers.
- FP = 7: good boards it wrongly flagged. A person wastes time re-checking them.
- TN = 963: good boards correctly passed.
Now compute every metric by hand
Accuracy — of all predictions, how many were right?
98.1%. Better than the do-nothing model’s 97%, by 1.1 points. That tiny gap is the problem: accuracy barely notices the difference between a model that catches 18 defects and one that catches none.
Precision — when it says defect, how often is it right?
Of 25 flagged boards, 18 really were defective. 7 people wasted their time.
Recall — of all real defects, how many did it find?
It found 60% of the defects. 12 faulty boards shipped. That number is what the factory manager cares about, and accuracy never showed it.
F1 — the harmonic mean, used when you want one number balancing both:
Note how far F1 (0.655) is from accuracy (0.981). They are describing the same model.
| # | Model | Accuracy | Precision | Recall | F1 | Defects shipped target |
|---|---|---|---|---|---|---|
| 1 | Always say OK | 0.97 | undefined | 0 | 0 | 30 |
| 2 | Real model | 0.981 | 0.72 | 0.6 | 0.655 | 12 |
| 3 | Always say defect | 0.03 | 0.03 | 1 | 0.058 | 0 |
Accuracy separates these three models by 5 points. Recall separates them completely.
Which of precision and recall matters more
They pull against each other. Catching more defects means flagging more boards, which means more false alarms. The right balance comes from the cost of each mistake, not from the maths.
| # | Application | Cost of a false positive | Cost of a false negative | Optimise for target |
|---|---|---|---|---|
| 1 | Cancer screening | One extra scan | A missed tumour | Recall |
| 2 | Factory defect check | Re-inspect a good part | A faulty part ships | Recall |
| 3 | Spam-image filter | A real photo is hidden | One spam gets through | Precision |
| 4 | Face unlock | Owner has to retry | A stranger unlocks the phone | Precision |
| 5 | Wildlife camera trigger | Some empty frames stored | A rare animal missed | Recall |
| 6 | Autonomous braking | Unnecessary hard brake | A collision | Recall, heavily |
The metric follows from which mistake hurts more
graph TD
Q{"Which mistake<br/>costs more?"} -->|"missing a real one"| R["Optimise recall<br/>lower the threshold"]
Q -->|"a false alarm"| P["Optimise precision<br/>raise the threshold"]
Q -->|"about equal"| F["Optimise F1<br/>or pick by F-beta"]
R --> T["Then report precision<br/>so the alarm load is visible"]
P --> U["Then report recall<br/>so the misses are visible"]
F --> V["Always report both<br/>plus the class balance"]
When the two costs are unequal but you still want one number, use , which weights recall times as much as precision:
With (recall matters four times as much), using our numbers:
(0.621) sits closer to recall (0.60) than F1 (0.655) does, which is exactly the intent.
The threshold moves everything
The model does not output “defect” or “OK”. It outputs a score between 0 and 1. You choose where to cut.
| # | Threshold | TP | FP | FN | Precision | Recall | F1 target |
|---|---|---|---|---|---|---|---|
| 1 | 0.1 | 29 | 210 | 1 | 0.121 | 0.967 | 0.215 |
| 2 | 0.3 | 26 | 74 | 4 | 0.26 | 0.867 | 0.4 |
| 3 | 0.5 | 18 | 7 | 12 | 0.72 | 0.6 | 0.655 |
| 4 | 0.7 | 12 | 2 | 18 | 0.857 | 0.4 | 0.545 |
| 5 | 0.9 | 5 | 0 | 25 | 1 | 0.167 | 0.286 |
One model, five thresholds. The model never changed.
At threshold 0.10 the model catches 29 of 30 defects, but raises 210 false alarms — the inspectors would ignore it within a week. At 0.90 every alarm is genuine, but 25 defects ship. The factory has to decide which failure it can live with. No metric makes that decision for you.
Feel the tradeoff yourself
Drag the threshold and watch all four numbers move together. Notice that precision and recall never rise at the same time. That is not a limitation of any particular model; it is what a threshold does.
IoU: the metric that only exists in vision
Classification has one question: is the label right? Detection has two: is the label right, and is the box in the right place? IoU Intersection over Union. The overlapping area between two boxes divided by the total area they cover together. 1.0 means identical, 0 means no overlap. answers the second.
Work one out completely
The model predicts a box at . The ground truth box is .
Step 1: find the overlapping rectangle. Take the larger of the two left edges and the smaller of the two right edges:
Step 2: overlap area.
Step 3: each box’s own area.
Step 4: union. Add the two areas and subtract the overlap once, because otherwise it is counted twice:
Step 5: IoU.
| Quantity | How it is built | Value |
|---|---|---|
| Overlap (intersection) | 70 × 80 | 5,600 |
| Predicted box area | 100 × 100 | 10,000 |
| Ground-truth box area | 100 × 110 | 11,000 |
| Union | 10,000 + 11,000 − 5,600 | 15,400 |
| IoU | 5,600 ÷ 15,400 | 0.364 |
The five numbers in one place. The overlap is subtracted from the union because adding the two areas counts it twice.
0.364 is below the usual cutoff of 0.5, so this detection counts as a false positive even though it found the right object with the right label. It was simply not placed accurately enough.
def iou(box_a, box_b):
ax1, ay1, ax2, ay2 = box_a
bx1, by1, bx2, by2 = box_b
ix1, iy1 = max(ax1, bx1), max(ay1, by1)
ix2, iy2 = min(ax2, bx2), min(ay2, by2)
iw, ih = max(0, ix2 - ix1), max(0, iy2 - iy1)
inter = iw * ih
if inter == 0:
return 0.0
area_a = (ax2 - ax1) * (ay2 - ay1)
area_b = (bx2 - bx1) * (by2 - by1)
return inter / (area_a + area_b - inter)
print(iou((50, 40, 150, 140), (80, 60, 180, 170))) # 0.36363...
The max(0, ...) on the width and height is essential. Without it, two non-overlapping boxes give negative width times negative height, which is a positive area and a completely wrong IoU.
What IoU values look like
| # | IoU | What it looks like | Verdict at 0.5 threshold target |
|---|---|---|---|
| 1 | 0.95 | Almost exactly on the object | Counts, excellent |
| 2 | 0.75 | Slightly loose or shifted | Counts, good |
| 3 | 0.50 | Half overlap, noticeably off | Just counts |
| 4 | 0.36 | Found it, but badly placed | Does not count |
| 5 | 0.10 | Clipping a corner | Does not count |
| 6 | 0.00 | No overlap at all | Does not count |
Reading IoU values in practice
The IoU threshold changes your score dramatically. mAP@0.5 is generous: any roughly correct box passes. mAP@0.5:0.95 averages over ten thresholds from 0.5 to 0.95 in steps of 0.05, and is much stricter because it rewards precise placement. The same model routinely scores 0.62 under the first and 0.41 under the second. Two papers reporting “mAP 0.62” may not be comparable at all if they used different thresholds.
Average Precision, worked out step by step
Average Precision The area under the precision-recall curve for one class. It summarises performance across every possible threshold in a single number. is the standard detection metric, and it is much less mysterious once you compute one.
Suppose a test set contains 5 real objects of one class. The model returns 7 detections. Sort them by confidence, highest first, and mark each as a true or false positive using IoU ≥ 0.5:
| # | Rank | Confidence | Best IoU | TP or FP target |
|---|---|---|---|---|
| 1 | 1 | 0.95 | 0.88 | TP |
| 2 | 2 | 0.91 | 0.72 | TP |
| 3 | 3 | 0.85 | 0.31 | FP |
| 4 | 4 | 0.78 | 0.65 | TP |
| 5 | 5 | 0.72 | 0.44 | FP |
| 6 | 6 | 0.61 | 0.58 | TP |
| 7 | 7 | 0.55 | 0.12 | FP |
7 detections ranked by confidence, against 5 ground-truth objects
Now walk down the list, keeping running totals. At each row, precision is so far, and recall is .
| Rank | TP so far | FP so far | Precision | Recall |
|---|---|---|---|---|
| 1 | 1 | 0 | 1/1 = 1.000 | 1/5 = 0.20 |
| 2 | 2 | 0 | 2/2 = 1.000 | 2/5 = 0.40 |
| 3 | 2 | 1 | 2/3 = 0.667 | 0.40 |
| 4 | 3 | 1 | 3/4 = 0.750 | 3/5 = 0.60 |
| 5 | 3 | 2 | 3/5 = 0.600 | 0.60 |
| 6 | 4 | 2 | 4/6 = 0.667 | 4/5 = 0.80 |
| 7 | 4 | 3 | 4/7 = 0.571 | 0.80 |
Recall stops at 0.80, because one of the five objects was never detected at any confidence.
The raw curve zigzags, because each false positive drops precision and each true positive lifts it. AP uses the interpolated version: at every recall level, take the highest precision achieved at that recall or any higher one.
Then AP is the area under that step function. Each recall step is wide:
mAP is just AP averaged over all classes:
| # | Class | Objects in test set | AP@0.5 target |
|---|---|---|---|
| 1 | person | 1420 | 0.81 |
| 2 | car | 890 | 0.76 |
| 3 | bicycle | 210 | 0.54 |
| 4 | traffic light | 95 | 0.38 |
Per-class AP. mAP = (0.81 + 0.76 + 0.54 + 0.38) / 4 = 0.62
That single 0.62 hides the real story: traffic lights are at 0.38 while people are at 0.81. If your product is about traffic lights, mAP told you nothing useful. Always look at the per-class table, not just the mean. Notice too that the weakest classes have the fewest examples, which points straight at what to collect next.
Segmentation: IoU and Dice
Segmentation predicts a class per pixel, so a bounding box is not enough. The same IoU idea applies, counting pixels instead of area.
Suppose for one image: the predicted mask has 4,200 pixels, the ground truth mask has 3,800, and 3,100 pixels are in both.
IoU (also called the Jaccard index):
Dice coefficient (also called F1 for pixels):
They measure the same overlap and are linked exactly:
| # | IoU | Dice target | Difference |
|---|---|---|---|
| 1 | 0.1 | 0.182 | +0.082 |
| 2 | 0.3 | 0.462 | +0.162 |
| 3 | 0.5 | 0.667 | +0.167 |
| 4 | 0.7 | 0.824 | +0.124 |
| 5 | 0.9 | 0.947 | +0.047 |
Dice is always higher than IoU, and the gap is largest in the middle
Dice always looks better. That is why medical imaging papers usually report Dice and object-detection papers usually report IoU. Neither is wrong, but you cannot compare a Dice number against an IoU number.
graph TD A["Segmentation output"] --> B["Per-image IoU or Dice"] B --> C["Average over images<br/>= dataset score"] B --> D["Average over classes<br/>= mIoU"] C --> E["Beware: large objects<br/>dominate the average"] D --> F["Beware: rare classes<br/>get equal weight"]
The two averaging choices give different numbers and answer different questions. Averaging over images weights big objects heavily, because they contribute more pixels. Averaging per class first (mIoU) gives a rare class the same weight as a common one. Say which one you used.
Choosing a metric
| Classification | Detection | Segmentation | |
|---|---|---|---|
| Primary metric | Precision, recall, F1 | mAP@0.5 and mAP@0.5:0.95 | mIoU or Dice |
| Localisation measured? | No | Yes, via IoU | Yes, per pixel |
| Threshold-free option | ROC AUC / PR AUC | AP (already averaged) | Usually fixed at 0.5 |
| Main trap | Accuracy on imbalanced data | Comparing across IoU thresholds | Mixing up Dice and IoU |
| Always also report | Class balance | Per-class AP table | Per-class IoU, and rare-class scores |
- You report the class balance alongside every metric
- You state the IoU threshold explicitly for any detection number
- You show a per-class breakdown, not just the mean
- The threshold was chosen on validation data and frozen before the test run
- You include a few example failures, not just the numbers
- Accuracy is the only number and the classes are imbalanced
- The threshold was picked after seeing the test results
- Detection numbers appear with no IoU threshold stated
- A mean hides one class scoring near zero
A metric that beats all of these
For a working product, translate the confusion matrix into money or time. Using the board example with a false negative costing 5 of inspector time:
Now compare against the threshold-0.30 row from earlier (FN = 4, FP = 74):
The lower threshold has worse precision, worse F1, and less than half the cost. F1 said threshold 0.5 was better. The business says otherwise, and the business is right.
def expected_cost(tp, fp, fn, cost_fp=5, cost_fn=200):
return fp * cost_fp + fn * cost_fn
for thr, tp, fp, fn in [(0.1,29,210,1), (0.3,26,74,4), (0.5,18,7,12),
(0.7,12,2,18), (0.9,5,0,25)]:
print(f"thr={thr} cost=${expected_cost(tp, fp, fn):,}")
Run it and you will find the minimum sits at 0.30, not at the F1 optimum. Whenever you can put a number on each mistake, do it. It ends most arguments about which threshold to use.
Practice task
Take the coin counter you built in the OpenCV tasks post and evaluate it properly.
- Label 30 photos by hand with the true coin count.
- Run your counter and record predicted counts.
- Build a confusion matrix at the object level: a coin found is a TP, a coin missed is an FN, an extra blob is an FP.
- Compute precision, recall, and F1 by hand for at least one image before trusting any library.
- Sweep your area threshold from 100 to 2,000 in steps of 100 and plot precision and recall against it.
- Assign a cost to each error type and find the area threshold that minimises total cost.
Step 5 is the one that teaches the most. You will see precision and recall cross somewhere, and that crossing point is almost never where you would have guessed.
Summary
Accuracy is the metric to distrust first. On a problem with 3% positives it rates a do-nothing model at 97%, and it moved by only 1.1 points between a model that catches nothing and one that catches 18 of 30 defects.
Precision and recall separate the two ways of being wrong, and the threshold moves you along the curve between them. IoU adds the vision-specific question of whether the box or mask is in the right place, and you computed one at 0.364 that looked correct but failed the standard 0.5 gate. Average Precision folds the whole precision-recall curve into one number, and you worked one out to 0.683 from a ranked list of seven detections. mAP averages that across classes and hides per-class disasters, so always read the breakdown. Dice and IoU measure the same thing and are related by , with Dice always the friendlier figure.
When you can price the mistakes, expected cost beats all of them.
- Never report accuracy alone on imbalanced data. Publish the class balance next to it.
- Precision is about false alarms. Recall is about misses. Pick which one by asking which mistake costs more.
- IoU is overlap divided by union. Remember to subtract the overlap once when computing the union.
- A detection with the right label and IoU below the threshold counts as a false positive, not a partial credit.
- mAP@0.5 and mAP@0.5:0.95 are different metrics. State which one you mean, every time.
- mAP hides per-class failures. Always print the per-class AP table alongside it.
- Dice = 2·IoU / (1 + IoU). Dice is always the higher number, so never compare one against the other.
- Tune the threshold on validation data and freeze it before touching the test set.
- If you can attach a cost to each error type, expected cost is a better objective than F1.
What comes next
The CV project workflow puts these metrics to work inside a loop: build a baseline, measure it, look at the failures, fix the biggest cause, and measure again. That loop is what actually turns a 0.60 recall into a 0.90 recall, and it starts with the numbers from this post.