{ "cells": [ { "cell_type": "markdown", "id": "e8b29cc0", "metadata": {}, "source": [ "# Lesson 1: Images as Arrays\n", "\n", "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." ] }, { "cell_type": "code", "execution_count": null, "id": "400181a9", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import cv2\n", "import matplotlib.pyplot as plt" ] }, { "cell_type": "markdown", "id": "78c6caa4", "metadata": {}, "source": [ "## A grayscale image is a 2D array\n", "\n", "Each entry is a pixel intensity. For an 8-bit image, pixel values range from 0 (black) to 255 (white)." ] }, { "cell_type": "code", "execution_count": null, "id": "e4fd22d4", "metadata": {}, "outputs": [], "source": [ "im = np.zeros((100, 100), dtype=np.uint8)\n", "im[20:80, 20:80] = 255 # a white square on a black background\n", "\n", "print('shape:', im.shape, 'dtype:', im.dtype)\n", "\n", "plt.imshow(im, cmap='gray', vmin=0, vmax=255)\n", "plt.title('Grayscale image (100x100)')\n", "plt.axis('off')\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "35df26b0", "metadata": {}, "source": [ "## Simple pixel-level operations\n", "\n", "Because an image is just an array of numbers, standard NumPy operations apply directly, e.g., inverting intensities." ] }, { "cell_type": "code", "execution_count": null, "id": "88675206", "metadata": {}, "outputs": [], "source": [ "im_inv = 255 - im\n", "\n", "fig, axes = plt.subplots(1, 2, figsize=(6, 3))\n", "axes[0].imshow(im, cmap='gray', vmin=0, vmax=255)\n", "axes[0].set_title('Original')\n", "axes[0].axis('off')\n", "axes[1].imshow(im_inv, cmap='gray', vmin=0, vmax=255)\n", "axes[1].set_title('Inverted (255 - pixel)')\n", "axes[1].axis('off')\n", "plt.tight_layout()\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "3cd8d7d7", "metadata": {}, "source": [ "## A color image is a 3D array\n", "\n", "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." ] }, { "cell_type": "code", "execution_count": null, "id": "d9497443", "metadata": {}, "outputs": [], "source": [ "imc = np.zeros((50, 100, 3), dtype=np.uint8)\n", "imc[:, :, 0] = 255 # red channel on for the whole image\n", "imc[10:40, 20:80, 1] = 255 # add green in the middle -> red + green looks yellow\n", "\n", "print('shape:', imc.shape, 'dtype:', imc.dtype)\n", "\n", "plt.imshow(imc)\n", "plt.title('Color image (100x100x3)')\n", "plt.axis('off')\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "b7ed4d15", "metadata": {}, "source": [ "## Colors are typically in RGB order\n", "\n", "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." ] }, { "cell_type": "code", "execution_count": null, "id": "f2c7ea5c", "metadata": {}, "outputs": [], "source": [ "im_primaries = np.zeros((2, 3, 3), dtype=np.uint8)\n", "im_primaries[0, 0, :] = (0, 0, 0) # black\n", "im_primaries[0, 1, :] = (255, 255, 255) # white\n", "im_primaries[0, 2, :] = (255, 0, 0) # red\n", "im_primaries[1, 0, :] = (255, 255, 0) # yellow\n", "im_primaries[1, 1, :] = (0, 255, 0) # green\n", "im_primaries[1, 2, :] = (0, 0, 255) # blue\n", "print('shape:', im_primaries.shape, ' (2 rows, 3 columns, 3 color channels)')\n", "\n", "plt.imshow(im_primaries)\n", "plt.title('The six psychological primaries')\n", "plt.axis('off')\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "06bfb2ff", "metadata": {}, "source": [ "Slicing the **first** axis selects whole rows (a horizontal band of the image); slicing the **second** axis selects whole columns (a vertical band)." ] }, { "cell_type": "code", "execution_count": null, "id": "0076f109", "metadata": {}, "outputs": [], "source": [ "top_row = im_primaries[0:1, :, :] # first axis sliced -> a full-width horizontal band\n", "left_column = im_primaries[:, 0:1, :] # second axis sliced -> a full-height vertical band\n", "# NOTE: a bare integer index (im_primaries[0, :, :]) would DROP that axis entirely, turning\n", "# shape (1, 3, 3) into (3, 3) -- imshow would then misread it as a 3x3 grayscale image instead\n", "# of a 1-row RGB strip. Slicing with 0:1 keeps the axis alive as size 1.\n", "\n", "fig, axes = plt.subplots(1, 3, figsize=(9, 3))\n", "axes[0].imshow(im_primaries)\n", "axes[0].set_title('Full image', fontsize=9)\n", "axes[1].imshow(top_row)\n", "axes[1].set_title(f'Top row [0:1, :, :]', fontsize=9)\n", "axes[2].imshow(left_column)\n", "axes[2].set_title(f'Left column [:, 0:1, :]', fontsize=9)\n", "for ax in axes:\n", " ax.axis('off')\n", "plt.tight_layout()\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "4465e990", "metadata": {}, "source": [ "## Loading a real photo: BGR vs. RGB\n", "\n", "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." ] }, { "cell_type": "code", "execution_count": null, "id": "67cf7c95", "metadata": {}, "outputs": [], "source": [ "im_bgr = cv2.imread('../img/rose.jpg')\n", "print('shape:', im_bgr.shape, ' (last axis is in BGR order)')\n", "\n", "plt.imshow(im_bgr)\n", "plt.title('im_bgr shown directly -- wrong colors!')\n", "plt.axis('off')\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "dfc452a7", "metadata": {}, "source": [ "### Two ways to fix this problem\n", "\n", "1. **Reverse the channel axis with NumPy indexing.** `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.\n", "2. **`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).\n", "\n", "Both should produce identical results here, since a BGR→RGB conversion is nothing more than reversing three channels." ] }, { "cell_type": "code", "execution_count": null, "id": "b83f0775", "metadata": {}, "outputs": [], "source": [ "im_rgb1 = im_bgr[:, :, ::-1]\n", "im_rgb2 = cv2.cvtColor(im_bgr, cv2.COLOR_BGR2RGB)\n", "\n", "print('the two methods agree exactly:', np.array_equal(im_rgb1, im_rgb2))\n", "\n", "fig, axes = plt.subplots(1, 3, figsize=(10, 4))\n", "for ax, im, title in zip(axes, [im_bgr, im_rgb1, im_rgb2],\n", " ['im_bgr (wrong)', 'img[:, :, ::-1]', \"cv2.cvtColor(..., BGR2RGB)\"]):\n", " ax.imshow(im)\n", " ax.set_title(title, fontsize=9)\n", " ax.axis('off')\n", "plt.tight_layout()\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "3ed0194b", "metadata": {}, "source": "

Image source: Picryl

" }, { "cell_type": "markdown", "id": "9eaf929e", "metadata": {}, "source": [ "### Exercise\n", "\n", "1. Modify the color image `imc` so the rectangle is cyan instead of yellow. Which channel(s) do you need to change?\n", "2. In `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)`.\n", "3. Load `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." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3.x" } }, "nbformat": 4, "nbformat_minor": 5 }