Annotation recipes


Three recipes: task_import_annotations.py loads an annotation file into an existing task, task_edit_annotations.py reads a task’s annotations, applies a bulk edit, and writes it back, and project_annotation_stats.py walks a project’s tasks and aggregates object counts per label and type into a CSV report.

Import annotations into a task

Uploads a local annotations file (e.g., predictions of a model, or work exported from another server) into an existing task, and shows the object counts before and after, so you can see what the import added. The import format is validated against the server’s importer list.

Flag Required Meaning
--host yes Server URL
--token yes Personal Access Token
--task-id yes Id of the task to import into
--annotations-file yes File to import, e.g. 'annotations.zip'
--import-format no Importer name (default 'COCO 1.0')
python task_import_annotations.py --host 'https://app.cvat.ai' --token '<your token>' \
    --task-id 42 --annotations-file 'annotations.zip' --import-format 'COCO 1.0'

The script

# Copyright (C) CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT

"""Import annotations from a local file into an existing task, e.g. to load
predictions of a model or work made on another server.

Steps:
  1. Retrieve the task and count the objects it already has.
  2. Fetch the server's import format list and validate --import-format.
  3. Upload the annotations file and wait for the server to process it.
  4. Count the objects again to show what the import added.

Usage (run ``python task_import_annotations.py --help`` for the full list of options):
  python task_import_annotations.py --host 'https://app.cvat.ai' --token '<your token>' \\
      --task-id 42 --annotations-file 'annotations.zip' --import-format 'COCO 1.0'
"""

import argparse
import sys
from pathlib import Path

from cvat_sdk import make_client


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
    parser.add_argument("--host", required=True, help="CVAT server URL, e.g. 'https://app.cvat.ai'")
    parser.add_argument(
        "--token",
        required=True,
        help="Personal Access Token (CVAT UI: Profile -> Security)",
    )
    parser.add_argument(
        "--task-id", type=int, required=True, help="id of an existing task, e.g. 42"
    )
    parser.add_argument(
        "--annotations-file",
        type=Path,
        required=True,
        help="file to import, e.g. 'annotations.zip'",
    )
    parser.add_argument(
        "--import-format",
        default="COCO 1.0",
        help="importer name, e.g. 'COCO 1.0' (default: '%(default)s')",
    )
    return parser.parse_args()


def count_objects(task) -> int:
    """All annotation objects of a task: tags, shapes, and tracks."""
    annotations = task.get_annotations()
    return len(annotations.tags) + len(annotations.shapes) + len(annotations.tracks)


def main() -> None:
    args = parse_args()
    with make_client(args.host, access_token=args.token) as client:
        task = client.tasks.retrieve(args.task_id)
        print(f"Task {task.id}: {count_objects(task)} objects before import")

        formats, _ = client.api_client.server_api.retrieve_annotation_formats()
        names = [f.name for f in formats.importers]
        if args.import_format not in names:
            sys.exit(
                f"Unknown import format {args.import_format!r}. Choose one of: {', '.join(names)}"
            )

        task.import_annotations(args.import_format, args.annotations_file)
        print(f"Imported {args.annotations_file} as {args.import_format!r}")

        print(f"Task {task.id}: {count_objects(task)} objects after import")


if __name__ == "__main__":
    main()

Read, edit, and write back annotations

Reads all of a task’s annotations (tags, shapes, and tracks), applies one bulk edit — move every object from one label to another (--relabel FROM TO) or delete every object with a given label (--delete-label NAME) — and writes the edit back with a partial update, so the untouched objects are not re-uploaded. Prints the per-label object counts before and after.

Flag Required Meaning
--host yes Server URL
--token yes Personal Access Token
--task-id yes Id of the task to edit
--relabel FROM TO one of --relabel / --delete-label Move all objects from label FROM to label TO
--delete-label NAME one of --relabel / --delete-label Delete all objects with this label
python task_edit_annotations.py --host 'https://app.cvat.ai' --token '<your token>' \
    --task-id 42 --relabel 'car' 'vehicle'
python task_edit_annotations.py --host 'https://app.cvat.ai' --token '<your token>' \
    --task-id 42 --delete-label 'draft'

The script

# Copyright (C) CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT

