Tile & augment
Slice images into tiles for small-object detection, or make flipped, rotated and color-jittered variants. Boxes, polygons and keypoints follow each transform.
Tiling slices each image into a grid, so small objects fill more of each frame. Augmenting makes randomized variants, so the model sees more variety. Annotations follow every transform. To do both, tile first, then augment the tiled dataset.
Tile
For aerial, satellite and microscope images:
from pictograph import Client, TileReport
client = Client()
report: TileReport = client.images.tile(
dataset="aerial",
rows=2,
cols=2,
overlap=0.1,
into="aerial-tiled",
)
print(f"{report.tiles_created} tiles from {report.source_images} images")
pictograph tile dataset aerial --into aerial-tiled --rows 2 --cols 2 --overlap 0.1
overlap extends each tile past its edge, so an object on a boundary appears whole in a
neighbor. include_empty=False (--exclude-empty) skips tiles with no annotations. Every
argument: tile.
Augment
from pictograph import AugmentReport, Client
from pictograph.augment import Brightness, HorizontalFlip, Rotate
client = Client()
report: AugmentReport = client.images.augment(
dataset="road-signs",
ops=[HorizontalFlip(), Rotate((-15, 15)), Brightness((0.8, 1.2))],
multiplier=3,
into="road-signs-aug",
)
print(f"{report.variants_created} variants of {report.source_images} images")
pictograph augment dataset road-signs --into road-signs-aug --multiplier 3 --flip --rotate 15 --brightness 0.2
A CLI flag sets one op’s range: --rotate 15 is Rotate((-15, 15)); pictograph augment ops
lists them. drop_classes (--drop-class) removes those classes’ annotations first, and
skip_empty=True (--skip-empty) skips images left with none. Every argument:
augment.
Ops
A magnitude is a value or a (low, high) range sampled each time; p is the chance an op
runs.
Geometric ops change the image and move its annotations:
| Op | What it does |
|---|---|
HorizontalFlip(p=0.5) |
Mirrors left to right |
VerticalFlip(p=0.5) |
Mirrors top to bottom |
Rotate90(k=1) |
Rotates 90, 180 or 270 degrees with no loss (k=None picks one) |
Rotate(degrees=(-15, 15)) |
Rotates by any angle, growing the canvas so nothing is cut |
Resize(width, height) |
Resizes to a fixed size |
Crop(scale=(0.8, 1.0)) |
Crops a random region that keeps this fraction of each side |
Shear(degrees=(-10, 10)) |
Shears horizontally, keeping the canvas |
Photometric ops change pixels only:
| Op | What it does |
|---|---|
Brightness(factor=(0.8, 1.2)) |
Scales brightness |
Contrast(factor=(0.8, 1.2)) |
Scales contrast |
Saturation(factor=(0.8, 1.2)) |
Scales color saturation |
HueShift(degrees=(-20, 20)) |
Rotates the hue |
Grayscale(p=1.0) |
Converts to grayscale |
Blur(radius=(0.0, 2.0)) |
Applies a Gaussian blur |
Noise(amount=(0.0, 0.08)) |
Adds luminance noise |
CutOut(size=(0.1, 0.3), count=1) |
Erases random rectangles |
Output
into names the output dataset, created if missing with the source’s classes. Without into,
new images go into the source dataset under /tiles or /augmented. Generated images count
toward your image quota.
Geometry
- Boxes and polygons are clipped to their tile or crop; keypoints outside it are dropped.
- A rotated box grows to enclose its rotated corners.
- An annotation with less than
min_visibilityof its area left is removed. - Tiling is deterministic; augmenting is too with a fixed
seed.
Local engines
Source: tile/_tiler.py, augment/_engine.py
Both run on a local image and its annotations, with no API call:
from pictograph import Client
from pictograph.tile import tile_image
client = Client()
annotations = client.annotations.get(
dataset="aerial",
image="aerial.jpg",
)
tiles = tile_image("aerial.jpg", annotations, rows=2, cols=2, overlap=0.1)
for t in tiles:
t.image.save(f"tile_r{t.row}_c{t.col}.jpg")
print(len(t.annotations), "annotations in this tile", t.origin)
Each Tile has the cropped image, its shifted annotations, its row and col, and its
origin (its top-left corner in the source).
from pictograph import Client
from pictograph.augment import Augmenter, Brightness, HorizontalFlip, Rotate
client = Client()
annotations = client.annotations.get(
dataset="road-signs",
image="photo.jpg",
)
aug = Augmenter([HorizontalFlip(), Rotate((-15, 15)), Brightness((0.8, 1.2))], seed=42)
image, moved = aug(image="photo.jpg", annotations=annotations) # one variant
variants = aug.generate(image="photo.jpg", annotations=annotations, n=3) # three variants
print(len(moved), "annotations moved;", len(variants), "variants")
Augmenter takes a file path or a Pillow image, and annotations as models or dicts.