Sign in Get started

Annotations

Read, save, and delete annotations on individual images. Save is a full overwrite - pass the complete list every time.

View as Markdown

Annotations follow the canonical Pictograph JSON schema. See Annotation format for the full spec. The class label field is name (not class). Polygons use multi-ring paths, not flat coordinate arrays.

An image is addressed by its dataset plus its filename on all three surfaces. In REST the two are path segments, and the image’s directory is part of the filename segment: an image at /val/img-001.jpg in road-signs is .../annotations/road-signs/val/img-001.jpg. In the SDK and the CLI the directory is a separate optional argument, needed only when the same filename appears in more than one directory.

get

Fetch the typed annotation list attached to an image.

Source: Annotations.get

Arg Type Default Notes
dataset_name str required Dataset name.
image str required Image filename (a UUID also works)
directory_path str | None None Disambiguates a filename that appears in more than one directory
annotations = client.annotations.get(
    dataset_name="road-signs",
    image="img-001.jpg",
)
for ann in annotations:
    print(ann.name, ann.type)
pictograph annotations get road-signs img-001.jpg
curl -s "https://api.pictograph.io/api/v1/developer/annotations/road-signs/img-001.jpg" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY"

Returns list[Annotation]

Annotation
Annotation = BBoxAnnotation | PolygonAnnotation | PolylineAnnotation | KeypointAnnotation

An image with no annotations returns [] (never raises for the “no annotations” case, only for “no such image”). The REST response is {"success": true, "image_id": …, "filename": …, "annotations": [...], "annotation_count": N}.

save

Replace the image’s annotations with the supplied list. Full overwrite: existing annotations are dropped. Requires member+ role.

Source: Annotations.save

Arg Type Default Notes
dataset_name str required Dataset name.
image str required Image filename (a UUID also works)
annotations Sequence[Annotation] required Validated client-side; the server re-validates
directory_path str | None None Disambiguates a repeated filename
from pictograph import BBoxAnnotation, BoundingBox, PolygonAnnotation, PolygonGeometry, Point

result = client.annotations.save(
    dataset_name="road-signs",
    image="img-001.jpg",
    annotations=[
        BBoxAnnotation(
            id="b41e7d90-5c62-4a38-8e15-9d3f2a7c6b81",
            name="person",
            bounding_box=BoundingBox(x=100, y=200, w=50, h=80),
        ),
        PolygonAnnotation(
            id="c52f8ea1-6d73-4b49-9f26-ae4038d7c592",
            name="car",
            polygon=PolygonGeometry(paths=[
                [Point(x=0, y=0), Point(x=10, y=0), Point(x=10, y=10)],
            ]),
        ),
    ],
)
print(result.previous_count, "to", result.new_count, result.status)
pictograph annotations save road-signs img-001.jpg --file annotations.json

The CLI reads a JSON file holding the annotation list - the same array the SDK builds and the same array REST puts under annotations.

curl -s -X POST "https://api.pictograph.io/api/v1/developer/annotations/road-signs/img-001.jpg" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "annotations": [
      {"id": "b41e7d90-5c62-4a38-8e15-9d3f2a7c6b81", "name": "person", "type": "bbox",
       "bounding_box": {"x": 100, "y": 200, "w": 50, "h": 80}},
      {"id": "c52f8ea1-6d73-4b49-9f26-ae4038d7c592", "name": "car", "type": "polygon",
       "polygon": {"paths": [[{"x": 0, "y": 0}, {"x": 10, "y": 0}, {"x": 10, "y": 10}]]}}
    ]
  }'

Returns SaveResult

SaveResult · 4 fields
class SaveResult(BaseModel):
    """Outcome of Annotations.save."""
    image_id: str
    previous_count: int
    new_count: int
    status: str

annotations is the only body field the endpoint accepts - the image is identified entirely by the URL. Polygons may omit bounding_box on save: the server computes the enclosing rectangle. Saving never changes the image’s workflow stage.

bulk_save

Save annotations for many images in one call. Same full-overwrite semantics as save - each image’s existing annotations are dropped. Up to 200 images per call. A bad image id lands in failed rather than failing the whole batch. Requires member+ role.

Source: Annotations.bulk_save

Arg Type Default Notes
saves Mapping[str, Sequence[Annotation]] required Mapping of image_id → the annotations to set on it (canonical Pictograph JSON Annotation objects). At most 200 entries; a larger map raises ValidationError (the backend caps the batch).
from pictograph import BBoxAnnotation, BoundingBox