"""Read a task's annotations, edit them, and write the edit back: move every
object from one label to another (--relabel) or delete every object with a
given label (--delete-label).

Steps:
  1. Retrieve the task and map its label names to ids.
  2. Read all annotations (tags, shapes, and tracks) and count objects per label.
  3. Write the edit back with a partial annotation update, so the objects that
     are not affected by it are not re-uploaded.
  4. Re-read the annotations and print the per-label object counts diff.

Usage (run ``python task_edit_annotations.py --help`` for the full list of options):
  python task_edit_annotations.py --host 'https://app.cvat.ai' --token '<your token>' \\
      --task-id 42 --relabel 'car' 'vehicle'
  python task_edit_annotations.py --host 'https://app.cvat.ai' --token '<your token>' \\
      --task-id 42 --delete-label 'draft'
"""

import argparse
import sys
from collections import Counter

from cvat_sdk import make_client, models
from cvat_sdk.core.proxies.annotations import AnnotationUpdateAction


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
    parser.add_argument("--host", required=True, help="CVAT server URL, e.g. 'https://app.cvat.ai'")
    parser.add_argument(
        "--token",
        required=True,
        help="Personal Access Token (CVAT UI: Profile -> Security)",
    )
    parser.add_argument(
        "--task-id", type=int, required=True, help="id of an existing task, e.g. 42"
    )
    action = parser.add_mutually_exclusive_group(required=True)
    action.add_argument(
        "--relabel",
        nargs=2,
        metavar=("FROM", "TO"),
        help="move all objects from label FROM to label TO",
    )
    action.add_argument("--delete-label", metavar="NAME", help="delete all objects with this label")
    return parser.parse_args()


def label_counts(annotations, label_names: dict[int, str]) -> Counter:
    """Objects per label name, over tags, shapes, and tracks alike."""
    return Counter(
        label_names[obj.label_id]
        for obj in [*annotations.tags, *annotations.shapes, *annotations.tracks]
    )


def main() -> None:
    args = parse_args()
    with make_client(args.host, access_token=args.token) as client:
        task = client.tasks.retrieve(args.task_id)
        label_ids = {label.name: label.id for label in task.get_labels()}
        label_names = {id: name for name, id in label_ids.items()}

        source = args.relabel[0] if args.relabel else args.delete_label
        affected = set(args.relabel) if args.relabel else {source}
        for name in affected:
            if name not in label_ids:
                sys.exit(f"Label {name!r} not found in task {task.id}")

        annotations = task.get_annotations()
        before = label_counts(annotations, label_names)

        tags = [tag for tag in annotations.tags if tag.label_id == label_ids[source]]
        shapes = [shape for shape in annotations.shapes if shape.label_id == label_ids[source]]
        tracks = [track for track in annotations.tracks if track.label_id == label_ids[source]]
        matched = len(tags) + len(shapes) + len(tracks)

        if args.relabel:
            target_id = label_ids[args.relabel[1]]
            task.update_annotations(
                models.PatchedLabeledDataRequest(
                    tags=[
                        models.LabeledImageRequest(**{**tag.to_dict(), "label_id": target_id})
                        for tag in tags
                    ],
                    shapes=[
                        models.LabeledShapeRequest(**{**shape.to_dict(), "label_id": target_id})
                        for shape in shapes
                    ],
                    tracks=[
                        models.LabeledTrackRequest(**{**track.to_dict(), "label_id": target_id})
                        for track in tracks
                    ],
                ),
                action=AnnotationUpdateAction.UPDATE,
            )
            print(f"Moved {matched} objects from {source!r} to {args.relabel[1]!r}")
        else:
            # An empty id list would make remove_annotations() drop *all* the task
            # annotations, so skip the request when the label has no objects.
            if matched:
                task.remove_annotations(ids=[obj.id for obj in [*tags, *shapes, *tracks]])
            print(f"Deleted {matched} objects with label {source!r}")

        after = label_counts(task.get_annotations(), label_names)
        for name in sorted(affected):
            print(f"  {name}: {before[name]} -> {after[name]}")


if __name__ == "__main__":
    main()

Aggregate annotation statistics over a project

Walks every task of a project and counts the annotated objects per label and per type (a shape type such as rectangle or polygon, tag, or track). Prints a per-task breakdown with per-label project totals and writes annotation_stats.csv into the current directory — one row per (task, label, type).

