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:
- An object’s
label_idmust be a label of the task (or of its project);task.get_labels()maps names to ids. - Both editing recipes re-read the annotations after writing, so the printed “after” counts show the server’s state, not the client’s intention.
- Full recipes:
task_import_annotations.py,task_edit_annotations.py,project_annotation_stats.py.