AI & Computer Vision, lesson 12 of 52
Annotation & Labelling: Writing the Answer Key
Introduction
A model learns by comparing its guesses with the right answers. Those right answers are labels: boxes drawn round every player, ball and goal, by people. Today you'll set up a labelling project, learn the rules that make labels trustworthy, read the label files a YOLO model uses, and measure how well two people's boxes agree.
By the end of this lesson you'll be able to:
- Explain what annotation is and why labels are the "answer key" for a model
- Tell apart bounding boxes, keypoints and segmentation masks, and say when each is used
- Create a Roboflow project with the classes
player,ballandgoal - Draw tight, consistent boxes following a written label guide
- Read a YOLO label file and convert between pixels and YOLO format
- Measure agreement between two labellers with IoU
Working principle
Kinds of annotation
| Type | What you draw | Used for | Cost |
|---|---|---|---|
| Bounding box | A rectangle around each object | Detection (YOLO). Our main tool. | Cheap |
| Keypoints | Dots on joints or parts (knee, elbow, ball centre) | Pose estimation (lesson 44) | Medium |
| Segmentation | The exact outline of each object | Separating touching players, pixel-level tasks | Expensive |
The three golden rules of boxes
- Tight: the box touches the object's edges, with no big empty margin.
- Complete: label every instance of every class in every image, not just the clear ones. A missed ball teaches the model that balls are background.
- Consistent: the same rules for everyone, every time (see the label guide).
A written label guide
Before labelling, the team agrees on rules and writes them down. For example:
| Question | Our rule |
|---|---|
| Partly hidden player? | Label if at least half the body is visible. Box only the visible part. |
| Ball in the air, blurry? | Label if you can tell it is the ball. Box the blur. |
| Ball hidden behind a leg? | Don't label. |
| Goalkeeper? | player |
| Goal | Box the whole frame of posts and bar, and the net |
| Crowd or players far in the distance, under 10 pixels tall? | Don't label |
| Referee | player (or a separate referee class if the team decides so) |
The rule matters less than everyone using the same one.
YOLO label format
Each image has a .txt file with one line per box:
class_id x_center y_center width heightAll four numbers are fractions of the image size (0 to 1). Example: a ball with its centre at (320, 180) in a 640 by 360 image, 18 pixels wide and 18 tall is 1 0.5 0.5 0.028125 0.05 if the ball is class 1.
Measuring agreement: IoU
Intersection over Union (IoU) is the overlap area of two boxes divided by the area they cover together. 1.0 is a perfect match, 0 is no overlap. Two careful labellers usually score 0.8 or higher on a clear object. We use IoU again in lesson 23, to score a detector.
IoU = overlap ÷ area covered by both = 0.64. Counts as a match (≥ 0.5).
What you'll need
- A free Roboflow account (a teacher or parent can set up a class workspace)
- About 50 of the frames you captured in lesson 11
- Your
cv_courseenvironment for the checking scripts
Set up step by step
- Create a free Roboflow account and a new Object Detection project named
football-s12. - Upload your 50 frames from lesson 11 (drag the folder in).
- In Classes, add
player,ballandgoal. Keep the names exactly the same for everyone in the team. - Open the Annotate tab. Draw a box and choose its class. Use the keyboard shortcuts shown in the tool to go faster.
- Roboflow also offers AI-assisted labelling features that suggest boxes. Their names change over time, so look on the Annotate page. If you use them, you still check every box.
- Mark images with no objects as null (background) images, don't skip them.
- When all 50 are done, open the dataset Health Check to see the class balance and any problems.
Build it
Step 1: convert between pixels and YOLO, and draw a box to check
import cv2
import numpy as np
def pixels_to_yolo(box, img_w, img_h):
"""box = (x1, y1, x2, y2) in pixels -> (xc, yc, w, h) as fractions."""
x1, y1, x2, y2 = box
xc = (x1 + x2) / 2 / img_w
yc = (y1 + y2) / 2 / img_h
return xc, yc, (x2 - x1) / img_w, (y2 - y1) / img_h
def yolo_to_pixels(label, img_w, img_h):
"""label = (xc, yc, w, h) fractions -> (x1, y1, x2, y2) in pixels."""
xc, yc, w, h = label
x1 = int((xc - w / 2) * img_w)
y1 = int((yc - h / 2) * img_h)
x2 = int((xc + w / 2) * img_w)
y2 = int((yc + h / 2) * img_h)
return x1, y1, x2, y2
img = np.zeros((360, 640, 3), np.uint8)
img[:] = (34, 142, 28)
cv2.circle(img, (320, 180), 9, (0, 140, 255), -1) # an orange ball
label = pixels_to_yolo((311, 171, 329, 189), 640, 360)
print("YOLO line: 1 %.6f %.6f %.6f %.6f" % label)
print("Back to pixels:", yolo_to_pixels(label, 640, 360))
x1, y1, x2, y2 = yolo_to_pixels(label, 640, 360)
cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 1) # if the box is tight, the conversion is right
cv2.imwrite("label_check.jpg", img)Question: why must the box drawn from the label sit exactly on the ball?
Step 2: IoU between two labellers
def iou(a, b):
"""a, b = (x1, y1, x2, y2) in pixels."""
ix1, iy1 = max(a[0], b[0]), max(a[1], b[1])
ix2, iy2 = min(a[2], b[2]), min(a[3], b[3])
inter = max(0, ix2 - ix1) * max(0, iy2 - iy1)
area_a = (a[2] - a[0]) * (a[3] - a[1])
area_b = (b[2] - b[0]) * (b[3] - b[1])
union = area_a + area_b - inter
return inter / union if union > 0 else 0.0
alice = (100, 100, 200, 300)
ben = (110, 105, 210, 310) # nearly the same box, slightly shifted
print("IoU:", round(iou(alice, ben), 3))
print("Identical:", iou(alice, alice))
print("No overlap:", iou(alice, (400, 400, 500, 500)))Question: ask your partner to label the same ball. What IoU do you get? Try a ball and a player: why is the ball's IoU usually lower?
Step 3: check a folder of YOLO labels
Run this on the labels folder Roboflow exports (labels/*.txt). It counts boxes per class and flags suspicious ones.
import os
from collections import Counter
NAMES = {0: "player", 1: "ball", 2: "goal"} # must match the order in your export's data.yaml
def check_labels(folder):
counts = Counter()
problems = []
n_files = 0
for name in sorted(os.listdir(folder)):
if not name.endswith(".txt"):
continue
n_files += 1
with open(os.path.join(folder, name)) as f:
for line_no, line in enumerate(f, 1):
parts = line.split()
if not parts:
continue
if len(parts) != 5:
problems.append((name, line_no, "not 5 numbers"))
continue
cls = int(parts[0])
xc, yc, w, h = map(float, parts[1:])
counts[NAMES.get(cls, f"class {cls}")] += 1
if not (0 <= xc <= 1 and 0 <= yc <= 1 and 0 < w <= 1 and 0 < h <= 1):
problems.append((name, line_no, "outside the image"))
elif w < 0.005 or h < 0.005:
problems.append((name, line_no, "tiny box"))
print(f"{n_files} label files")
for cls, n in counts.most_common():
print(f" {cls}: {n}")
for p in problems[:20]:
print(" PROBLEM", p)
return counts, problems
if __name__ == "__main__":
import sys
check_labels(sys.argv[1] if len(sys.argv) > 1 else "labels")If it goes wrong
| Error | Cause | Fix |
|---|---|---|
| Boxes too loose | Rushed labelling | Zoom in, and draw to the edge of the object |
| Missed small ball | It's hard to see | Zoom in. Use the tool's zoom, and check the image twice. |
| Class names differ between teammates | Typed by hand (Player, player ) | Create classes once and choose from the list |
Exported label numbers don't match your NAMES | The class order in data.yaml differs | Read data.yaml and copy the order |
not 5 numbers problem in the checker | A segmentation export (polygon) was used | Re-export as YOLO (bounding boxes) |
| IoU for the ball is much lower than for players | Small objects: one pixel of shift changes the IoU a lot | Expected. Zoom in to improve. |
Try it
- Label keypoints on a few images (ball centre, goal posts)
- Compute the mean IoU across the 10 shared images
- Write a 5-line summary of where labellers disagreed most
Check what you learnt
Why must you label every player in a picture, not just some?
A YOLO label line is: 0 0.52 0.40 0.10 0.30. What are the last four numbers?
Two people's boxes have an IoU of 0.9. What does that mean?
Why write a label guide?
Which is quicker to draw: a box or a segmentation mask?
Conclusion
Labels are the answer key a model learns from, so they must be complete (every instance), tight and consistent (follow a written guide). YOLO stores each box as a class number plus four fractions, and IoU measures how closely two boxes agree, which you'll use again to judge detectors.
What next?
Next: Train/val/test splits & augmentation. You'll split your labelled data fairly and multiply it with augmentation.