Flag Required Meaning
--host yes Server URL
--token yes Personal Access Token
--project-id yes Id of the project to aggregate
python project_annotation_stats.py --host 'https://app.cvat.ai' --token '<your token>' \
    --project-id 7

The script

# Copyright (C) CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT

"""Aggregate what was annotated across a project: object counts per label and
per object type for every task, printed and written to a CSV report.

Steps:
  1. Retrieve the project and its label names.
  2. Walk the project's tasks and read each task's annotations.
  3. Count objects per (label, type), where the type is a shape type such as
     'rectangle' or 'polygon', 'tag' for tags, or 'track' for tracks.
  4. Print a per-task breakdown with per-label project totals, and write
     the CSV report to --output.

Usage (run ``python project_annotation_stats.py --help`` for the full list of options):
  python project_annotation_stats.py --host 'https://app.cvat.ai' --token '<your token>' \\
      --project-id 7 --output annotation_stats.csv
"""

import argparse
import csv
from collections import Counter
from pathlib import Path

from cvat_sdk import make_client


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
    parser.add_argument("--host", required=True, help="CVAT server URL, e.g. 'https://app.cvat.ai'")
    parser.add_argument(
        "--token",
        required=True,
        help="Personal Access Token (CVAT UI: Profile -> Security)",
    )
    parser.add_argument(
        "--project-id", type=int, required=True, help="id of an existing project, e.g. 7"
    )
    parser.add_argument(
        "--output",
        type=Path,
        default=Path("annotation_stats.csv"),
        help="path to write the CSV report to (default: %(default)s)",
    )
    return parser.parse_args()


def task_counts(task, label_names: dict[int, str]) -> Counter:
    """Objects per (label name, object type) in one task."""
    annotations = task.get_annotations()
    counts = Counter()
    for tag in annotations.tags:
        counts[(label_names[tag.label_id], "tag")] += 1
    for shape in annotations.shapes:
        counts[(label_names[shape.label_id], str(shape.type))] += 1
    for track in annotations.tracks:
        counts[(label_names[track.label_id], "track")] += 1
    return counts


def main() -> None:
    args = parse_args()
    report_path = args.output
    with make_client(args.host, access_token=args.token) as client:
        project = client.projects.retrieve(args.project_id)
        label_names = {label.id: label.name for label in project.get_labels()}
        tasks = project.get_tasks()

        label_totals = Counter()
        total = 0
        with report_path.open("w", newline="") as f:
            writer = csv.writer(f)
            writer.writerow(["task_id", "task_name", "label", "type", "count"])
            for task in tasks:
                counts = task_counts(task, label_names)
                print(f"Task {task.id} {task.name!r}: {sum(counts.values())} objects")
                for (label, type_), count in sorted(counts.items()):
                    print(f"  {label}/{type_}: {count}")
                    writer.writerow([task.id, task.name, label, type_, count])
                    label_totals[label] += count
                total += sum(counts.values())

        print(f"Project {project.id}: {total} objects across {len(tasks)} tasks")
        for label, count in sorted(label_totals.items()):
            print(f"  {label}: {count}")
        print(f"Wrote {report_path.resolve()}")


if __name__ == "__main__":
    main()

Other SDK options:

SDK method / parameter What it adds
Task.import_annotations(..., import_mode="append") Add the imported objects to the existing annotations instead of replacing them (server support required).
Task.import_annotations(..., conv_mask_to_poly=True) Convert imported masks to polygons on the fly.
Task.import_annotations(..., pbar=ProgressReporter()) Report upload progress (a cvat_sdk.core.progress.ProgressReporter).
Job.import_annotations(format_name, path) The same import scoped to a single job.
Task.set_annotations(LabeledDataRequest(...)) Replace a task’s annotations with the given objects.
Task.update_annotations(PatchedLabeledDataRequest(...), action=AnnotationUpdateAction.CREATE | UPDATE | DELETE) Partial update: create, update, or delete only the objects in the request.
Task.remove_annotations(ids=[...]) Delete specific objects by id — or all of them when ids is omitted.
Project.get_annotations() Read the annotations of every task in a project in one call.

Notes: