Pillow and NumPy arrays

Pillow · Practical · 10 min

ML pipelines, OpenCV and scientific code all work on arrays, so moving images in and out of Pillow is routine.

Pillow images implement the array interface, so np.asarray(im) gives an array without writing any conversion code:

  • RGB becomes shape (height, width, 3), dtype uint8
  • L becomes (height, width); RGBA (height, width, 4)
  • 1 mode becomes a bool array; I;16 gives uint16, F gives float32

Note the axis order flips relative to im.size, which is (width, height). The array from np.asarray is read-only; use np.array(im) for a writable copy.

Image.fromarray(arr) goes the other way, inferring the mode from shape and dtype: uint8 with 3 channels becomes RGB, 2-D uint8 becomes L, float32 becomes F. If your array is float64 in the range 0 to 1, as many pipelines produce, scale and convert first: (arr * 255).clip(0, 255).astype(np.uint8). Using fromarray's mode= argument to reinterpret the data as a different type is deprecated; convert the array's dtype instead, or call .convert() afterwards.

OpenCV uses BGR channel order while Pillow uses RGB, so reverse the last axis when crossing between them: Image.fromarray(bgr[..., ::-1]).

For performance, prefer whole-array NumPy operations over getpixel/putpixel loops, then convert back once.

Example

import numpy as np
from PIL import Image

im = Image.new("RGB", (4, 2), (10, 20, 30))
arr = np.asarray(im)
print(arr.shape, arr.dtype, arr.flags.writeable)   # (2, 4, 3) uint8 False

work = np.array(im)                                # writable copy
work[:, :2] = [255, 0, 0]                          # left half red
out = Image.fromarray(work)
print(out.mode, out.size, out.getpixel((0, 0)))    # RGB (4, 2) (255, 0, 0)

prob = np.random.default_rng(0).random((2, 4))     # float64 in [0, 1)
gray = Image.fromarray((prob * 255).astype(np.uint8))
print(gray.mode)                                   # L
print(Image.fromarray(prob.astype(np.float32)).mode)   # F

bgr = work[..., ::-1]                              # pretend OpenCV array
print(Image.fromarray(np.ascontiguousarray(bgr[..., ::-1])).getpixel((0, 0)))  # (255, 0, 0)

Remember

  • np.asarray(im) gives (h, w, channels); it is read-only, np.array copies
  • Image.fromarray infers the mode from shape and dtype
  • Scale float images to uint8 before making RGB/L images
  • Swap BGR and RGB when moving between OpenCV and Pillow

Watch out for

  • Passing a float64 array in 0..1 to Image.fromarray and getting an F-mode or black image instead of a photo.

Check yourself

  1. An RGB image has `im.size == (640, 480)`. What is `np.asarray(im).shape`?

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.fromarray

Builds on

Unlocks

Nothing builds on this yet.