# bulk_save is the one annotation call keyed by image UUID rather than
# filename: it writes many images at once, and an id needs no lookup.
result = client.annotations.bulk_save(
    saves={
        "a1b2c3d4-5e6f-4708-9a1b-2c3d4e5f6a7b": [
            BBoxAnnotation(id="b41e7d90-5c62-4a38-8e15-9d3f2a7c6b81", name="person",
                           bounding_box=BoundingBox(x=100, y=200, w=50, h=80)),
        ],
        "b2c3d4e5-6f70-4819-a2bc-3d4e5f6a7b8c": [
            BBoxAnnotation(id="c52f8ea1-6d73-4b49-9f26-ae4038d7c592", name="car",
                           bounding_box=BoundingBox(x=10, y=20, w=30, h=40)),
        ],
    },
)
print(result.saved_count, "saved,", len(result.failed), "failed")
for fail in result.failed:
    print(fail.image_id, fail.error)
pictograph annotations bulk-save --file saves.json

The CLI file is a JSON object mapping image UUID to its annotation list, the same shape as the saves argument.

curl -s -X POST "https://api.pictograph.io/api/v1/developer/annotations/bulk" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "saves": [
      {"image_id": "a1b2c3d4-5e6f-4708-9a1b-2c3d4e5f6a7b", "annotations": [
        {"id": "b41e7d90-5c62-4a38-8e15-9d3f2a7c6b81", "name": "person", "type": "bbox",
         "bounding_box": {"x": 100, "y": 200, "w": 50, "h": 80}}
      ]},
      {"image_id": "b2c3d4e5-6f70-4819-a2bc-3d4e5f6a7b8c", "annotations": [
        {"id": "c52f8ea1-6d73-4b49-9f26-ae4038d7c592", "name": "car", "type": "bbox",
         "bounding_box": {"x": 10, "y": 20, "w": 30, "h": 40}}
      ]}
    ]
  }'

Returns BulkSaveResult

BulkSaveResult · 2 fields
class BulkSaveResult(BaseModel):
    """Outcome of Annotations.bulk_save."""
    saved: list[SaveResult]
    failed: list[BulkSaveFailure]

delete

Remove every annotation from the image. Equivalent to save with an empty list, but uses DELETE and requires admin+ role.

Source: Annotations.delete

Arg Type Default Notes
dataset_name str required Dataset name.
image str required Image filename (a UUID also works)
directory_path str | None None Disambiguates a repeated filename
result = client.annotations.delete(
    dataset_name="road-signs",
    image="img-001.jpg",
)
print(result.deleted_count)
pictograph annotations delete road-signs img-001.jpg --yes
curl -s -X DELETE "https://api.pictograph.io/api/v1/developer/annotations/road-signs/img-001.jpg" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY"

Returns DeleteResult

DeleteResult · 2 fields
class DeleteResult(BaseModel):
    """Outcome of Annotations.delete."""
    image_id: str
    deleted_count: int

rename_class

Rename an annotation class across a whole dataset in one call - both the class ontology entry (the class list the editor shows) and every stored annotation carrying the old name. One set-based server-side statement, not a per-image loop - use it for label-taxonomy cleanup (car to vehicle). Requires member+ role.

Source: Annotations.rename_class

Arg Type Default Notes
dataset_name str required The dataset’s name (a UUID also works).
old_name str required The class name to replace.
new_name str required The replacement name. A collision with an existing class of the same annotation type raises ConflictError.
result = client.annotations.rename_class(
    dataset_name="road-signs",
    old_name="car",
    new_name="vehicle",
)
print(result.annotations_updated, "annotations across", result.images_updated, "images")
# result.config_updated is True when the ontology entry was renamed too
pictograph annotations rename-class road-signs car vehicle
curl -s -X POST "https://api.pictograph.io/api/v1/developer/annotations/rename-class" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"dataset": "road-signs", "old_name": "car", "new_name": "vehicle"}'

Returns RenameClassResult

RenameClassResult · 6 fields
class RenameClassResult(BaseModel):
    """Outcome of Annotations.rename_class."""
    dataset_id: str
    old_name: str
    new_name: str
    images_updated: int
    annotations_updated: int
    config_updated: bool

