PIL Image
NumPy
image conversion
Python
data manipulation

How do I convert a PIL Image into a NumPy array?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

To convert a PIL (Pillow) image into a NumPy array, use numpy.array(image) or numpy.asarray(image). The resulting array has shape (height, width, channels) for color images and (height, width) for grayscale. np.array() creates a writable copy, while np.asarray() creates a read-only view when possible. To convert back, use Image.fromarray(array). This conversion is essential for image processing with libraries like OpenCV, scikit-image, TensorFlow, and PyTorch.

Basic Conversion

python
1from PIL import Image
2import numpy as np
3
4# Open an image
5img = Image.open("photo.jpg")
6
7# Convert to NumPy array
8arr = np.array(img)
9
10print(type(arr))     # <class 'numpy.ndarray'>
11print(arr.shape)     # (480, 640, 3) — height x width x channels
12print(arr.dtype)     # uint8
13print(arr.min(), arr.max())  # 0 255

Array Shape by Image Mode

Different PIL image modes produce different array shapes:

python
1from PIL import Image
2import numpy as np
3
4# RGB — 3 channels
5rgb_img = Image.open("photo.jpg").convert("RGB")
6rgb_arr = np.array(rgb_img)
7print(rgb_arr.shape)  # (480, 640, 3)
8
9# RGBA — 4 channels (with alpha/transparency)
10rgba_img = Image.open("icon.png").convert("RGBA")
11rgba_arr = np.array(rgba_img)
12print(rgba_arr.shape)  # (480, 640, 4)
13
14# Grayscale — 2D array (no channel dimension)
15gray_img = Image.open("photo.jpg").convert("L")
16gray_arr = np.array(gray_img)
17print(gray_arr.shape)  # (480, 640)
18
19# Binary (1-bit) — 2D array of True/False
20bw_img = Image.open("photo.jpg").convert("1")
21bw_arr = np.array(bw_img)
22print(bw_arr.shape, bw_arr.dtype)  # (480, 640) bool
PIL ModeChannelsArray ShapeDtype
L (grayscale)1(H, W)uint8
RGB3(H, W, 3)uint8
RGBA4(H, W, 4)uint8
1 (binary)1(H, W)bool
F (float)1(H, W)float32
I (32-bit int)1(H, W)int32

np.array vs np.asarray

python
1from PIL import Image
2import numpy as np
3
4img = Image.open("photo.jpg")
5
6# np.array — creates a writable COPY
7arr_copy = np.array(img)
8arr_copy[0, 0] = [255, 0, 0]  # OK — modifiable
9
10# np.asarray — creates a read-only VIEW (shares memory when possible)
11arr_view = np.asarray(img)
12# arr_view[0, 0] = [255, 0, 0]  # ValueError: assignment destination is read-only
13
14# Make it writable by copying
15arr_writable = np.asarray(img).copy()
16arr_writable[0, 0] = [255, 0, 0]  # OK

Use np.array() when you plan to modify pixel values. Use np.asarray() when you only need to read, as it avoids an unnecessary memory copy.

Converting Back: NumPy to PIL

python
1from PIL import Image
2import numpy as np
3
4# Create or modify a NumPy array
5arr = np.random.randint(0, 256, (100, 100, 3), dtype=np.uint8)
6
7# Convert to PIL Image
8img = Image.fromarray(arr)
9img.save("output.png")
10
11# Grayscale array
12gray_arr = np.random.randint(0, 256, (100, 100), dtype=np.uint8)
13gray_img = Image.fromarray(gray_arr, mode="L")
14gray_img.save("gray_output.png")

Handling Float Arrays

python
1# Float arrays (0.0 to 1.0) must be converted to uint8 first
2float_arr = np.random.rand(100, 100, 3).astype(np.float32)
3
4# Scale to 0-255 and convert dtype
5uint8_arr = (float_arr * 255).astype(np.uint8)
6img = Image.fromarray(uint8_arr)

Working with OpenCV

OpenCV uses BGR channel order while PIL uses RGB:

python
1import cv2
2from PIL import Image
3import numpy as np
4
5# PIL to OpenCV (RGB → BGR)
6pil_img = Image.open("photo.jpg")
7cv_img = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
8
9# OpenCV to PIL (BGR → RGB)
10cv_result = cv2.GaussianBlur(cv_img, (5, 5), 0)
11pil_result = Image.fromarray(cv2.cvtColor(cv_result, cv2.COLOR_BGR2RGB))

Working with TensorFlow/PyTorch

python
1import numpy as np
2from PIL import Image
3
4img = Image.open("photo.jpg").resize((224, 224))
5arr = np.array(img)
6
7# TensorFlow — expects (batch, height, width, channels)
8import tensorflow as tf
9tensor = tf.constant(arr[np.newaxis, ...], dtype=tf.float32) / 255.0
10print(tensor.shape)  # (1, 224, 224, 3)
11
12# PyTorch — expects (batch, channels, height, width)
13import torch
14tensor = torch.from_numpy(arr).permute(2, 0, 1).unsqueeze(0).float() / 255.0
15print(tensor.shape)  # torch.Size([1, 3, 224, 224])

Pixel Manipulation Examples

python
1from PIL import Image
2import numpy as np
3
4img = Image.open("photo.jpg")
5arr = np.array(img)
6
7# Invert colors
8inverted = 255 - arr
9Image.fromarray(inverted).save("inverted.jpg")
10
11# Increase brightness
12bright = np.clip(arr.astype(np.int16) + 50, 0, 255).astype(np.uint8)
13Image.fromarray(bright).save("bright.jpg")
14
15# Extract red channel only
16red_only = arr.copy()
17red_only[:, :, 1] = 0  # Zero out green
18red_only[:, :, 2] = 0  # Zero out blue
19Image.fromarray(red_only).save("red_channel.jpg")
20
21# Crop using array slicing
22cropped = arr[100:300, 50:250]  # rows 100-299, cols 50-249
23Image.fromarray(cropped).save("cropped.jpg")

Common Pitfalls

  • Forgetting that PIL uses RGB while OpenCV uses BGR: Passing a PIL-converted array directly to OpenCV functions produces wrong colors (red and blue swapped). Always use cv2.cvtColor() to convert between RGB and BGR.
  • Modifying a read-only np.asarray result: np.asarray(img) may return a read-only array. Attempting to modify pixels raises ValueError: assignment destination is read-only. Use np.array(img) or .copy() when you need to modify values.
  • Passing float arrays to Image.fromarray without converting to uint8: Image.fromarray expects uint8 arrays (0-255) for RGB images. Passing a float array (0.0-1.0) produces garbage output. Multiply by 255 and cast with .astype(np.uint8) first.
  • Confusing (height, width) axis order with (width, height): NumPy arrays store images as (height, width, channels) — rows first, columns second. PIL's Image.size returns (width, height). This mismatch causes confusion when resizing or creating new images.
  • Not converting image mode before array conversion: A palette image (mode P) or CMYK image produces unexpected array shapes. Always call .convert("RGB") or .convert("L") before converting to ensure a predictable array shape.

Summary

  • np.array(img) converts a PIL image to a writable NumPy array
  • np.asarray(img) creates a read-only view (more memory efficient for reading)
  • RGB images produce (H, W, 3) arrays; grayscale produces (H, W)
  • Convert back with Image.fromarray(arr) — ensure uint8 dtype for RGB images
  • Use cv2.cvtColor() when bridging between PIL (RGB) and OpenCV (BGR)

Course illustration
Course illustration

All Rights Reserved.