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.

Every example below shows the Python SDK call and the equivalent raw REST request. The REST examples authenticate with an X-API-Key header; set PICTOGRAPH_API_KEY in your shell to copy-and-run them.

from pictograph import Client
client = Client()  # reads PICTOGRAPH_API_KEY

get

Fetch the typed annotation list attached to an image.

annotations = client.annotations.get("img-uuid-1")
for ann in annotations:
    print(ann.name, ann.type)
curl -s "https://api.pictograph.io/api/v1/developer/annotations/img-uuid-1" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY"

Returns list[Annotation]: a discriminated union over BBoxAnnotation / PolygonAnnotation / PolylineAnnotation / KeypointAnnotation. An image with no annotations returns [] (never raises for the “no annotations” case, only for “no such image”).

save

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

ArgTypeNotes
image_idstrImage UUID
annotationsSequence[Annotation]Pydantic-validated client-side; backend re-validates
from pictograph import BBoxAnnotation, BoundingBox, PolygonAnnotation, PolygonGeometry, Point

result = client.annotations.save("img-uuid-1", [
    BBoxAnnotation(
        id="ann-1",
        name="person",
        bounding_box=BoundingBox(x=100, y=200, w=50, h=80),
    ),
    PolygonAnnotation(
        id="ann-2",
        name="car",
        polygon=PolygonGeometry(paths=[
            [Point(x=0, y=0), Point(x=10, y=0), Point(x=10, y=10)],
        ]),
    ),
])
print(result.previous_count, "→", result.new_count, result.status)
curl -s -X POST "https://api.pictograph.io/api/v1/developer/annotations/img-uuid-1" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "image_id": "img-uuid-1",
    "annotations": [
      {"id": "ann-1", "name": "person", "type": "bbox",
       "bounding_box": {"x": 100, "y": 200, "w": 50, "h": 80}},
      {"id": "ann-2", "name": "car", "type": "polygon",
       "polygon": {"paths": [[{"x": 0, "y": 0}, {"x": 10, "y": 0}, {"x": 10, "y": 10}]]}}
    ]
  }'

Returns SaveResult with image_id, previous_count, new_count, status. The image_id in the URL must match the one in the body. Polygons may omit bounding_box on save: the backend computes the enclosing rectangle server-side. Requires member+ role.

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.

from pictograph import BBoxAnnotation, BoundingBox

result = client.annotations.bulk_save({
    "img-uuid-1": [
        BBoxAnnotation(id="ann-1", name="person",
                       bounding_box=BoundingBox(x=100, y=200, w=50, h=80)),
    ],
    "img-uuid-2": [
        BBoxAnnotation(id="ann-2", 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)
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": "img-uuid-1", "annotations": [
        {"id": "ann-1", "name": "person", "type": "bbox",
         "bounding_box": {"x": 100, "y": 200, "w": 50, "h": 80}}
      ]},
      {"image_id": "img-uuid-2", "annotations": [
        {"id": "ann-2", "name": "car", "type": "bbox",
         "bounding_box": {"x": 10, "y": 20, "w": 30, "h": 40}}
      ]}
    ]
  }'

Returns BulkSaveResult with saved (list[SaveResult]), failed (list[BulkSaveFailure]), and saved_count. Requires member+ role.

delete

Remove every annotation from the image. Equivalent to save(image_id, []) but uses DELETE and requires admin+ role.

result = client.annotations.delete("img-uuid-1")
print(result.deleted_count)
curl -s -X DELETE "https://api.pictograph.io/api/v1/developer/annotations/img-uuid-1" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY"

Returns DeleteResult with image_id and deleted_count.

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 (carvehicle). Requires member+ role.

result = client.annotations.rename_class(dataset.id, "car", "vehicle")
print(result.annotations_updated, "annotations across", result.images_updated, "images")
# result.config_updated is True when the ontology entry was renamed too
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_id": "<dataset-uuid>", "old_name": "car", "new_name": "vehicle"}'
pictograph annotations rename-class my-dataset car vehicle

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 + autovehicle). One set-based server-side statement. Requires member+ role.

result = client.annotations.merge_class(dataset.id, "auto", "vehicle")
print(result.annotations_updated, "annotations reassigned to", result.target_name)
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_id": "<dataset-uuid>", "source_name": "auto", "target_name": "vehicle"}'
pictograph annotations merge-class my-dataset auto vehicle

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.

# ontology only (annotations left as-is)
client.annotations.delete_class(dataset.id, "obsolete")

# also strip every annotation of the class
result = client.annotations.delete_class(dataset.id, "obsolete", delete_annotations=True)
print(result.annotations_removed, "annotations removed")
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_id": "<dataset-uuid>", "name": "obsolete", "delete_annotations": true}'
pictograph annotations delete-class my-dataset obsolete --with-annotations

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 backend re-validates on save as defense-in-depth: callers that construct dicts directly will hit 422 ValidationError for the same class of mistakes.

Common errors

StatusExceptionCause
404NotFoundErrorimage_id doesn’t exist
422ValidationErrorclass instead of name, flat polygon array, unknown class label
400ApiErrorURL image_id does not match the body image_id on save
403ForbiddenErrorsave/rename_class require member+; delete requires admin+ role
409ConflictErrorrename_class onto a name that already exists for the same annotation type

Auto-annotate workflow

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

Copied to clipboard