The response is {"data": {dataset_id, old_name, new_name, images_updated, annotations_updated, config_updated}}. Renaming onto a class name that already exists for the same annotation type returns 409 ConflictError (merge the classes deliberately instead); a class that only exists on annotations, not in the ontology, still renames, with config_updated: false.

merge_class

Merge one class into another across a whole dataset: every annotation labeled source_name is reassigned to target_name and the source class is dropped from the ontology (the target is kept). This is the deliberate counterpart to the rename_class 409 - use it to combine two classes (car plus auto into vehicle). One set-based server-side statement. Requires member+ role.

Source: Annotations.merge_class

Arg Type Default Notes
dataset_name str required The dataset’s name (a UUID also works).
source_name str required The class merged away.
target_name str required The class kept; source annotations now carry it.
result = client.annotations.merge_class(
    dataset_name="road-signs",
    source_name="auto",
    target_name="vehicle",
)
print(result.annotations_updated, "annotations reassigned to", result.target_name)
pictograph annotations merge-class road-signs auto vehicle
curl -s -X POST "https://api.pictograph.io/api/v1/developer/annotations/merge-class" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"dataset": "road-signs", "source_name": "auto", "target_name": "vehicle"}'

Returns MergeClassResult

MergeClassResult · 6 fields
class MergeClassResult(BaseModel):
    """Outcome of Annotations.merge_class."""
    dataset_id: str
    source_name: str
    target_name: str
    images_updated: int
    annotations_updated: int
    config_updated: bool

The response is {"data": {dataset_id, source_name, target_name, images_updated, annotations_updated, config_updated}}.

delete_class

Delete a class from a dataset’s ontology, optionally also removing every annotation of that class in one set-based statement. With delete_annotations=False (the default) only the ontology entry is removed and existing annotations are left in place; True strips them too. Requires member+ role.

Source: Annotations.delete_class

Arg Type Default Notes
dataset_name str required Dataset name.
name str required The class name to delete
class_type str | None None Narrow the removal to one (name, type) ontology entry
delete_annotations bool False Also strip every annotation of the class
# ontology only (annotations left as-is)
client.annotations.delete_class(
    dataset_name="road-signs",
    name="obsolete",
)

# also strip every annotation of the class
result = client.annotations.delete_class(
    dataset_name="road-signs",
    name="obsolete",
    delete_annotations=True,
)
print(result.annotations_removed, "annotations removed")
pictograph annotations delete-class road-signs obsolete --with-annotations --yes
curl -s -X POST "https://api.pictograph.io/api/v1/developer/annotations/delete-class" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"dataset": "road-signs", "name": "obsolete", "delete_annotations": true}'

Returns DeleteClassResult

DeleteClassResult · 5 fields
class DeleteClassResult(BaseModel):
    """Outcome of Annotations.delete_class."""
    dataset_id: str
    name: str
    config_updated: bool
    images_updated: int
    annotations_removed: int

The response is {"data": {dataset_id, name, config_updated, images_updated, annotations_removed}}.

Validation

The SDK Pydantic models reject malformed payloads at construction:

from pictograph import PolygonAnnotation, PolygonGeometry, Point

PolygonGeometry(paths=[[Point(x=0, y=0)]])
# ValidationError: paths[0] has 1 point(s); polygon ring requires >= 3

The server re-validates on save as defense in depth: callers that construct dicts directly hit 422 ValidationError for the same class of mistakes.

import_coco

Parse a COCO dataset and save its annotations onto a Pictograph dataset.

Source: Annotations.import_coco

Arg Type Default Notes
dataset_name str required Dataset name.
coco dict[str, Any] | str | Path required A parsed COCO dict, or a path / JSON string to one.
create_missing_classes bool True When True (default), add any class the COCO categories reference but the dataset doesn’t yet define, inferring each class’s annotation type from its first annotation.
save_chunk int 200 Images per bulk_save call (backend cap 200).
report = client.annotations.import_coco(
    dataset_name="road-signs",
    coco="./instances_train.json",
)
print(report.images_matched, "matched;", report.unmatched_files, "unmatched")
# No `import-coco` command; use the SDK or REST.
# No import command - the SDK converts and saves in one call.
pictograph annotations save road-signs img-001.jpg --file annotations.json
# `import_coco` parses the COCO file on your machine, matches each
# record to an image by filename, then writes them in one call:
curl -s -X POST "https://api.pictograph.io/api/v1/developer/annotations/bulk" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"saves": [{"image_id": "$IMAGE_ID", "annotations": [ ... ]}]}'

