Sign in Get started

Images

List a dataset's images, and upload, fetch, download, or delete them.

View as Markdown

This page covers listing a dataset’s images and single-image ops (fetch, upload, download, delete). For bulk operations across many images, prefer the upload workflow and the batch resource.

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

list

List a dataset’s images, newest first, filtered by folder and/or status. Returns a single page (list[Image]); use iter to page over all of them.

ArgTypeDefaultNotes
dataset_idstrrequiredUUID of the dataset to list
folder_pathstr | NoneNoneRestrict to one virtual folder (e.g. /train); None lists all folders
statusstr | NoneNoneRestrict to a stage: new / annotate / review / complete
include_archivedboolFalseInclude soft-deleted (archived) images
min_confidence_ltfloat | NoneNoneActive-learning filter: keep only images whose model confidence is below this (0–1). None applies no filter
limitint100Page size (max 1000)
offsetint0Pagination offset
dataset = client.datasets.get("my-dataset")
images = client.images.list(dataset.id, folder_path="/train", limit=50)
for img in images:
    print(img.filename, img.status, img.annotation_count, img.min_confidence)

# Active learning: page the model-uncertainty review queue (least-confident
# images first) — every listed image carries `min_confidence` (1.0 = certain).
for img in client.images.iter(dataset.id, min_confidence_lt=0.9):
    print(img.filename, img.min_confidence)
curl -s "https://api.pictograph.io/api/v1/developer/images/?dataset_id=<dataset-uuid>&folder_path=/train&status=complete&limit=50&offset=0" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY"

Returns list[Image]. The response shape is {"data": [...], "pagination": {"limit": 50, "offset": 0, "total": 1234, "has_more": true}}total is the dataset-wide count for the active filters, so has_more is authoritative.

iter

Auto-page over every image in a dataset — no offset bookkeeping. Returns an OffsetPager[Image]: iterate it directly, materialise with .all(), or peek the first match with .first(). Filters mirror list.

ArgTypeDefaultNotes
dataset_idstrrequiredUUID of the dataset to iterate
folder_pathstr | NoneNoneRestrict to one virtual folder; None iterates all
statusstr | NoneNoneRestrict to a stage; None iterates all
include_archivedboolFalseInclude archived images
min_confidence_ltfloat | NoneNoneActive-learning filter: iterate only images with model confidence below this (0–1)
page_sizeint100Items per backend round-trip (max 1000)
max_totalint | NoneNoneStop after this many items; None yields all
# Walk an entire folder, paging transparently
for img in client.images.iter(project.id, folder_path="/train"):
    print(img.filename)

# Or cap the total and materialise to a list
complete = client.images.iter(project.id, status="complete", max_total=500).all()

iter hits the same REST endpoint as list, walking offset for you until pagination.has_more is false.

get

Fetch metadata for a single image.

image = client.images.get("img-uuid-1")
print(image.filename, image.status, image.annotation_count)
curl -s "https://api.pictograph.io/api/v1/developer/images/img-uuid-1/metadata" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY"

Returns Image — the one canonical image shape every endpoint uses:

{
  "data": {
    "id": "img-uuid-1",
    "dataset_id": "dataset-uuid",
    "filename": "photo.jpg",
    "status": "complete",
    "split": "train",
    "annotation_count": 3,
    "min_confidence": 0.91,
    "file_size": 123456,
    "width": 1920,
    "height": 1080,
    "content_type": "image/jpeg",
    "folder_path": "/cars",
    "tags": ["night", "rainy"],
    "is_archived": false,
    "image_url": "https://api.pictograph.io/api/v1/developer/images/img-uuid-1",
    "thumbnail_url": "https://api.pictograph.io/api/cdn/images/...?size=md",
    "annotation_url": "https://api.pictograph.io/api/v1/developer/annotations/img-uuid-1/file",
    "created_at": "2026-07-01T00:00:00Z"
  }
}

tags are the user image tags (bulk_tag writes them); min_confidence is the active-learning signal (1.0 = certain / human-drawn). Annotations live on the annotations resource: call client.annotations.get(image.id) to fetch them, or pass ?include_annotations=true on the REST endpoint to embed them inline under data.annotations.

upload

Upload a local file to a dataset. The SDK uses the three-step signed-URL flow: request a signed upload URL, PUT the bytes straight to storage, then register the image. Bytes never relay through the API, which is faster and avoids the request-body size limit.

ArgTypeDefaultNotes
dataset_idstrrequiredUUID of the destination dataset
file_pathstr | PathrequiredLocal file. Pillow extracts dimensions client-side
folder_pathstr"/"Virtual folder (e.g. /cars). Storage paths are immutable
filenamestr | NonebasenameOverride the destination filename
content_typestr | NoneinferredOverride the MIME type (inferred from extension)
from pathlib import Path

