AnyLabeling: AI-Powered Data Annotation
11 / 11
Lesson 11 of 11

Lesson 11: Scaling Annotation for Teams

8 min readViet-Anh NguyenViet-Anh Nguyen

A single annotator can label 500-1,000 images per day with AnyLabeling and AI assistance. That is enough for personal projects and small experiments. But production ML often requires 10,000-100,000+ labeled images, which means a team.

Team annotation introduces problems that solo annotation does not: inconsistency between annotators, quality variance, version conflicts, and the organizational overhead of coordinating who labels what. This lesson covers the patterns that make it work.

Splitting Work Across Annotators

Directory-Based Partitioning

The simplest approach: split your image directory into non-overlapping subsets and assign each subset to an annotator.

from pathlib import Path
import shutil

def partition_images(images_dir, num_annotators, output_base):
    images = sorted(Path(images_dir).glob("*"))
    images = [p for p in images if p.suffix.lower() in (".jpg", ".jpeg", ".png", ".bmp")]

    per_annotator = len(images) // num_annotators
    remainder = len(images) % num_annotators

    idx = 0
    for annotator_id in range(num_annotators):
        count = per_annotator + (1 if annotator_id < remainder else 0)
        annotator_dir = Path(output_base) / f"annotator_{annotator_id:02d}"
        annotator_dir.mkdir(parents=True, exist_ok=True)

        for img in images[idx:idx + count]:
            shutil.copy2(img, annotator_dir / img.name)
        idx += count

    print(f"Partitioned {len(images)} images across {num_annotators} annotators")

partition_images("unlabeled/", num_annotators=4, output_base="annotation_batches/")

Each annotator opens their directory in AnyLabeling, labels independently, and returns the JSON annotation files.

Overlap for Quality Measurement

Reserve 5-10% of images as overlap — images assigned to multiple annotators. This lets you compute inter-annotator agreement (Lesson 09) as an ongoing quality metric.

import random

def partition_with_overlap(images_dir, num_annotators, output_base, overlap_pct=0.1):
    images = sorted(Path(images_dir).glob("*"))
    images = [p for p in images if p.suffix.lower() in (".jpg", ".jpeg", ".png", ".bmp")]

    # Select overlap images
    overlap_count = int(len(images) * overlap_pct)
    overlap_images = set(random.sample(range(len(images)), overlap_count))

    # Partition the rest
    non_overlap = [img for i, img in enumerate(images) if i not in overlap_images]
    overlap = [images[i] for i in overlap_images]

    per_annotator = len(non_overlap) // num_annotators

    idx = 0
    for annotator_id in range(num_annotators):
        annotator_dir = Path(output_base) / f"annotator_{annotator_id:02d}"
        annotator_dir.mkdir(parents=True, exist_ok=True)

        # Unique images for this annotator
        end = idx + per_annotator + (1 if annotator_id < len(non_overlap) % num_annotators else 0)
        for img in non_overlap[idx:end]:
            shutil.copy2(img, annotator_dir / img.name)
        idx = end

        # Overlap images go to ALL annotators
        for img in overlap:
            shutil.copy2(img, annotator_dir / img.name)

Quality Assurance Workflow

Annotation without QA is annotation you cannot trust. Here is a three-tier QA system:

Tier 1: Automated Checks

Run automated validation on every annotation submission:

import json
from pathlib import Path

def validate_annotations(annotation_dir, min_box_size=20, valid_labels=None):
    issues = []
    for json_file in Path(annotation_dir).glob("*.json"):
        with open(json_file) as f:
            data = json.load(f)

        for shape in data.get("shapes", []):
            # Check label validity
            if valid_labels and shape["label"] not in valid_labels:
                issues.append(f"{json_file.name}: unknown label '{shape['label']}'")

            # Check minimum size
            if shape["shape_type"] == "rectangle":
                pts = shape["points"]
                w = abs(pts[1][0] - pts[0][0])
                h = abs(pts[1][1] - pts[0][1])
                if w < min_box_size or h < min_box_size:
                    issues.append(f"{json_file.name}: box too small ({w:.0f}x{h:.0f})")

            # Check empty labels
            if not shape["label"].strip():
                issues.append(f"{json_file.name}: empty label")

    return issues

issues = validate_annotations(
    "annotator_00/",
    valid_labels={"car", "truck", "pedestrian", "cyclist"}
)
for issue in issues:
    print(f"  {issue}")

Tier 2: Spot Check Review

A senior annotator or ML engineer reviews a random sample of each annotator's work. 10-20% is a reasonable sample rate.

Checklist for each reviewed image:

  • All visible objects of target classes are annotated (no misses)
  • Bounding boxes are tight (no excessive padding)
  • Labels are correct (no class confusion)
  • Edge cases follow the annotation guidelines
  • No duplicate annotations on the same object

Tier 3: Model-Assisted QA

Train a model on the annotations and examine its failure cases. If the model consistently fails on a specific annotator's images, that annotator may be introducing systematic errors.

from ultralytics import YOLO

model = YOLO("latest_model.pt")

