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.

In [1]:
import imageio.v3 as iio
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

Reading an image¶

Let's use imread to load a color picture of Dubs II in grayscale with mode="L" standing for "luminance" or "lightness". To show an image, we can plot its pixels using the matplotlib function imshow.

In [22]:
dubs = iio.imread("dubs.jpg", mode="L")
plt.imshow(dubs, cmap="gray")
Out[22]:
<matplotlib.image.AxesImage at 0x7ec7cdd00a00>
No description has been provided for this image
In [13]:
dubs.ndim
Out[13]:
2
In [23]:
dubs.shape
Out[23]:
(360, 640)
In [3]:
plt.imshow(dubs)
Out[3]:
<matplotlib.image.AxesImage at 0x7ec7dc8bcd60>
No description has been provided for this image
In [4]:
dubs = iio.imread("dubs.jpg")
plt.imshow(dubs)
Out[4]:
<matplotlib.image.AxesImage at 0x7ec7cef17700>
No description has been provided for this image
In [5]:
dubs
Out[5]:
array([[[235, 238, 245],
        [235, 238, 245],
        [236, 239, 246],
        ...,
        [229, 231, 243],
        [229, 231, 243],
        [229, 231, 243]],

       [[235, 238, 245],
        [236, 239, 246],
        [236, 239, 246],
        ...,
        [229, 231, 243],
        [229, 231, 243],
        [229, 231, 243]],

       [[236, 239, 246],
        [236, 239, 246],
        [236, 239, 246],
        ...,
        [229, 231, 243],
        [229, 231, 243],
        [229, 231, 243]],

       ...,

       [[237, 240, 247],
        [237, 240, 247],
        [237, 240, 247],
        ...,
        [233, 235, 247],
        [233, 235, 247],
        [233, 235, 247]],

       [[237, 240, 247],
        [237, 240, 247],
        [237, 240, 247],
        ...,
        [233, 235, 247],
        [233, 235, 247],
        [233, 235, 247]],

       [[237, 240, 247],
        [237, 240, 247],
        [237, 240, 247],
        ...,
        [233, 235, 247],
        [233, 235, 247],
        [233, 235, 247]]], 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 color values ranging from [0, 255].

In [14]:
pd.DataFrame(dubs)
Out[14]:
0 1 2 3 4 5 6 7 8 9 ... 630 631 632 633 634 635 636 637 638 639
0 238 238 239 239 240 240 241 241 240 240 ... 231 231 232 232 232 232 232 232 232 232
1 238 239 239 239 240 240 241 241 240 240 ... 231 231 232 232 232 232 232 232 232 232
2 239 239 239 240 240 240 240 241 240 240 ... 231 231 232 232 232 232 232 232 232 232
3 239 239 240 240 240 240 240 241 240 240 ... 231 231 232 232 232 232 232 232 232 232
4 240 240 240 240 240 240 240 240 240 240 ... 231 231 232 232 232 232 232 232 232 232
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
355 240 240 240 240 240 240 240 240 240 240 ... 237 237 237 237 237 237 237 237 237 237
356 240 240 240 240 240 240 240 240 240 240 ... 237 237 236 236 236 236 236 236 236 236
357 240 240 240 240 240 240 240 240 240 240 ... 237 237 236 236 236 236 236 236 236 236
358 240 240 240 240 240 240 240 240 240 240 ... 237 237 236 236 236 236 236 236 236 236
359 240 240 240 240 240 240 240 240 240 240 ... 237 237 236 236 236 236 236 236 236 236

360 rows × 640 columns

What would a color image of Dubs II look like instead? Let's try loading the picture without mode="L" to maintain its color data.

In [25]:
dubs = iio.imread("dubs.jpg")
plt.imshow(dubs)
Out[25]:
<matplotlib.image.AxesImage at 0x7ec7cde7b3a0>
No description has been provided for this image

What do you think the colorful DataFrame should look like?

In [11]:
dubs.ndim
Out[11]:
3
In [10]:
pd.DataFrame(dubs)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[10], line 1
----> 1 pd.DataFrame(dubs)

File /opt/conda/lib/python3.10/site-packages/pandas/core/frame.py:782, in DataFrame.__init__(self, data, index, columns, dtype, copy)
    771         mgr = dict_to_mgr(
    772             # error: Item "ndarray" of "Union[ndarray, Series, Index]" has no
    773             # attribute "name"
   (...)
    779             copy=_copy,
    780         )
    781     else:
--> 782         mgr = ndarray_to_mgr(
    783             data,
    784             index,
    785             columns,
    786             dtype=dtype,
    787             copy=copy,
    788             typ=manager,
    789         )
    791 # For data is list-like, or Iterable (will consume into list)
    792 elif is_list_like(data):

File /opt/conda/lib/python3.10/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.10/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=(360, 640, 3)
In [19]:
# red color channel of the rgb image
dubs[:, :, 0]
Out[19]:
array([[235, 235, 236, ..., 229, 229, 229],
       [235, 236, 236, ..., 229, 229, 229],
       [236, 236, 236, ..., 229, 229, 229],
       ...,
       [237, 237, 237, ..., 233, 233, 233],
       [237, 237, 237, ..., 233, 233, 233],
       [237, 237, 237, ..., 233, 233, 233]], dtype=uint8)
In [20]:
pd.DataFrame(dubs[:, :, 0])
Out[20]:
0 1 2 3 4 5 6 7 8 9 ... 630 631 632 633 634 635 636 637 638 639
0 235 235 236 236 237 237 238 238 237 237 ... 228 228 229 229 229 229 229 229 229 229
1 235 236 236 236 237 237 238 238 237 237 ... 228 228 229 229 229 229 229 229 229 229
2 236 236 236 237 237 237 237 238 237 237 ... 228 228 229 229 229 229 229 229 229 229
3 236 236 237 237 237 237 237 238 237 237 ... 228 228 229 229 229 229 229 229 229 229
4 237 237 237 237 237 237 237 237 237 237 ... 228 228 229 229 229 229 229 229 229 229
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
355 237 237 237 237 237 237 237 237 237 237 ... 234 234 234 234 234 234 234 234 234 234
356 237 237 237 237 237 237 237 237 237 237 ... 234 234 233 233 233 233 233 233 233 233
357 237 237 237 237 237 237 237 237 237 237 ... 234 234 233 233 233 233 233 233 233 233
358 237 237 237 237 237 237 237 237 237 237 ... 234 234 233 233 233 233 233 233 233 233
359 237 237 237 237 237 237 237 237 237 237 ... 234 234 233 233 233 233 233 233 233 233

360 rows × 640 columns

In [21]:
dubs.shape
Out[21]:
(360, 640, 3)
In [27]:
pd.concat([
    pd.DataFrame(dubs[:, :, 0]),
    pd.DataFrame(dubs[:, :, 1]),
    pd.DataFrame(dubs[:, :, 2])
], keys=['R', 'G', 'B'])
Out[27]:
0 1 2 3 4 5 6 7 8 9 ... 630 631 632 633 634 635 636 637 638 639
R 0 235 235 236 236 237 237 238 238 237 237 ... 228 228 229 229 229 229 229 229 229 229
1 235 236 236 236 237 237 238 238 237 237 ... 228 228 229 229 229 229 229 229 229 229
2 236 236 236 237 237 237 237 238 237 237 ... 228 228 229 229 229 229 229 229 229 229
3 236 236 237 237 237 237 237 238 237 237 ... 228 228 229 229 229 229 229 229 229 229
4 237 237 237 237 237 237 237 237 237 237 ... 228 228 229 229 229 229 229 229 229 229
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
B 355 247 247 247 247 247 247 247 247 247 247 ... 248 248 248 248 248 248 248 248 248 248
356 247 247 247 247 247 247 247 247 247 247 ... 248 248 247 247 247 247 247 247 247 247
357 247 247 247 247 247 247 247 247 247 247 ... 248 248 247 247 247 247 247 247 247 247
358 247 247 247 247 247 247 247 247 247 247 ... 248 248 247 247 247 247 247 247 247 247
359 247 247 247 247 247 247 247 247 247 247 ... 248 248 247 247 247 247 247 247 247 247

1080 rows × 640 columns

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 Dubs II appears surrounded by black borders?

In [28]:
dubs = iio.imread("dubs.jpg")
dubs[:50, :25] = 0
plt.imshow(dubs)
Out[28]:
<matplotlib.image.AxesImage at 0x7ec7cdd4e650>
No description has been provided for this image

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.

In [34]:
# task: set the borders of left and right of the image to be black
dubs[:, :100] = 0
dubs[:, -100:] = 0
plt.imshow(dubs)
Out[34]:
<matplotlib.image.AxesImage at 0x7ec7cdd79de0>
No description has been provided for this image
In [31]:
dubs[:, :100].shape
Out[31]:
(360, 100, 3)
In [36]:
# what if we just set the red channel?
dubs = iio.imread("dubs.jpg")
dubs[:, :100, 0] = 0
dubs[:, -100:, 0] = 0
plt.imshow(dubs)
Out[36]:
<matplotlib.image.AxesImage at 0x7ec7cdc20df0>
No description has been provided for this image
In [ ]:
dubs[:, slice(0,100)] = 0
In [37]:
dubs = iio.imread("dubs.jpg")
dubs[:, :100] = dubs[:, -100:] = 0
plt.imshow(dubs)
Out[37]:
<matplotlib.image.AxesImage at 0x7ec7cddcd0f0>
No description has been provided for this image

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.

In [38]:
5 // 2
Out[38]:
2
In [43]:
dubs = iio.imread("dubs.jpg")
plt.imshow(dubs // 2)
Out[43]:
<matplotlib.image.AxesImage at 0x7ec7cda05ed0>
No description has been provided for this image
In [40]:
plt.imshow(dubs[1] // 2)
Out[40]:
<matplotlib.image.AxesImage at 0x7ec7cddb3220>
No description has been provided for this image
In [42]:
dubs[:, :, 1] = dubs[:, :, 1] // 2 # decrease the green channel pixel value
plt.imshow(dubs)
Out[42]:
<matplotlib.image.AxesImage at 0x7ec7cdb8a050>
No description has been provided for this image
In [44]:
plt.imshow(dubs // [1, 2, 1])
Out[44]:
<matplotlib.image.AxesImage at 0x7ec7cda7a6b0>
No description has been provided for this image
In [45]:
plt.imshow(dubs // [:, 2, :])
  Cell In[45], line 1
    plt.imshow(dubs // [:, 2, :])
                        ^
SyntaxError: invalid syntax

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 dog array from integer values to floating-point decimal values. To display the final image, the code converts the numbers in the dog array back to uint8 before passing the result to imshow.

In [47]:
dog = iio.imread("dog.jpg").astype("float32")
dog = dog * 0.77 + 38.25
plt.imshow(dog.astype("uint8"))
Out[47]:
<matplotlib.image.AxesImage at 0x7ec7cd931930>
No description has been provided for this image

Practice: Gotham¶

Write code to apply the following operations to an image.

  1. 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.
  2. Increase the blue colors by 13 by adding 13 to each blue channel value.
  3. Add black letterboxing bars by setting the top 150 and bottom 150 pixels to black.
  4. Clip color values outside the range [0, 255] by reassign all values above 255 to 255 and all values below 0 to 0.
In [54]:
dog = iio.imread("dog.jpg").astype("float32")
dog[:, :, 0] = (dog[:, :, 0] - 128) * 1.5 + 128
dog[:, :, 2] += 13
dog[:150, :] = 0
dog[-150:, :] = 0
dog[dog > 255] = 255
dog[dog < 0] = 0
# np.clip()
plt.imshow(dog.astype("uint8"))
Out[54]:
<matplotlib.image.AxesImage at 0x7ec7cd771000>
No description has been provided for this image
In [52]:
(dog > 255).shape # numpy equivalent of pandas boolean series
Out[52]:
(1000, 1600, 3)
In [53]:
dog[dog > 255]
Out[53]:
array([260. , 257. , 267.5, ..., 266. , 261.5, 257. ], dtype=float32)
In [55]:
if dog>255: # if boolean_expression
    pass
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[55], line 1
----> 1 if dog>255: # if boolean_expression
      2     pass

ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()

Optional: Advanced broadcasting¶

What is the result of adding the following two arrays together following the broadcasting rules?

In [56]:
x = np.array([[1], [2], [3]])
x
Out[56]:
array([[1],
       [2],
       [3]])
In [57]:
y = np.array([1, 2, 3])
y
Out[57]:
array([1, 2, 3])
In [58]:
x.shape, y.shape
Out[58]:
((3, 1), (3,))
In [59]:
x + y
Out[59]:
array([[2, 3, 4],
       [3, 4, 5],
       [4, 5, 6]])
In [ ]:
(3 x 1)
    (3)