ImageOps: autocontrast, pad, fit and montage
Normalise exposure, place an image in an exact canvas, crop avatars to squares, and build contact sheets without writing geometry by hand.
Tone operations
from PIL import Image, ImageOps
im = Image.open("scan.png").convert("RGB")
im = ImageOps.autocontrast(im, cutoff=1) # stretch to full range, ignoring 1% tails
im = ImageOps.equalize(im) # flatten the histogram
im = ImageOps.posterize(im, bits=4) # 16 levels per channel, a poster look
im = ImageOps.solarize(im, threshold=128) # invert values above the threshold
grey = ImageOps.grayscale(im) # luminance conversion
tinted = ImageOps.colorize(grey, black="navy", white="#fffbe6")
print(tinted.mode, ImageOps.invert(tinted.convert("RGB")).size)autocontrastneeds anLorRGBimage and returns the same mode.equalizecan amplify noise in a nearly uniform image;autocontrastis usually the gentler option.colorizetakes a greyscale image and maps black and white to the two colours you supply.
Placement operations
| Operation | Behaviour |
|---|---|
ImageOps.pad | Scales to fit and pads to the exact size, keeping the whole image |
ImageOps.fit | Scales to fill and crops the overflow, discarding edges |
ImageOps.contain | Scales down to fit, never enlarges beyond the original |
ImageOps.cover | Scales up to cover, cropping as needed |
ImageOps.expand | Adds a border of a given width and colour |
ImageOps.crop | Removes a border of a given width |
ImageOps.mirror / flip | Horizontal and vertical reflection |
ImageOps.exif_transpose | Applies the orientation tag and removes it |
avatar = ImageOps.fit(im, (256, 256), method=Image.Resampling.LANCZOS, centering=(0.5, 0.4))
avatar.save("avatar.webp", quality=82)
framed = ImageOps.expand(im, border=12, fill="white")
# a contact sheet: uniform cells, no arithmetic errors
cell = (300, 300)
files = ["a.jpg", "b.jpg", "c.jpg", "d.jpg"]
sheet = Image.new("RGB", (cell[0] * 2, cell[1] * 2), "white")
for i, path in enumerate(files):
tile = ImageOps.fit(Image.open(path).convert("RGB"), cell)
sheet.paste(tile, ((i % 2) * cell[0], (i // 2) * cell[1]))
sheet.save("sheet.jpg", quality=85)ImageOps.fit takes a centering tuple in the range 0 to 1. The default (0.5, 0.5) centres the crop, which is often wrong for portraits; (0.5, 0.3) usually keeps faces in frame.
Pad or fit?
Use pad whenever every part of the image matters, such as a product photo or a document scan. Use fit when the grid must look uniform, such as avatars and thumbnails, and accept that the edges are cropped.
⚠️
ImageOps.fit silently discards content outside the crop box, so a wide diagram squeezed into a square may lose its labels. If you cannot lose pixels, pad and accept the letterboxing instead.FAQ
How do I add a centred caption below an image?
Grow the canvas with
expand and draw into the added area with ImageDraw, or create a larger Image.new and paste the picture at an offset. Both keep the original pixels untouched.What does contain without enlarging mean in practice?
An image of 2000 px asked to fit inside 512 px becomes 512 px. A 200 px image asked for the same box stays 200 px, so upscaling never introduces invented detail.
Related
Resizing, cropping and rotating Filters and enhancement
Last refreshed 2026-09-18.