# Run validation per annotator batch
for annotator_dir in Path("annotation_batches/").iterdir():
    metrics = model.val(data=f"{annotator_dir}/dataset.yaml")
    print(f"{annotator_dir.name}: mAP@50={metrics.box.map50:.3f}")

A significantly lower mAP on one annotator's batch is a red flag worth investigating.

Resolving Annotation Conflicts

When two annotators label the same image differently, you need a resolution strategy:

Strategy 1: Senior Adjudication

A senior annotator reviews all disagreements and makes the final call. This is the gold standard for quality but the most expensive in terms of time.

Strategy 2: Majority Vote

If three annotators label the same image, take the majority label for each object. Fast, but requires 3x annotation for overlap images.

Strategy 3: Confidence-Weighted Merge

Weight each annotator's labels by their historical agreement rate with the adjudicator:

def weighted_merge(annotations_by_annotator, annotator_weights):
    """
    annotations_by_annotator: dict of annotator_id -> list of (label, bbox) tuples
    annotator_weights: dict of annotator_id -> float (0-1, based on historical accuracy)
    """
    # For each detected region, accumulate weighted votes per label
    # This is a simplified version — production code needs IoU matching
    merged = []
    for ann_id, anns in annotations_by_annotator.items():
        weight = annotator_weights.get(ann_id, 1.0)
        for label, bbox in anns:
            merged.append({"label": label, "bbox": bbox, "weight": weight, "source": ann_id})
    return merged

Dataset Versioning

Your dataset is code. Version it.

Git LFS for Small-Medium Datasets (Under 10 GB)

# Initialize Git LFS
git lfs install
git lfs track "*.jpg" "*.png" "*.json"

# Structure
dataset/
├── .gitattributes
├── images/
├── labels/
├── annotations.json
└── CHANGELOG.md

Commit after each annotation round with a descriptive message:

git add images/ labels/ annotations.json
git commit -m "Round 3: +150 images, focused on nighttime driving scenes"

DVC for Large Datasets (Over 10 GB)

pip install dvc
dvc init
dvc add images/ labels/
git add images.dvc labels.dvc .gitignore
git commit -m "Round 3: +150 images"
dvc push  # pushes to remote storage (S3, GCS, etc.)

Changelog

Maintain a CHANGELOG.md that records what changed in each annotation round:

## Round 3 — 2026-03-15

- Added 150 images focused on nighttime driving
- Fixed 23 label errors found in Tier 2 QA review
- Updated guidelines: golf carts now classified as "other_vehicle"
- Training mAP improved from 0.72 to 0.78

## Round 2 — 2026-03-10

- Added 200 images from uncertainty sampling
- Annotator 02 retrained after low IAA score (0.58 -> 0.81)
- Total dataset: 450 images

The Complete Team Workflow

Putting it all together:

1. ML lead writes annotation guidelines (Lesson 09)
2. Pilot round: 2-3 annotators label 50 overlap images
3. Compute IAA. Refine guidelines if IAA < threshold
4. Partition images across annotators (with 10% overlap)
5. Annotators label in AnyLabeling with AI assistance
6. Automated validation catches format errors
7. Spot check review on 10-20% sample
8. Merge annotations, resolve conflicts on overlap images
9. Version the dataset (git commit / dvc push)
10. Train model, evaluate, select next round's images (Lesson 10)
11. Repeat from step 4

When to Use a Labeling Platform Instead

AnyLabeling is a desktop tool. For teams larger than 5-10 annotators, you may need a web-based platform with built-in task management:

Team SizeRecommended Approach
1-3AnyLabeling + shared drive + git
3-10AnyLabeling + partitioning scripts + DVC
10-50Label Studio or CVAT (self-hosted)
50+Commercial platform (Scale AI, Labelbox)

AnyLabeling remains valuable even in large teams as the tool individual annotators use for AI-assisted labeling. The web platform handles task management and QA; AnyLabeling handles the actual annotation.

Key Takeaways

  • Split images across annotators with 5-10% overlap for quality measurement.
  • Three-tier QA: automated checks, spot review, model-assisted validation.
  • Version your dataset like code. Use git LFS or DVC.
  • Maintain a changelog for every annotation round.
  • AnyLabeling scales to teams of 5-10. Beyond that, layer a web platform on top for task management.

Course Conclusion

You now have a complete annotation pipeline:

  1. Understand why annotation quality is the highest-leverage ML activity.
  2. Install AnyLabeling and navigate the interface.
  3. Annotate with every tool — rectangles, polygons, points, text.
  4. Accelerate with SAM and YOLO auto-labeling.
  5. Export to any training framework format.
  6. Extend with custom ONNX models.
  7. Specify with annotation guidelines that eliminate ambiguity.
  8. Iterate with active learning loops.
  9. Scale across a team with QA and versioning.

The gap between a prototype model and a production model is almost always data. Close that gap deliberately, and the models follow.

Next steps:

  • Download AnyLabeling and annotate your first 50 images.
  • Write annotation guidelines for your specific task before touching any images.
  • Set up the active learning loop from Lesson 10 and commit to at least 3 rounds.
  • Join the community: open issues, contribute models, share your workflows.