Large images: lazy loading, draft and reduce

Pillow · Advanced · 10 min

Decoding a 50-megapixel photo to make a 200-pixel thumbnail wastes seconds and hundreds of megabytes if done naively.

Memory is roughly width x height x bands bytes once decoded: a 8000x6000 RGB photo needs about 144 MB, before any copies.

Pillow helps in several ways:

  • Lazy open: Image.open reads only the header, so checking size, format and mode of thousands of files is cheap. Reject oversize files before decoding.
  • `im.draft(mode, size)` (JPEG and some others): asks the decoder to decode at a reduced scale (1/2, 1/4 or 1/8) directly using the JPEG's DCT structure. It must be called before the image is loaded. thumbnail() calls draft automatically, which is why it is so fast on JPEGs.
  • `im.reduce(factor)`: fast integer downscaling by box averaging; resize(..., reducing_gap=2.0) uses it internally before the precise resampling step, trading a little quality for large speedups.
  • Cropping doesn't save decoding: im.crop() on a freshly opened file calls load() and decodes the whole image first, so crop after downscaling when you can.

Close files you open in loops. Opened-but-unloaded images keep file handles; after load(), single-frame images close them automatically, but multi-frame ones stay open. with Image.open(p) as im: is the reliable pattern.

Image.MAX_IMAGE_PIXELS guards against decompression bombs and will stop absurdly large images; don't just set it to None for untrusted input.

Example

import time
from PIL import Image

Image.effect_noise((4000, 3000), 40).convert("RGB").save("big.jpg", quality=90)

with Image.open("big.jpg") as im:
    print(im.size, im.mode)               # (4000, 3000) RGB -- header only

t = time.perf_counter()
with Image.open("big.jpg") as im:
    full = im.resize((400, 300))          # decodes all 12 megapixels
t_full = time.perf_counter() - t

t = time.perf_counter()
with Image.open("big.jpg") as im:
    im.draft("RGB", (400, 300))           # decode at 1/8 scale
    print(im.size)                        # (500, 375)
    fast = im.resize((400, 300))
t_draft = time.perf_counter() - t
print(t_draft < t_full)                   # True, typically several times faster

print(Image.open("big.jpg").reduce(4).size)   # (1000, 750)

Remember

  • Opening is lazy: inspect size and format before decoding
  • JPEG draft mode decodes directly at 1/2, 1/4 or 1/8 scale; thumbnail uses it
  • reduce() and reducing_gap speed up big downscales
  • Use with Image.open(...) in loops to release file handles

Watch out for

  • Calling draft() after the image has already been loaded, when it has no effect.

Check yourself

  1. Why is `im.thumbnail((200, 200))` on a large JPEG much faster than `im.resize((200, 150))`?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.draft