Upload images
Walk a local directory and upload every image to a dataset - parallel, idempotent, and directory-structure aware.
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
- Images - the underlying
client.images.upload() - SAM3 auto-annotation - label what you just uploaded