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
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:
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 Mode | Channels | Array Shape | Dtype |
L (grayscale) | 1 | (H, W) | uint8 |
RGB | 3 | (H, W, 3) | uint8 |
RGBA | 4 | (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
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
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
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:
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
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
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)