A digital image is a 2D grid of numbers. In this lesson we build a small image from scratch with NumPy, look at how grayscale and color images are represented, and display them with Matplotlib.
import numpy as np
import cv2
import matplotlib.pyplot as plt
Each entry is a pixel intensity. For an 8-bit image, pixel values range from 0 (black) to 255 (white).
im = np.zeros((100, 100), dtype=np.uint8)
im[20:80, 20:80] = 255 # a white square on a black background
print('shape:', im.shape, 'dtype:', im.dtype)
plt.imshow(im, cmap='gray', vmin=0, vmax=255)
plt.title('Grayscale image (100x100)')
plt.axis('off')
plt.show()
Because an image is just an array of numbers, standard NumPy operations apply directly, e.g., inverting intensities.
im_inv = 255 - im
fig, axes = plt.subplots(1, 2, figsize=(6, 3))
axes[0].imshow(im, cmap='gray', vmin=0, vmax=255)
axes[0].set_title('Original')
axes[0].axis('off')
axes[1].imshow(im_inv, cmap='gray', vmin=0, vmax=255)
axes[1].set_title('Inverted (255 - pixel)')
axes[1].axis('off')
plt.tight_layout()
plt.show()
An image's dimensions are (height, width, channels) — the output of array.shape. The first axis selects the row (moving down the image, i.e., $y$), the second axis selects the column (moving across, i.e., $x$); the third axis selects the color channel (red, green, or blue). By convention, the origin is the top-left corner.
imc = np.zeros((50, 100, 3), dtype=np.uint8)
imc[:, :, 0] = 255 # red channel on for the whole image
imc[10:40, 20:80, 1] = 255 # add green in the middle -> red + green looks yellow
print('shape:', imc.shape, 'dtype:', imc.dtype)
plt.imshow(imc)
plt.title('Color image (100x100x3)')
plt.axis('off')
plt.show()
Usually the order of the 3 color channels is red, green, and blue (RGB). To illustrate this, let's build a small image out of the six psychological primaries — arranged in a 2-row, 3-column grid. These are the six hues (black, white, red, yellow, green, blue) that the human visual system treats as elementary, unmixed colors.
im_primaries = np.zeros((2, 3, 3), dtype=np.uint8)
im_primaries[0, 0, :] = (0, 0, 0) # black
im_primaries[0, 1, :] = (255, 255, 255) # white
im_primaries[0, 2, :] = (255, 0, 0) # red
im_primaries[1, 0, :] = (255, 255, 0) # yellow
im_primaries[1, 1, :] = (0, 255, 0) # green
im_primaries[1, 2, :] = (0, 0, 255) # blue
print('shape:', im_primaries.shape, ' (2 rows, 3 columns, 3 color channels)')
plt.imshow(im_primaries)
plt.title('The six psychological primaries')
plt.axis('off')
plt.show()
Slicing the first axis selects whole rows (a horizontal band of the image); slicing the second axis selects whole columns (a vertical band).
top_row = im_primaries[0:1, :, :] # first axis sliced -> a full-width horizontal band
left_column = im_primaries[:, 0:1, :] # second axis sliced -> a full-height vertical band
# NOTE: a bare integer index (im_primaries[0, :, :]) would DROP that axis entirely, turning
# shape (1, 3, 3) into (3, 3) -- imshow would then misread it as a 3x3 grayscale image instead
# of a 1-row RGB strip. Slicing with 0:1 keeps the axis alive as size 1.
fig, axes = plt.subplots(1, 3, figsize=(9, 3))
axes[0].imshow(im_primaries)
axes[0].set_title('Full image', fontsize=9)
axes[1].imshow(top_row)
axes[1].set_title(f'Top row [0:1, :, :]', fontsize=9)
axes[2].imshow(left_column)
axes[2].set_title(f'Left column [:, 0:1, :]', fontsize=9)
for ax in axes:
ax.axis('off')
plt.tight_layout()
plt.show()
Every color image so far was manually constructed with the color channel in RGB order — matching what Matplotlib's plt.imshow expects. Real photos, however, are loaded with OpenCV's cv2.imread, which reads (and writes) color images with channels in BGR order — blue first, red last — the opposite of the RGB order every other Python imaging/plotting library assumes. Handing a BGR array straight to plt.imshow misinterprets these color channels, resulting in a weird display.
im_bgr = cv2.imread('../img/rose.jpg')
print('shape:', im_bgr.shape, ' (last axis is in BGR order)')
plt.imshow(im_bgr)
plt.title('im_bgr shown directly -- wrong colors!')
plt.axis('off')
plt.show()
img[:, :, ::-1] reverses the order of the last axis, turning [B, G, R] into [R, G, B] — a plain array operation, no OpenCV function needed.cv2.cvtColor. OpenCV's general-purpose color-conversion function; cv2.COLOR_BGR2RGB does exactly the same channel swap, but is more explicit about why, and is the idiomatic choice in OpenCV code (the same function also handles conversions that aren't a simple reversal, e.g. to grayscale or HSV, in later lessons).Both should produce identical results here, since a BGR→RGB conversion is nothing more than reversing three channels.
im_rgb1 = im_bgr[:, :, ::-1]
im_rgb2 = cv2.cvtColor(im_bgr, cv2.COLOR_BGR2RGB)
print('the two methods agree exactly:', np.array_equal(im_rgb1, im_rgb2))
fig, axes = plt.subplots(1, 3, figsize=(10, 4))
for ax, im, title in zip(axes, [im_bgr, im_rgb1, im_rgb2],
['im_bgr (wrong)', 'img[:, :, ::-1]', "cv2.cvtColor(..., BGR2RGB)"]):
ax.imshow(im)
ax.set_title(title, fontsize=9)
ax.axis('off')
plt.tight_layout()
plt.show()
Image source: Picryl
imc so the rectangle is cyan instead of yellow. Which channel(s) do you need to change?im_primaries, slice out the bottom-right block (blue) using row and column ranges, and confirm with np.array_equal that it matches a fresh block filled with (0, 0, 255).rose.jpg and print im_bgr[0, 0, :] (top-left pixel, BGR order) alongside im_rgb2[0, 0, :] (RGB order). Confirm by hand that the three numbers are the same three numbers, just reordered.