Returns AnnotationImportReport

AnnotationImportReport · 6 fields
class AnnotationImportReport(BaseModel):
    """Outcome of an Annotations.import_coco / `import_pascal_voc` / `import_yolo` call."""
    dataset_name: str
    images_matched: int = 0
    images_saved: int = 0
    annotations_saved: int = 0
    unmatched_files: list[str] = []
    failures: list[AnnotationImportFailure] = []

import_pascal_voc

Parse per-image Pascal VOC XML and save the annotations onto a dataset.

Source: Annotations.import_pascal_voc

Arg Type Default Notes
dataset_name str required Dataset name.
xml_by_filename Mapping[str, str] required file_name → that image’s Pascal VOC .xml contents.
create_missing_classes bool True Add referenced-but-undefined classes (default True).
save_chunk int 200 Images per bulk_save call (backend cap 200).
report = client.annotations.import_pascal_voc(
    dataset_name="road-signs",
    xml_by_filename={"img-001.jpg": Path("./img-001.xml").read_text()},
)
print(report.images_matched, "matched;", report.unmatched_files, "unmatched")
# No `import-pascal-voc` command; use the SDK or REST.
# No import command - the SDK converts and saves in one call.
pictograph annotations save road-signs img-001.jpg --file annotations.json
# `import_pascal_voc` parses the PASCAL VOC file on your machine, matches each
# record to an image by filename, then writes them in one call:
curl -s -X POST "https://api.pictograph.io/api/v1/developer/annotations/bulk" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"saves": [{"image_id": "$IMAGE_ID", "annotations": [ ... ]}]}'

Returns AnnotationImportReport

AnnotationImportReport · 6 fields
class AnnotationImportReport(BaseModel):
    """Outcome of an Annotations.import_coco / `import_pascal_voc` / `import_yolo` call."""
    dataset_name: str
    images_matched: int = 0
    images_saved: int = 0
    annotations_saved: int = 0
    unmatched_files: list[str] = []
    failures: list[AnnotationImportFailure] = []

import_yolo

Parse YOLO label text per image and save the annotations onto a dataset.

Source: Annotations.import_yolo

Arg Type Default Notes
dataset_name str required Dataset name.
labels Mapping[str, str] required file_name → that image’s YOLO .txt contents.
class_names Sequence[str] required Ordered class names - YOLO’s integer class index maps here.
create_missing_classes bool True Add referenced-but-undefined classes (default True).
save_chunk int 200 Images per bulk_save call (backend cap 200).
report = client.annotations.import_yolo(
    dataset_name="road-signs",
    labels={"img-001.jpg": Path("./img-001.txt").read_text()},
    class_names=["stop", "yield", "speed-limit"],
)
print(report.images_matched, "matched;", report.unmatched_files, "unmatched")
# No `import-yolo` command; use the SDK or REST.
# No import command - the SDK converts and saves in one call.
pictograph annotations save road-signs img-001.jpg --file annotations.json
# `import_yolo` parses the YOLO file on your machine, matches each
# record to an image by filename, then writes them in one call:
curl -s -X POST "https://api.pictograph.io/api/v1/developer/annotations/bulk" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"saves": [{"image_id": "$IMAGE_ID", "annotations": [ ... ]}]}'

Returns AnnotationImportReport

AnnotationImportReport · 6 fields
class AnnotationImportReport(BaseModel):
    """Outcome of an Annotations.import_coco / `import_pascal_voc` / `import_yolo` call."""
    dataset_name: str
    images_matched: int = 0
    images_saved: int = 0
    annotations_saved: int = 0
    unmatched_files: list[str] = []
    failures: list[AnnotationImportFailure] = []

Common errors

Status Exception Cause
404 NotFoundError No such dataset, or no such image in it; also raised when the image belongs to another organization
422 ValidationError class instead of name, a flat polygon array, or an unrecognized body field
409 ConflictError rename_class onto a name that already exists for the same annotation type
403 ForbiddenError save, bulk_save and the class operations require member+; delete requires admin+

Auto-annotate

If you want SAM3 to generate annotations rather than write them by hand, see the auto-annotate resource. client.auto_annotate.dataset() saves annotations automatically; the single-prompt methods return a PromptResult and you call save yourself.

Copied to clipboard