dataset = client.datasets.get("my-dataset")
image = client.images.upload(
    dataset_id=dataset.id,
    file_path=Path("./photo.jpg"),
    folder_path="/cars",          # virtual folder on the dataset
)
# Step 1 — request a signed upload URL.
curl -s -X POST "https://api.pictograph.io/api/v1/developer/images/upload-url" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "dataset_id": "<dataset-uuid>",
    "filename": "photo.jpg",
    "folder_path": "/cars",
    "content_type": "image/jpeg"
  }'

# Step 2 — PUT the raw bytes to the returned upload_url (no API key on this URL).
curl -s -X PUT "<upload_url>" \
  -H "Content-Type: image/jpeg" \
  --upload-file ./photo.jpg

# Step 3 — register the uploaded blob (JSON; same field names as step 1 —
# the server derives storage paths itself, nothing storage-internal round-trips).
curl -s -X POST "https://api.pictograph.io/api/v1/developer/images/register" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "dataset_id": "<dataset-uuid>",
    "filename": "photo.jpg",
    "folder_path": "/cars",
    "file_size": 123456,
    "content_type": "image/jpeg",
    "width": 1920,
    "height": 1080
  }'

Register returns the full canonical Image ({"data": {...}}) — no follow-up metadata fetch needed. Raises ConflictError (409) if a file with the same name already exists in the same folder.

If you’d rather hand the bytes to the backend in one request, POST /upload relays the file and does all three steps server-side:

curl -s -X POST "https://api.pictograph.io/api/v1/developer/images/upload" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY" \
  -F "dataset_id=<dataset-uuid>" \
  -F "folder_path=/cars" \
  -F "file=@./photo.jpg"

Supported extensions: .jpg, .jpeg, .png, .webp, .bmp, .tif, .tiff, .gif, .heic. HEIC is auto-converted to PNG server-side.

bulk_upload

Upload many local files to a dataset in one efficient pass. Up to 500 files per call. Rather than the per-file three-step flow, this makes one bulk-upload-url call, PUTs each file straight to storage, then makes one bulk-register call — two round-trips plus N PUTs, not 3N. A filename collision lands in failed rather than failing the whole batch.

ArgTypeDefaultNotes
dataset_idstrrequiredUUID of the destination dataset
file_pathsSequence[str | Path]requiredLocal files. Pillow extracts dimensions client-side
folder_pathstr"/"Virtual folder for all files in the batch
result = client.images.bulk_upload(
    "dataset-uuid",
    ["a.jpg", "b.jpg"],
    folder_path="/",
)
print(result.count, "registered,", len(result.failed), "failed")
for img in result.succeeded:   # each a full Image
    print(img.filename, img.id, img.status)
# Step 1 — request signed upload URLs for the whole batch.
curl -s -X POST "https://api.pictograph.io/api/v1/developer/images/bulk-upload-url" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "dataset_id": "<dataset-uuid>",
    "images": [
      {"filename": "a.jpg", "folder_path": "/", "content_type": "image/jpeg"},
      {"filename": "b.jpg", "folder_path": "/", "content_type": "image/jpeg"}
    ]
  }'

# Step 2 — PUT each file's bytes to its returned upload_url (no API key on these URLs).
curl -s -X PUT "<upload_url_for_a>" -H "Content-Type: image/jpeg" --upload-file ./a.jpg

# Step 3 — register the uploaded blobs in one call.
curl -s -X POST "https://api.pictograph.io/api/v1/developer/images/bulk-register" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "dataset_id": "<dataset-uuid>",
    "images": [
      {"filename": "a.jpg", "folder_path": "/", "file_size": 12345,
       "content_type": "image/jpeg", "width": 640, "height": 480},
      {"filename": "b.jpg", "folder_path": "/", "file_size": 23456,
       "content_type": "image/jpeg", "width": 800, "height": 600}
    ]
  }'

The REST response is {"data": {"succeeded": [...], "failed": [...], "count": N}}succeeded carries the full canonical image per registered row, failed carries {filename, folder_path, error} per declined item. The SDK returns BulkUploadResult with succeeded (list[Image]), failed (list[BulkUploadFailure]), and count.

download

Stream the original image bytes to a local file (chunked, safe for large images).

ArgTypeDefault
image_idstrrequired
output_pathstr | Pathrequired
client.images.download("img-uuid-1", output_path="./photo.jpg")
curl -s "https://api.pictograph.io/api/v1/developer/images/img-uuid-1" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY" \
  -o ./photo.jpg

The bytes are served via Cloud CDN with 30-day edge caching, so repeat downloads are fast.

delete

Soft-delete (archive) by default. Set permanent=True to free the stored bytes. This is irreversible.

client.images.delete("img-uuid-1")                  # archive (recoverable)
client.images.delete("img-uuid-1", permanent=True)  # permanent
# Archive (recoverable)
curl -s -X DELETE "https://api.pictograph.io/api/v1/developer/images/img-uuid-1" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY"

# Permanent (irreversible, admin/owner role)
curl -s -X DELETE "https://api.pictograph.io/api/v1/developer/images/img-uuid-1?permanent=true" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY"

Permanent deletes require admin+ role on the API key. Permanent delete is reference-counted: a blob still referenced by a fork of the image is retained.

