NumPy¶
In this lesson, we'll learn about ways to represent and manipulate images in Python. By the end of this lesson, students will be able to:
- Apply
ndarray
arithmetic and logical operators with numbers and other arrays. - Analyze the shape of an
ndarray
and index into a multidimensional array. - Apply arithmetic operators, indexing, and slicing to manipulate RGB images.
We'll need two new modules: imageio
, which provides utilities to read and write images in Python, and numpy
, which provides the data structures for representing images in Python.
import imageio.v3 as iio
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
Reading an image¶
Let's use imageio's imread
method to load a color picture as a grid of pixels. To then show an image, we can plot its pixels using the matplotlib function imshow
.
jax = iio.imread("jax.jpg")
plt.imshow(jax)
<matplotlib.image.AxesImage at 0x793fc2af7fd0>
We could also read this image in as a black-and-white image using the mode="L"
option to imread
and cmap="gray"
to imshow
. "L" stands for "luminance," a method to convert color to grayscale. "cmap" stands for "colormap," saying to treat pixels as grayscale.
jax = iio.imread("jax.jpg", mode="L")
plt.imshow(jax, cmap="gray")
<matplotlib.image.AxesImage at 0x793fd0f0f0d0>
What kind of Python object does imread
return?
type(jax)
numpy.ndarray
jax.shape
(600, 600)
jax
array([[109, 115, 115, ..., 48, 47, 45], [114, 116, 114, ..., 47, 47, 48], [122, 124, 122, ..., 45, 45, 47], ..., [116, 99, 94, ..., 79, 80, 83], [106, 93, 89, ..., 72, 81, 86], [105, 102, 100, ..., 76, 76, 94]], dtype=uint8)
Pandas uses NumPy to represent a Series
of values, so many element-wise operations should seem familiar. In fact, we can load an image into a Pandas DataFrame
and see that this grayscale image is really a 2-d array of grayscale values ranging from [0, 255].
pd.DataFrame(jax)
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ... | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 109 | 115 | 115 | 116 | 122 | 121 | 117 | 120 | 128 | 121 | ... | 49 | 49 | 46 | 47 | 47 | 48 | 48 | 48 | 47 | 45 |
1 | 114 | 116 | 114 | 113 | 118 | 119 | 116 | 117 | 131 | 124 | ... | 50 | 49 | 49 | 47 | 47 | 48 | 48 | 47 | 47 | 48 |
2 | 122 | 124 | 122 | 118 | 118 | 121 | 116 | 107 | 120 | 122 | ... | 50 | 50 | 50 | 47 | 45 | 47 | 47 | 45 | 45 | 47 |
3 | 123 | 123 | 124 | 121 | 115 | 115 | 116 | 113 | 118 | 127 | ... | 50 | 49 | 48 | 47 | 46 | 47 | 47 | 46 | 45 | 46 |
4 | 124 | 115 | 113 | 118 | 120 | 120 | 122 | 125 | 126 | 127 | ... | 49 | 49 | 47 | 48 | 48 | 47 | 47 | 48 | 48 | 47 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
595 | 128 | 125 | 128 | 124 | 112 | 106 | 109 | 118 | 124 | 123 | ... | 78 | 98 | 104 | 101 | 97 | 105 | 100 | 85 | 91 | 106 |
596 | 123 | 113 | 113 | 116 | 116 | 106 | 95 | 101 | 111 | 114 | ... | 70 | 79 | 85 | 93 | 109 | 111 | 100 | 86 | 83 | 97 |
597 | 116 | 99 | 94 | 108 | 125 | 119 | 103 | 103 | 121 | 124 | ... | 69 | 78 | 84 | 82 | 82 | 84 | 83 | 79 | 80 | 83 |
598 | 106 | 93 | 89 | 104 | 126 | 133 | 125 | 123 | 123 | 125 | ... | 62 | 69 | 82 | 81 | 57 | 55 | 68 | 72 | 81 | 86 |
599 | 105 | 102 | 100 | 105 | 119 | 133 | 137 | 132 | 116 | 113 | ... | 65 | 61 | 88 | 113 | 113 | 96 | 86 | 76 | 76 | 94 |
600 rows × 600 columns
What do you think the colorful DataFrame
should look like?
jax_color = iio.imread("jax.jpg")
pd.DataFrame(jax_color)
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) Cell In[18], line 2 1 jax_color = iio.imread("jax.jpg") ----> 2 pd.DataFrame(jax_color) File /opt/conda/lib/python3.11/site-packages/pandas/core/frame.py:827, in DataFrame.__init__(self, data, index, columns, dtype, copy) 816 mgr = dict_to_mgr( 817 # error: Item "ndarray" of "Union[ndarray, Series, Index]" has no 818 # attribute "name" (...) 824 copy=_copy, 825 ) 826 else: --> 827 mgr = ndarray_to_mgr( 828 data, 829 index, 830 columns, 831 dtype=dtype, 832 copy=copy, 833 typ=manager, 834 ) 836 # For data is list-like, or Iterable (will consume into list) 837 elif is_list_like(data): File /opt/conda/lib/python3.11/site-packages/pandas/core/internals/construction.py:314, in ndarray_to_mgr(values, index, columns, dtype, copy, typ) 308 _copy = ( 309 copy_on_sanitize 310 if (dtype is None or astype_is_view(values.dtype, dtype)) 311 else False 312 ) 313 values = np.array(values, copy=_copy) --> 314 values = _ensure_2d(values) 316 else: 317 # by definition an array here 318 # the dtypes will be coerced to a single dtype 319 values = _prep_ndarraylike(values, copy=copy_on_sanitize) File /opt/conda/lib/python3.11/site-packages/pandas/core/internals/construction.py:592, in _ensure_2d(values) 590 values = values.reshape((values.shape[0], 1)) 591 elif values.ndim != 2: --> 592 raise ValueError(f"Must pass 2-d input. shape={values.shape}") 593 return values ValueError: Must pass 2-d input. shape=(600, 600, 3)
jax_color.shape
(600, 600, 3)
jax.shape
(600, 600)
Array manipulation¶
Images are represented in Python with the type numpy.ndarray
or "n-dimensional array." Grayscale images are 2-dimensional arrays with pixel luminance values indicated in each position. Color images are 3-dimensional arrays with pixel color values indicated for each channel (red, green, blue) in each position. Can you set the left and right sides of this picture to 0
so that Jax appears surrounded by black borders?
jax = iio.imread("jax.jpg")
jax[:100, :] = 0
jax[-100:, :] = 0
plt.imshow(jax)
<matplotlib.image.AxesImage at 0x793fc1b86e90>
jax[:50, :25] = [0, 0, 1]
jax[:50, :25]
array([[[0, 0, 1], [0, 0, 1], [0, 0, 1], ..., [0, 0, 1], [0, 0, 1], [0, 0, 1]], [[0, 0, 1], [0, 0, 1], [0, 0, 1], ..., [0, 0, 1], [0, 0, 1], [0, 0, 1]], [[0, 0, 1], [0, 0, 1], [0, 0, 1], ..., [0, 0, 1], [0, 0, 1], [0, 0, 1]], ..., [[0, 0, 1], [0, 0, 1], [0, 0, 1], ..., [0, 0, 1], [0, 0, 1], [0, 0, 1]], [[0, 0, 1], [0, 0, 1], [0, 0, 1], ..., [0, 0, 1], [0, 0, 1], [0, 0, 1]], [[0, 0, 1], [0, 0, 1], [0, 0, 1], ..., [0, 0, 1], [0, 0, 1], [0, 0, 1]]], dtype=uint8)
jax.shape
(600, 600, 3)
nums = [1, 2, 3, 4]
nums[:2] = [0, 0]
nums
[0, 0, 3, 4]
When we're performing an assignment on 2-dimensions of a 3-dimensional image, NumPy follows broadcasting rules to evaluate the operation. The simplest version of broadcasting are just element-wise operations.
plt.imshow(jax + 70)
<matplotlib.image.AxesImage at 0x793fc1603010>
Let's try a more complicated example. Using the floor division operator, fill in the imshow
call to decrease only the green channel so that the overall picture is much more purple than before.
3 // 2
1
type(jax)
numpy.ndarray
jax[:, :, 1] // 2
array([[59, 62, 62, ..., 24, 23, 22], [61, 62, 61, ..., 23, 23, 24], [65, 66, 65, ..., 22, 22, 23], ..., [64, 55, 52, ..., 44, 44, 46], [59, 52, 50, ..., 40, 45, 48], [58, 57, 55, ..., 42, 43, 52]], dtype=uint8)
jax[:, :, 1] // 2
jax // 2
array([[[43, 59, 60], [46, 62, 63], [46, 62, 63], ..., [25, 24, 21], [25, 23, 21], [24, 22, 20]], [[46, 61, 62], [47, 62, 63], [46, 61, 62], ..., [25, 23, 21], [25, 23, 21], [25, 24, 21]], [[50, 65, 66], [51, 66, 67], [50, 65, 66], ..., [24, 22, 20], [24, 22, 20], [25, 23, 21]], ..., [[43, 64, 67], [35, 55, 58], [33, 52, 56], ..., [27, 44, 48], [28, 44, 49], [29, 46, 50]], [[37, 59, 62], [32, 52, 55], [30, 50, 53], ..., [23, 40, 45], [28, 45, 49], [29, 48, 52]], [[37, 58, 62], [35, 57, 60], [36, 55, 59], ..., [25, 42, 47], [24, 43, 47], [33, 52, 56]]], dtype=uint8)
jax // [1, 2, 1]
array([[[ 87, 59, 120], [ 93, 62, 126], [ 93, 62, 126], ..., [ 51, 24, 43], [ 50, 23, 42], [ 48, 22, 40]], [[ 92, 61, 125], [ 94, 62, 127], [ 92, 61, 125], ..., [ 50, 23, 42], [ 50, 23, 42], [ 51, 24, 43]], [[100, 65, 133], [102, 66, 135], [100, 65, 133], ..., [ 48, 22, 40], [ 48, 22, 40], [ 50, 23, 42]], ..., [[ 87, 64, 134], [ 70, 55, 117], [ 66, 52, 112], ..., [ 55, 44, 97], [ 56, 44, 98], [ 58, 46, 101]], [[ 75, 59, 125], [ 64, 52, 111], [ 61, 50, 107], ..., [ 47, 40, 90], [ 56, 45, 99], [ 59, 48, 104]], [[ 74, 58, 124], [ 71, 57, 121], [ 72, 55, 118], ..., [ 51, 42, 94], [ 49, 43, 94], [ 67, 52, 112]]])
jax = iio.imread("jax.jpg")
plt.imshow(jax // [1, 2, 1])
<matplotlib.image.AxesImage at 0x793fc1667790>
Practice: Instafade¶
Write code to apply a fading filter to the image. The fading filter reduces all color values to 77% intensity and then adds 38.25 to each resulting color value. (These numbers are somewhat arbitrarily chosen to get the desired effect.)
The provided code converts the oj
array from integer values to floating-point decimal values. To display the final image, the code converts the numbers in the oj
array back to uint8
before passing the result to imshow
.
oj = iio.imread("oj.jpg").astype("float32")
oj *= 0.77
oj += 38.25
plt.imshow(oj.astype("uint8"))
<matplotlib.image.AxesImage at 0x793fc14f9710>
Practice: Image Color Manipulation¶
Write code to apply the following operations to an image.
- Expand the red colors by 50% by subtracting 128 from each red channel value, multiply the result by 1.5, and then add 128 to restore the original value range.
- Increase the blue colors by 25 by adding 25 to each blue channel value.
- Add black letterboxing bars by setting the top 150 and bottom 150 pixels to black.
- Clip color values outside the range [0, 255] by reassign all values above 255 to 255 and all values below 0 to 0.
np.clip([1, 2, 3, 4, 5,6 , 6, 7, 8], a_min=0, a_max=4)
array([1, 2, 3, 4, 4, 4, 4, 4, 4])
oj = iio.imread("oj.jpg").astype("float32")
oj *= [0.5, 1, 1]
oj += [0, 0, 25]
oj[:100, :] = 0
oj[-100:, :] = 0
oj = np.clip(oj, a_min=0, a_max=255)
plt.imshow(oj.astype("uint8"))
<matplotlib.image.AxesImage at 0x793fc12511d0>
Optional: Advanced broadcasting¶
What is the result of adding the following two arrays together following the broadcasting rules?
x = np.array([[1], [2], [3]])
x
y = np.array([1, 2, 3])
y