Dec 04, 2025 | 791 words | 8 min read
12.1.1. Materials#
Representing Images as Matrices#
In mathematics, a rectangular arrangement of numbers is called a matrix (plural: matrices). Each position in a matrix is identified by its row and column number, just as each pixel in an image can be identified by its location. This means that a digital image can be represented as a grid of pixel values arranged in a 2D matrix :
Where \(p_{ij}\) means “the pixel in row \(i\), column \(j\)”. Here, the image has a size of \(m\) pixels by \(n\) pixels. Remember from Checkpoint 1 :
Grayscale images : pixel values range from 0 (black) to 255 (white).
Color images : each pixel has three channels (R, G, B), which actually forms a 3D matrix where each “layer” of the 3D matrix corresponds to one of the color channels. The shape of this matrix is \(\text{height}\times~\text{width}\times 3\).
In practice, when using Python, we represent these matrices using NumPy arrays, which allow easy manipulation of pixel values.
import numpy as np
# Creating a 3x3 matrix
image = np.array([
[10, 20, 30],
[40, 50, 60],
[70, 80, 90]
])
Multidimensional Indexing#
Because images are stored as two-dimensional (or three-dimensional) matrices, we refer to each pixel using multiple indices. This allows us to :
Access individual pixel values using their row, column, color channel (when in RGB).
Access sub-regions of pixels by specifying a range or rows and columns.
Apply mathematical operations to all or part of an image.
Accessing a single pixel value#
import numpy as np
image = np.array([
[10, 20, 30],
[40, 50, 60],
[70, 80, 90]
])
# Indexing a singular pixel value
pixel = image[2, 0]
Remember that Python uses 0-indexing, meaning that the \(3^{\text{rd}}\) row, \(1^{\text{st}}\) column is index \((2, 0)\).
pixel = 70
Accessing Rows and Columns#
import numpy as np
image = np.array([
[10, 20, 30],
[40, 50, 60],
[70, 80, 90]
])
# Indexing an entire row/column
first_row = image[0, :]
first_column = image[:, 0]
The colon : means “take all elements along that dimension”.
image[0, :]\(\rightarrow\) all columns in row \(0\)image[:, 0]\(\rightarrow\) all rows in column \(0\)
first_row = [10 20 30]
first_column = [10 40 70]
Multidimensional Indexing and Slicing#
import numpy as np
image = np.array([
[10, 20, 30],
[40, 50, 60],
[70, 80, 90]
])
# Indexing a region using ranges
region = image[0:2, 0:2]
Specifying 0:2 means “start at index \(0\) and stop before index \(2\)”. The first range refers to the rows, the second range refers to the columns.
region = [[10 20]
[40 50]]
NumPy#
Nested loops will slow down your code substantially. You SHOULD be using the built-in functionality that NumPy offers to reduce the presence of nested loops as much as possible.
Padding with NumPy#
ImageOps can be used to pad images and NumPy arrays that are converted to images. If you wanted to pad a NumPy array without converting to Image you can do so using the following :
import numpy as np
image = np.array([
[10, 20, 30],
[40, 50, 60],
[70, 80, 90]
])
padded_image = np.pad(image, 1)
Read NumPy Pad for information about the parameters to np.pad().
In this case we take in the NumPy array, image, and the pad width of 1.
padded_image = [[0 0 0 0 0]
[0 10 20 30 0]
[0 40 50 60 0]
[0 70 80 90 0]
[0 0 0 0 0]]
Elementwise Operations#
One of the most powerful features of NumPy arrays is their ability to perform operations on all elements at once. Instead of writing loops to modify individual elements, NumPy applies mathematical and logical operations across the entire array in a single step.
Arithmetic Operations
import numpy as np
image = np.array([
[10, 20, 30],
[40, 50, 60],
[70, 80, 90]
])
# Adding a constant to every element
plus_constant = image + 15
# Multiplying every pixel by a constant
times_constant = image * 2
# Combining arrays of the same shape
a = np.array([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
])
combined_arrays = image + a
When adding, subtracting, multiplying, or dividing by a constant that operation will be performed on every element of the array. When adding or subtracting arrays of the same shape the elements of one array will be combined with the corresponding element of the other array.
plus_constant = [[ 25 35 45]
[ 55 65 75]
[ 85 95 105]]
times_constant = [[ 20 40 60]
[ 80 100 120]
[140 160 180]]
combined_arrays = [[11 22 33]
[44 55 66]
[77 88 99]]
Logical Operations
import numpy as np
image = np.array([
[10, 20, 30],
[40, 50, 60],
[70, 80, 90]
])
# Check which elements are greater than 50
large_value = image > 50
This will apply the logical operator to all elements of the array.
large_value = [[False False False]
[False False True]
[ True True True]]
The result is a boolean array, because the elements of the array are exclusively True or False (this is also true of a purely 1 and 0 array). These special arrays can be used to filter or modify selected elements.
import numpy as np
image = np.array([
[10, 20, 30],
[40, 50, 60],
[70, 80, 90]
])
# Check which elements are greater than 50
large_value = image > 50
# Set all values > 50 to zero using our large_value array
image[large_value] = 0
Since our large_value array is the same size as our image and is a boolean array, we can use large_value to select which values should be assigned to 0.
image = [[10 20 30]
[40 50 0]
[ 0 0 0]]
Mathematical Functions
NumPy also offers a wide range of built-in mathematical functions that will automatically operate on every element of the array. Take special notice of np.clip(), it can be very useful in image processing, as the pixel values are bounded.
import numpy as np
image = np.array([
[10, 20, 30],
[40, 50, 60],
[70, 80, 90]
])
# Square root of each element
root_image = np.sqrt(image)
# Cubing each element
cubed_image = np.pow(image, 3)
# Clipping each element to a certain range
clipped_image = np.clip(image, 20, 70)
Read Math Functions for a list of the built-in math functions.
root_image = [[3.16227766 4.47213595 5.47722558]
[6.32455532 7.07106781 7.74596669]
[8.36660027 8.94427191 9.48683298]]
cubed_image = [[ 1000 8000 27000]
[ 64000 125000 216000]
[343000 512000 729000]]
clipped_image = [[20 20 30]
[40 50 60]
[70 70 70]]
Pandas#
Here is the link to the Official Documentation on Pandas
pandas is an open source library providing high-performance data structures and data analysis tools for python.
Installing pandas#
Enter the following command in the terminal:
python3 -m pip install pandas
Importing pandas#
The standard convention is to import pandas with the alias pd:
import pandas as pd
Opening a CSV File:#
You can open a CSV file using the
pandas.read_csv()function. This function reads the CSV file and returns a DataFrame, which is a 2-dimensional labeled data structure with columns of potentially different types.
import pandas as pd
df = pd.read_csv('file.csv')
DataFrame Operations:#
You can perform various operations on DataFrames, such as filtering, sorting, and aggregating data.
# Creating a DataFrame from scratch
df = pd.DataFrame({
'A': [1, 2, 3],
'B': [4, 5, 6],
'C': [7, 8, 9]
})
# Create a DataFrame from a Numpy array
df = pd.DataFrame(np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]), columns=['A', 'B', 'C'])
# Display the first few rows of the DataFrame (commonly called the "head" of the DataFrame)
print(df.head())
# Filter rows based on a condition
filtered_df = df[df['column_name'] > value]
filtered_df = df[df['column_name'] == value]
# Sort the DataFrame by a specific column
sorted_df = df.sort_values(by='column_name')
# Concatenate two DataFrames
concatenated_df = pd.concat([df1, df2])
# Note that {python}`concat()` does not modify the original DataFrames; it returns a new DataFrame. If the DataFrames have indexes that overlap, pandas will try to align them. You can reset the index of the resulting DataFrame using {python}`reset_index()`.
Applying Functions to DataFrames:#
You can apply functions to DataFrame columns using the
DataFrame.apply()method.
# Apply a function to each value in a column
df['new_column'] = df['existing_column'].apply(lambda val: val * 2)
# Apply a function to each row
df['new_column'] = df.apply(lambda row: (row['column1'] + row['column2']) / row['column3'], axis=1)
# Lambda functions are small anonymous functions defined with the {python}`lambda` keyword. They can take any number of arguments but can only have one expression. Read more about them [here](https://realpython.com/python-lambda/) if you are curious. They are not required to complete this task.
Alternatively, you can use vectorized operations to perform calculations on entire columns without the need for
apply(), which is often more efficient.
# Vectorized operation to create a new column
df['new_column'] = (df['column1'] + df['column2']) / df['column3']
If your function is a lot more complex or calculates multiple values, you can define it separately and then pass it to
apply().
data = {'col1': [10, 20, 30],
'col2': [2, 4, 5],
'col3': [1, 1, 3]}
df = pd.DataFrame(data)
print("Original DataFrame:")
print(df)
def calculate_metrics(row):
"""A function to calculate multiple values from a row."""
# The function returns a Series with an index for the new column names
return pd.Series({
'average': (row.col1 + row.col2 + row.col3) / 3,
'is_large': (row.col1 * row.col2) > 50
})
# Apply the custom function and assign the results
df[['average', 'is_large']] = df.apply(calculate_metrics, axis=1)
print("\nDataFrame with multiple new columns:")
print(df)
# Output:
# Original DataFrame:
# col1 col2 col3
# 0 10 2 1
# 1 20 4 1
# 2 30 5 3
#
# DataFrame with multiple new columns:
# col1 col2 col3 average is_large
# 0 10 2 1 4.333333 False
# 1 20 4 1 8.333333 True
# 2 30 5 3 12.666667 True
DataFrame Indexes:#
DataFrames have an index that labels each row. You can set a specific column as the index or reset the index to the default integer index.
# Set a specific column as the index
df.set_index('column_name', inplace=True)
# Reset the index to the default integer index
df.reset_index(inplace=True)
# Drop the current index entirely
df.reset_index(drop=True, inplace=True)
# the 'inplace=True' argument modifies the DataFrame in the same variable without needing to reassign it to a new one.
Series Operations:#
A Series is a one-dimensional array-like object that can hold any data type. You can perform various operations on Series, such as mathematical operations and applying functions.
# Create a Series
s = pd.Series([1, 2, 3, 4, 5])
# Perform mathematical operations
s_squared = s ** 2
# Apply a function to each element in the Series
s_log = s.apply(np.mean)
# Extract a series from a DataFrame
column_series = df1['column_name']
# Add the series as a new column to a DataFrame
df2['new_column'] = column_series
Saving a DataFrame to a CSV File:#
You can save a DataFrame to a CSV file using the
DataFrame.to_csv()method.
df.to_csv('output_file.csv', index=False)
# The 'index=False' argument prevents pandas from writing row indices to the CSV file.