AI & Computer Vision, lesson 12 of 52

Annotation & Labelling: Writing the Answer Key

intermediate20 min read

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, ball and goal
  • 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

TypeWhat you drawUsed forCost
Bounding boxA rectangle around each objectDetection (YOLO). Our main tool.Cheap
KeypointsDots on joints or parts (knee, elbow, ball centre)Pose estimation (lesson 44)Medium
SegmentationThe exact outline of each objectSeparating touching players, pixel-level tasksExpensive

The three golden rules of boxes

  1. Tight: the box touches the object's edges, with no big empty margin.
  2. 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.
  3. 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:

QuestionOur 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
GoalBox the whole frame of posts and bar, and the net
Crowd or players far in the distance, under 10 pixels tall?Don't label
Refereeplayer (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  height

All 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_course environment for the checking scripts

Set up step by step

  1. Create a free Roboflow account and a new Object Detection project named football-s12.
  2. Upload your 50 frames from lesson 11 (drag the folder in).
  3. In Classes, add player, ball and goal. Keep the names exactly the same for everyone in the team.
  4. Open the Annotate tab. Draw a box and choose its class. Use the keyboard shortcuts shown in the tool to go faster.
  5. 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.
  6. Mark images with no objects as null (background) images, don't skip them.
  7. 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

ErrorCauseFix
Boxes too looseRushed labellingZoom in, and draw to the edge of the object
Missed small ballIt's hard to seeZoom in. Use the tool's zoom, and check the image twice.
Class names differ between teammatesTyped by hand (Player, player )Create classes once and choose from the list
Exported label numbers don't match your NAMESThe class order in data.yaml differsRead data.yaml and copy the order
not 5 numbers problem in the checkerA segmentation export (polygon) was usedRe-export as YOLO (bounding boxes)
IoU for the ball is much lower than for playersSmall objects: one pixel of shift changes the IoU a lotExpected. 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

  1. Why must you label every player in a picture, not just some?

  2. A YOLO label line is: 0 0.52 0.40 0.10 0.30. What are the last four numbers?

  3. Two people's boxes have an IoU of 0.9. What does that mean?

  4. Why write a label guide?

  5. 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.