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)
  • autocontrast needs an L or RGB image and returns the same mode.
  • equalize can amplify noise in a nearly uniform image; autocontrast is usually the gentler option.
  • colorize takes a greyscale image and maps black and white to the two colours you supply.

Placement operations

OperationBehaviour
ImageOps.padScales to fit and pads to the exact size, keeping the whole image
ImageOps.fitScales to fill and crops the overflow, discarding edges
ImageOps.containScales down to fit, never enlarges beyond the original
ImageOps.coverScales up to cover, cropping as needed
ImageOps.expandAdds a border of a given width and colour
ImageOps.cropRemoves a border of a given width
ImageOps.mirror / flipHorizontal and vertical reflection
ImageOps.exif_transposeApplies 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.

Resizing, cropping and rotating Filters and enhancement

Last refreshed 2026-09-18.