Images
List a dataset's images, and upload, fetch, download, or delete them.
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.
| Arg | Type | Default | Notes |
|---|---|---|---|
dataset_id | str | required | UUID of the dataset to list |
folder_path | str | None | None | Restrict to one virtual folder (e.g. /train); None lists all folders |
status | str | None | None | Restrict to a stage: new / annotate / review / complete |
include_archived | bool | False | Include soft-deleted (archived) images |
min_confidence_lt | float | None | None | Active-learning filter: keep only images whose model confidence is below this (0–1). None applies no filter |
limit | int | 100 | Page size (max 1000) |
offset | int | 0 | Pagination 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.
| Arg | Type | Default | Notes |
|---|---|---|---|
dataset_id | str | required | UUID of the dataset to iterate |
folder_path | str | None | None | Restrict to one virtual folder; None iterates all |
status | str | None | None | Restrict to a stage; None iterates all |
include_archived | bool | False | Include archived images |
min_confidence_lt | float | None | None | Active-learning filter: iterate only images with model confidence below this (0–1) |
page_size | int | 100 | Items per backend round-trip (max 1000) |
max_total | int | None | None | Stop 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.
| Arg | Type | Default | Notes |
|---|---|---|---|
dataset_id | str | required | UUID of the destination dataset |
file_path | str | Path | required | Local file. Pillow extracts dimensions client-side |
folder_path | str | "/" | Virtual folder (e.g. /cars). Storage paths are immutable |
filename | str | None | basename | Override the destination filename |
content_type | str | None | inferred | Override 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.
| Arg | Type | Default | Notes |
|---|---|---|---|
dataset_id | str | required | UUID of the destination dataset |
file_paths | Sequence[str | Path] | required | Local files. Pillow extracts dimensions client-side |
folder_path | str | "/" | 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).
| Arg | Type | Default |
|---|---|---|
image_id | str | required |
output_path | str | Path | required |
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).
| Arg | Type | Default | Notes |
|---|---|---|---|
dataset_id | str | required | Dataset UUID |
train / val / test | int | 70 / 20 / 10 | Integer percentage weights |
seed | int | 42 | Deterministic 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
| Status | Exception | Cause |
|---|---|---|
| 404 | NotFoundError | image_id or dataset doesn’t exist, or belongs to another org |
| 409 | ConflictError | Filename collision in the same virtual folder |
| 403 | ForbiddenError | Upload requires member+; permanent delete requires admin+ role |
| 400 | ApiError | Invalid filename or folder path, or the file is not an image |