Exports
Build and download dataset exports in COCO, YOLO, CVAT, Pascal VOC, LabelMe, CSV, or canonical Pictograph JSON.
An export is a ZIP of an annotated dataset in a chosen format, optionally embedding the original image files. Export builds run server-side (a few seconds for hundreds of images, longer for tens of thousands).
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
Addressing + envelope. An export is identified by its UUID (/exports/{id})
or by its (dataset_name, export_name) pair under /exports/by-name/{dataset}/{export}
— the :uuid id route and the /by-name/ prefix never shadow each other. A
single export is {"data": {…}}; a collection is {"data": [...], "pagination": {"limit", "offset", "total", "has_more"}} where total is the
org-wide count. A signed download URL comes back as {"data": {"download_url": …, "expires_in_minutes": 60}}.
Formats
format | Notes |
|---|---|
pictograph (default) | Canonical Pictograph JSON — the wire format the SDK consumes |
darwin | Darwin V7 JSON 2.0 (one per image) — drop-in compatible with V7 dataset imports |
coco | COCO instance segmentation / object detection (Darwin-consistent dialect: polygon holes export as RLE) |
yolo | YOLO darknet .txt files (one per image) |
yolo_obb | Oriented boxes — Ultralytics YOLO-OBB: class x1 y1 x2 y2 x3 y3 x4 y4, four corners normalized to [0,1]. This is the format yolo obb trains on. Every annotation type exports: a box is a rotated box at angle 0, a polygon reduces to its minimum-area rotated rectangle |
dota | Oriented boxes — the aerial/satellite standard (labelTxt/): four corners in absolute pixels plus the class name, so it needs no image dimensions and no class-index ordering |
cvat | CVAT XML — a rotated box exports natively as <box rotation="…">, an exact round-trip |
pascal_voc | Pascal VOC XML (one per image) |
datumaro | Intel Datumaro / CVAT’s native JSON dataset format |
labelme | LabelMe JSON (one per image) |
csv | Flat CSV — bbox annotations only |
Every format understands an oriented (rotated) box. Formats with a native rotated
representation (yolo_obb, dota, cvat) keep the angle exactly; the rest receive the
box’s four rotated corners as a polygon, so the shape survives losslessly even where
the parameterization cannot be expressed.
create
Build a new export. Defaults to wait=True, which blocks until the
ZIP is ready.
| Arg | Type | Default | Notes |
|---|---|---|---|
dataset_name | str | required | |
name | str | required | Unique within the dataset |
format | ExportFormat | "pictograph" | See table above |
include_images | bool | False | When True, the ZIP also bundles the original image bytes |
class_filter | list[str] | None | None | Limit to these class names |
status_filter | str | None | None | Image status filter (e.g. "complete") |
organize_by_split | bool | False | Organize the ZIP into train/ valid/ test/ folders by each image’s assigned split (see Train/valid/test split layout below) |
wait | bool | True | Block until terminal status |
poll_interval | float | 2.0 | Seconds between status checks |
timeout | float | 300.0 | Max seconds to wait |
export = client.exports.create(
"my-dataset",
"for-yolov8",
format="yolo",
include_images=True,
class_filter=["car", "truck"], # None = all classes
status_filter="complete", # None = all statuses
wait=True,
poll_interval=2.0,
timeout=600.0,
)
print(export.id, export.status, export.image_count, export.annotation_count)
curl -s -X POST "https://api.pictograph.io/api/v1/developer/exports/" \
-H "X-API-Key: $PICTOGRAPH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"dataset_name": "my-dataset", "name": "for-yolov8", "format": "yolo", "include_images": true, "class_filter": ["car", "truck"], "status_filter": "complete"}'
wait=False returns a pending / processing Export. Poll via
get or wait_for_completion.
list
Single-page list of exports for the organization. Optionally filter by dataset name or status.
| Arg | Type | Default | Notes |
|---|---|---|---|
dataset_name | str | None | None | Restrict to one dataset |
status | str | None | None | e.g. "completed" / "processing" / "failed" |
limit | int | 100 | Backend cap: 1000 |
offset | int | 0 | Page offset |
exports = client.exports.list(limit=20)
for e in exports:
print(e.dataset_name, e.name, e.status)
curl -s "https://api.pictograph.io/api/v1/developer/exports/?limit=20" \
-H "X-API-Key: $PICTOGRAPH_API_KEY"
iter
Auto-paging iterator over every export (transparently follows offset).
for e in client.exports.iter(page_size=50):
print(e.dataset_name, e.name, e.status)
# Page manually with limit + offset until fewer than `limit` rows return.
curl -s "https://api.pictograph.io/api/v1/developer/exports/?limit=50&offset=0" \
-H "X-API-Key: $PICTOGRAPH_API_KEY"
get
Fetch a single export by (dataset_name, export_name).
export = client.exports.get("my-dataset", "for-yolov8")
print(export.status, export.download_url)
curl -s "https://api.pictograph.io/api/v1/developer/exports/by-name/my-dataset/for-yolov8" \
-H "X-API-Key: $PICTOGRAPH_API_KEY"
Returns Export.
get_by_id
Fetch a single export by its UUID — the by-id complement to get’s
(dataset_name, export_name) lookup.
export = client.exports.get_by_id("export-uuid")
print(export.status, export.download_url)
curl -s "https://api.pictograph.io/api/v1/developer/exports/export-uuid" \
-H "X-API-Key: $PICTOGRAPH_API_KEY"
Returns Export. A cross-org or missing id is a 404.
download
Stream the ZIP to a local file (chunked). The SDK first fetches a signed download URL, then streams the bytes from storage.
| Arg | Type | Default | Notes |
|---|---|---|---|
dataset_name | str | required | |
export_name | str | required | |
output_path | str | Path | required | Local destination |
chunk_size | int | 8 MB | Streaming chunk size |
from pathlib import Path
client.exports.download(
"my-dataset", "for-yolov8",
output_path=Path("./my-dataset.zip"),
)
# Returns a signed download URL valid for 60 minutes; fetch the ZIP from it.
curl -s "https://api.pictograph.io/api/v1/developer/exports/by-name/my-dataset/for-yolov8/download" \
-H "X-API-Key: $PICTOGRAPH_API_KEY"
The download URL is a signed URL valid for 60 minutes — generated fresh on every call.
download_by_id
Stream the ZIP to a local file by export UUID — the by-id complement to
download. The SDK first fetches a signed download URL, then streams the bytes.
| Arg | Type | Default | Notes |
|---|---|---|---|
export_id | str | required | Export UUID |
output_path | str | Path | required | Local destination |
chunk_size | int | 8 MB | Streaming chunk size |
from pathlib import Path
path = client.exports.download_by_id("export-uuid", Path("./out.zip"))
print(path) # Path("./out.zip")
# Returns a signed download_url valid for 60 minutes; fetch the ZIP from it.
curl -s "https://api.pictograph.io/api/v1/developer/exports/export-uuid/download" \
-H "X-API-Key: $PICTOGRAPH_API_KEY"
Returns Path. A cross-org or missing id is a 404.
wait_for_completion
If you used wait=False, poll until the export reaches a terminal status.
export = client.exports.create("ds", "name", format="coco", wait=False)
# … later
export = client.exports.wait_for_completion("ds", "name", timeout=300.0)
# Poll the get endpoint until "status" becomes "completed" or "failed".
curl -s "https://api.pictograph.io/api/v1/developer/exports/by-name/ds/name" \
-H "X-API-Key: $PICTOGRAPH_API_KEY"
delete
Removes the export and the stored ZIP. Requires admin or owner role.
Other ongoing downloads of the same export will fail mid-stream.
client.exports.delete("my-dataset", "for-yolov8")
curl -s -X DELETE "https://api.pictograph.io/api/v1/developer/exports/by-name/my-dataset/for-yolov8" \
-H "X-API-Key: $PICTOGRAPH_API_KEY"
Train/valid/test split layout
Assign each image a split (train / val / test) with
client.images.set_split, then export
with organize_by_split=True to get a directly-trainable ZIP — images and
their annotation files are grouped into top-level train/ / valid/ / test/
folders (images with no assigned split go to train).
# 80/10/10, then export a directly-trainable YOLO layout
imgs = list(client.images.iter(dataset_name="my-dataset"))
import random; random.seed(0); random.shuffle(imgs)
n = len(imgs); n_train, n_val = int(n * 0.8), int(n * 0.1)
for i, im in enumerate(imgs):
client.images.set_split(im.id, "train" if i < n_train else "val" if i < n_train + n_val else "test")
export = client.exports.create(
"my-dataset", "yolo-split", format="yolo",
include_images=True, organize_by_split=True,
)
The resulting YOLO ZIP has one root data.yaml pointing at train: train/images,
val: valid/images, test: test/images (plus a root classes.txt), so it trains
with Ultralytics as-is. For COCO the manifest is written per split
(train/annotations.json, …); every per-image format nests its files under the
split folder. Without organize_by_split the flat layout is unchanged.
Class filtering
class_filter only includes annotations matching the given class names.
Images with no surviving annotations are still included if their
status matches status_filter — they get an empty annotation list.
Pass class_filter=None (default) to keep every annotation.
Common errors
| Status | Exception | Cause |
|---|---|---|
| 404 | NotFoundError | Dataset or export missing |
| 409 | ConflictError | Export name already exists in this dataset |
| 422 | ValidationError | Unknown format or status_filter |
| 403 | ForbiddenError | delete requires admin+ role |
| 408 | PollTimeoutError | wait=True timed out (export keeps building) |