Sign in Get started

Upload images

Walk a local directory and upload every image to a dataset - parallel, idempotent, and directory-structure aware.

View as Markdown

client.images.upload_from_directory() walks a local directory, creates the destination dataset if needed, and uploads everything through a thread pool. Re-runs are idempotent: duplicate filenames are skipped, not failed.

Source: resources/images.py

from pictograph import Client, UploadReport

client = Client()

report: UploadReport = client.images.upload_from_directory(
    dataset_name="road-signs",
    directory="./road_signs",
)
print(f"{report.images_uploaded} uploaded, {report.images_skipped} skipped")

Signature

client.images.upload_from_directory(
    dataset_name: str,
    directory: str | Path,
    *,
    organize_by_class: bool = True,
    preserve_structure: bool = False,
    parallel: bool = True,
    max_workers: int = 8,
    skip_existing: bool = True,
    create_if_missing: bool = True,
    progress: Callable[[int, int, str | None], None] | None = None,
) -> UploadReport
Argument Default Purpose
dataset_name required Destination dataset
directory required Local directory (walked recursively)
organize_by_class True First-level subdirectories become virtual directories
preserve_structure False Recreate the whole directory tree instead
parallel True Use a thread pool
max_workers 8 Pool size - higher values risk hitting the rate limit
skip_existing True Treat duplicate-filename conflicts as skips, not failures
create_if_missing True Create the dataset if it doesn’t exist (else NotFoundError)
progress None (completed, total, filename) callback fired after each file

Supported extensions: .jpg, .jpeg, .png, .webp, .bmp, .tif, .tiff, .gif, .heic.

Where each file lands

organize_by_class=True (the default) uses only the first subdirectory level, which is what you want for ImageFolder-style datasets where the top directory is the class. ./road_signs/stop/night/005.jpg still lands in /stop.

./road_signs/
├── stop/         → /stop
│   ├── 001.jpg
│   └── 002.jpg
├── yield/        → /yield
│   └── 003.jpg
└── 004.jpg       → / (root)

preserve_structure=True recreates the tree exactly as it is on disk.

report = client.images.upload_from_directory(
    dataset_name="road-signs",
    directory="./road_signs",
    preserve_structure=True,
)
./road_signs/
├── stop/
│   ├── night/
│   │   └── 005.jpg   → /stop/night
│   └── 001.jpg       → /stop
└── 004.jpg           → / (root)

organize_by_class=False puts every file at the root.

Image names are unique per directory, not per dataset - the same filename in two subdirectories stays two distinct images (/stop/001.jpg and /yield/001.jpg don’t collide).

The CLI mirrors all three, and matches the web app’s Add → Directory:

pictograph images upload-directory road-signs ./road_signs              # structure preserved
pictograph images upload-directory road-signs ./road_signs --by-class   # first level only
pictograph images upload-directory road-signs ./road_signs --flat       # everything at root

What comes back

@dataclass
class UploadReport:
    dataset_name: str
    images_attempted: int
    images_uploaded: int
    images_skipped: int
    failures: list[UploadFailure]  # each carries .path and .reason

    @property
    def success(self) -> bool: ...

Because skip_existing defaults to True, re-running the same call is safe: the second run reports every file as skipped rather than failed. Set skip_existing=False to force a re-upload, and conflicts are recorded as failures instead.

To drive a progress bar, pass a callback. It fires once per file, whether that file succeeded or failed:

def on_progress(done: int, total: int, filename: str | None) -> None:
    print(f"[{done}/{total}] {filename}")

client.images.upload_from_directory(
    dataset_name="road-signs",
    directory="./road_signs",
    progress=on_progress,
)

Errors

Raised Cause
FileNotFoundError directory doesn’t exist or isn’t a directory
NotFoundError dataset_name missing and create_if_missing=False

Per-file errors (network, validation, conflict) land in report.failures, not an exception.

See also

Copied to clipboard