PIL Image
NumPy
image conversion
Python
data manipulation

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

ML System Design practice on Codemia

Design recommenders, ranking systems and training pipelines the way ML interviews actually ask for them, with worked solutions.

Practice ML system design

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)

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

ML System Design practice on Codemia

Design recommenders, ranking systems and training pipelines the way ML interviews actually ask for them, with worked solutions.

Practice ML system design

All Rights Reserved.