review

Approve or request changes on an image in the annotation review workflow. approve marks the image complete (annotations accepted); request_changes sends it back to annotate with an optional note the annotator sees in the editor. Returns the image’s new status. Requires member+ role.

This is the programmatic entry point for QA automation — e.g. auto-approve high-confidence predictions and bounce low-confidence ones for human review:

# Approve — accept the annotations, mark complete
client.images.review("img-uuid-1", "approve")            # -> "complete"

# Request changes — send back to the annotator with a note
client.images.review(
    "img-uuid-2", "request_changes", note="tighten the left car bbox"
)                                                         # -> "annotate"

# QA loop: bounce every low-confidence image for a human to fix
for img in client.images.iter(dataset_id, min_confidence_lt=0.6):
    client.images.review(img.id, "request_changes", note="model unsure — please verify")
# Approve
curl -s -X POST "https://api.pictograph.io/api/v1/developer/images/img-uuid-1/review" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY" -H "Content-Type: application/json" \
  -d '{"action": "approve"}'

# Request changes with a note
curl -s -X POST "https://api.pictograph.io/api/v1/developer/images/img-uuid-2/review" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY" -H "Content-Type: application/json" \
  -d '{"action": "request_changes", "note": "tighten the left car bbox"}'

From the CLI:

pictograph images review img-uuid-1                              # approve
pictograph images review img-uuid-2 --request-changes -n "fix the bbox"

split — dataset splits

Assign an image to a train / val / test dataset split (or clear it), and filter a dataset by split. This is the programmatic side of the grid’s “Split” control — organize a dataset once, then pull each partition for training.

# One-call "Rebalance" — assign the WHOLE dataset a ratio split (the fast path).
counts = client.images.assign_splits(dataset_id, train=70, val=20, test=10)
# → {"processed": 100, "train": 70, "val": 20, "test": 10}

train = client.images.iter(dataset_id, split="train").all()   # filter to the train split
client.images.set_split(some_id, "test")                      # override one image
client.images.set_split(some_id, None)                        # clear an assignment
# Assign
curl -s -X POST "https://api.pictograph.io/api/v1/developer/images/img-uuid/split" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY" -H "Content-Type: application/json" \
  -d '{"split": "train"}'

# Filter a dataset by split
curl -s "https://api.pictograph.io/api/v1/developer/images/?dataset_id=DATASET_UUID&split=train" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY"
pictograph images split img-uuid train        # assign
pictograph images split img-uuid none         # clear
pictograph images list my-dataset --split val # filter

assign_splits — one-click Rebalance

client.images.assign_splits(dataset_id, train=70, val=20, test=10, seed=42) partitions the WHOLE (non-archived) dataset in one atomic call — the fast path vs. per-image set_split, and the same “Rebalance” one-click the grid offers. val/test take their floor and train the remainder, so counts sum to the total exactly (a 0 weight yields 0 images, e.g. 80/20/0); the shuffle is deterministic under seed. Member role or higher. Pairs with exports.create(organize_by_split=True).

ArgTypeDefaultNotes
dataset_idstrrequiredDataset UUID
train / val / testint70 / 20 / 10Integer percentage weights
seedint42Deterministic shuffle seed
pictograph images rebalance DATASET_UUID --train 80 --val 10 --test 10

curl -s -X POST "https://api.pictograph.io/api/v1/developer/images/assign-splits" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY" -H "Content-Type: application/json" \
  -d '{"dataset_id": "DATASET_UUID", "train": 70, "val": 20, "test": 10}'

Bulk uploads

For directories of images, use the workflow:

from pictograph.pipelines import upload_dataset_from_folder

report = upload_dataset_from_folder(
    client,
    "my-dataset",
    folder="./photos",
    organize_by_class=True,   # subdirectory → virtual folder
    parallel=True,
    max_workers=8,
)
print(report.images_uploaded, len(report.failures))
# The workflow batches signed URLs and registrations server-side. Use these
# endpoints directly if you orchestrate uploads yourself:
#   POST /api/v1/developer/images/bulk-upload-url   (request many signed URLs)
#   POST /api/v1/developer/images/bulk-register     (register many uploaded blobs)
# Both accept up to 500 images per call.
curl -s -X POST "https://api.pictograph.io/api/v1/developer/images/bulk-upload-url" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "dataset_id": "<dataset-uuid>",
    "images": [
      {"filename": "a.jpg", "folder_path": "/cars", "content_type": "image/jpeg"},
      {"filename": "b.jpg", "folder_path": "/cars", "content_type": "image/jpeg"}
    ]
  }'

See Quick Start for the full workflow surface.

Common errors

StatusExceptionCause
404NotFoundErrorimage_id or dataset doesn’t exist, or belongs to another org
409ConflictErrorFilename collision in the same virtual folder
403ForbiddenErrorUpload requires member+; permanent delete requires admin+ role
400ApiErrorInvalid filename or folder path, or the file is not an image
Copied to clipboard