Understanding Digital Images
Before we can process images, we need to understand how they're represented in computers. A digital image is a 2D or 3D array of numerical values representing pixel intensities.
Image Coordinate System
Images use a coordinate system where:
- Origin (0,0) is typically at the top-left corner
- X-axis extends horizontally (width)
- Y-axis extends vertically (height)
- Pixel at position (x, y) contains intensity value(s)
# Image array representation
import numpy as np
import cv2
# Create a simple 5x5 grayscale image
image = np.array([
[0, 50, 100, 150, 200],
[50, 100, 150, 200, 255],
[100, 150, 200, 250, 255],
[150, 200, 250, 255, 255],
[200, 250, 255, 255, 255]
], dtype=np.uint8)
print(f"Image shape: {image.shape}") # (5, 5)
print(f"Pixel at (2,2): {image[2,2]}") # 200
Color Spaces
Different color representations serve different purposes:
| Color Space | Channels | Use Cases |
|---|---|---|
| RGB | Red, Green, Blue | Display, web, general purpose |
| BGR | Blue, Green, Red | OpenCV default format |
| HSV | Hue, Saturation, Value | Color-based segmentation, tracking |
| LAB | Lightness, A, B | Perceptual uniformity, color correction |
| Grayscale | Single intensity | Edge detection, feature extraction |
| YCbCr | Luma, Chroma blue, Chroma red | Video compression, JPEG |
# Converting between color spaces
import cv2
# Read image (BGR by default in OpenCV)
image_bgr = cv2.imread('image.jpg')
# Convert to different color spaces
image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
image_hsv = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2HSV)
image_lab = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2LAB)
image_gray = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
# HSV is great for color-based masking
lower_red = np.array([0, 120, 70])
upper_red = np.array([10, 255, 255])
mask = cv2.inRange(image_hsv, lower_red, upper_red)
Basic Image Operations
Image Reading and Writing
# Using OpenCV
import cv2
# Read image
img = cv2.imread('input.jpg') # Returns BGR
img_unchanged = cv2.imread('input.png', cv2.IMREAD_UNCHANGED) # Includes alpha
img_gray = cv2.imread('input.jpg', cv2.IMREAD_GRAYSCALE)
# Write image
cv2.imwrite('output.jpg', img)
cv2.imwrite('output.png', img, [cv2.IMWRITE_PNG_COMPRESSION, 9])
# Using PIL/Pillow
from PIL import Image
img = Image.open('input.jpg') # Returns RGB
img.save('output.jpg', quality=95)
Image Resizing
Resizing is crucial for normalizing input to neural networks and managing computational resources.
# Various resizing methods
import cv2
image = cv2.imread('image.jpg')
height, width = image.shape[:2]
# Method 1: Specify exact dimensions
resized = cv2.resize(image, (640, 480)) # (width, height)
# Method 2: Scale by factor
scaled = cv2.resize(image, None, fx=0.5, fy=0.5)
# Method 3: Interpolation methods
nearest = cv2.resize(image, (640, 480), interpolation=cv2.INTER_NEAREST)
linear = cv2.resize(image, (640, 480), interpolation=cv2.INTER_LINEAR)
cubic = cv2.resize(image, (640, 480), interpolation=cv2.INTER_CUBIC)
lanczos = cv2.resize(image, (640, 480), interpolation=cv2.INTER_LANCZOS4)
# Maintain aspect ratio
def resize_with_aspect_ratio(image, width=None, height=None):
h, w = image.shape[:2]
if width is None and height is None:
return image
if width is None:
ratio = height / h
new_width = int(w * ratio)
return cv2.resize(image, (new_width, height))
else:
ratio = width / w
new_height = int(h * ratio)
return cv2.resize(image, (width, new_height))
Image Cropping and Padding
# Cropping
x, y, w, h = 100, 50, 300, 200
cropped = image[y:y+h, x:x+w]
# Padding
import cv2
# Add border around image
padded = cv2.copyMakeBorder(
image,
top=10, bottom=10, left=10, right=10,
borderType=cv2.BORDER_CONSTANT,
value=[0, 0, 0] # Black border
)
# Border types
# BORDER_CONSTANT: Solid color border
# BORDER_REPLICATE: Edge pixels replicated
# BORDER_REFLECT: Border reflects image
# BORDER_WRAP: Image wraps around
Image Filtering
Convolution Operations
Convolution is the fundamental operation in image processing. A kernel (small matrix) slides across the image, computing weighted sums.
Convolution Formula
Output(x, y) = Σ Σ Image(x+i, y+j) × Kernel(i, j)
The kernel is centered at each pixel position, and the weighted sum becomes the new pixel value.
# Custom convolution
import cv2
import numpy as np
# Define a custom kernel (3x3 sharpening)
kernel = np.array([
[ 0, -1, 0],
[-1, 5, -1],
[ 0, -1, 0]
])
# Apply convolution
sharpened = cv2.filter2D(image, -1, kernel)
# Common kernels
identity = np.array([[0, 0, 0], [0, 1, 0], [0, 0, 0]])
edge_detect = np.array([[-1, -1, -1], [-1, 8, -1], [-1, -1, -1]])
emboss = np.array([[-2, -1, 0], [-1, 1, 1], [0, 1, 2]])
Blurring and Smoothing
Blur filters reduce noise and detail by averaging neighboring pixels.
# Averaging (Box) Blur
blur_avg = cv2.blur(image, (5, 5))
# Gaussian Blur - weights based on Gaussian distribution
blur_gaussian = cv2.GaussianBlur(image, (5, 5), sigmaX=0)
# Median Blur - good for salt-and-pepper noise
blur_median = cv2.medianBlur(image, 5)
# Bilateral Filter - preserves edges while smoothing
blur_bilateral = cv2.bilateralFilter(image, d=9, sigmaColor=75, sigmaSpace=75)
# Comparison
import matplotlib.pyplot as plt
fig, axes = plt.subplots(2, 2, figsize=(12, 12))
axes[0,0].imshow(blur_avg); axes[0,0].set_title('Average Blur')
axes[0,1].imshow(blur_gaussian); axes[0,1].set_title('Gaussian Blur')
axes[1,0].imshow(blur_median); axes[1,0].set_title('Median Blur')
axes[1,1].imshow(blur_bilateral); axes[1,1].set_title('Bilateral Filter')
plt.show()
Sharpening
Sharpening enhances edges and fine details.
# Method 1: Unsharp masking
gaussian = cv2.GaussianBlur(image, (9, 9), 10.0)
sharpened = cv2.addWeighted(image, 1.5, gaussian, -0.5, 0)
# Method 2: Laplacian sharpening
laplacian = cv2.Laplacian(image, cv2.CV_64F)
sharpened = cv2.convertScaleAbs(image - laplacian)
Edge Detection
Edge detection identifies boundaries where pixel intensity changes rapidly.
Sobel Operator
Detects edges using gradient calculations in X and Y directions.
# Sobel edge detection
import cv2
import numpy as np
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# Calculate gradients
sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3) # X direction
sobely = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3) # Y direction
# Combine gradients
sobel_combined = np.sqrt(sobelx**2 + sobely**2)
sobel_combined = np.uint8(sobel_combined)
# Alternative: Scharr operator (more accurate)
scharrx = cv2.Scharr(gray, cv2.CV_64F, 1, 0)
scharry = cv2.Scharr(gray, cv2.CV_64F, 0, 1)
Canny Edge Detection
Multi-stage algorithm producing clean, connected edges.
# Canny edge detection
edges = cv2.Canny(gray, threshold1=100, threshold2=200)
# Steps performed by Canny:
# 1. Gaussian blur to reduce noise
# 2. Sobel operator to find gradients
# 3. Non-maximum suppression to thin edges
# 4. Double threshold to identify strong/weak edges
# 5. Edge tracking by hysteresis
# Adaptive thresholds
def auto_canny(image, sigma=0.33):
v = np.median(image)
lower = int(max(0, (1.0 - sigma) * v))
upper = int(min(255, (1.0 + sigma) * v))
return cv2.Canny(image, lower, upper)
Laplacian Edge Detection
# Laplacian (second derivative)
laplacian = cv2.Laplacian(gray, cv2.CV_64F)
laplacian = np.uint8(np.absolute(laplacian))
Morphological Operations
Morphological operations process images based on shapes, using structuring elements.
Basic Operations
import cv2
import numpy as np
# Create structuring element
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
kernel_ellipse = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
kernel_cross = cv2.getStructuringElement(cv2.MORPH_CROSS, (5, 5))
# Erosion - shrinks white regions
eroded = cv2.erode(binary_image, kernel, iterations=1)
# Dilation - expands white regions
dilated = cv2.dilate(binary_image, kernel, iterations=1)
# Opening - erosion followed by dilation (removes noise)
opening = cv2.morphologyEx(binary_image, cv2.MORPH_OPEN, kernel)
# Closing - dilation followed by erosion (fills holes)
closing = cv2.morphologyEx(binary_image, cv2.MORPH_CLOSE, kernel)
# Gradient - difference between dilation and erosion (outline)
gradient = cv2.morphologyEx(binary_image, cv2.MORPH_GRADIENT, kernel)
# Top Hat - difference between input and opening
tophat = cv2.morphologyEx(image, cv2.MORPH_TOPHAT, kernel)
# Black Hat - difference between closing and input
blackhat = cv2.morphologyEx(image, cv2.MORPH_BLACKHAT, kernel)
Image Transformations
Geometric Transformations
# Translation
M = np.float32([[1, 0, 100], [0, 1, 50]]) # Shift (100, 50)
translated = cv2.warpAffine(image, M, (width, height))
# Rotation
center = (width // 2, height // 2)
M = cv2.getRotationMatrix2D(center, angle=45, scale=1.0)
rotated = cv2.warpAffine(image, M, (width, height))
# Scaling
scaled = cv2.resize(image, None, fx=2.0, fy=2.0)
# Affine transformation (preserves parallelism)
pts1 = np.float32([[50, 50], [200, 50], [50, 200]])
pts2 = np.float32([[10, 100], [200, 50], [100, 250]])
M = cv2.getAffineTransform(pts1, pts2)
affine = cv2.warpAffine(image, M, (width, height))
# Perspective transformation
pts1 = np.float32([[56, 65], [368, 52], [28, 387], [389, 390]])
pts2 = np.float32([[0, 0], [300, 0], [0, 300], [300, 300]])
M = cv2.getPerspectiveTransform(pts1, pts2)
perspective = cv2.warpPerspective(image, M, (300, 300))
Intensity Transformations
# Brightness adjustment
bright = cv2.convertScaleAbs(image, alpha=1.0, beta=50) # beta is brightness
# Contrast adjustment
contrast = cv2.convertScaleAbs(image, alpha=1.5, beta=0) # alpha is contrast
# Gamma correction
def adjust_gamma(image, gamma=1.0):
inv_gamma = 1.0 / gamma
table = np.array([((i / 255.0) ** inv_gamma) * 255
for i in np.arange(0, 256)]).astype("uint8")
return cv2.LUT(image, table)
# Histogram equalization
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
equalized = cv2.equalizeHist(gray)
# CLAHE (Contrast Limited Adaptive Histogram Equalization)
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
clahe_img = clahe.apply(gray)
Image Thresholding
Convert grayscale images to binary by separating foreground from background.
# Simple thresholding
ret, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
ret, binary_inv = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY_INV)
# Otsu's method - automatic threshold calculation
ret, otsu = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# Adaptive thresholding - local threshold
adaptive_mean = cv2.adaptiveThreshold(
gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C,
cv2.THRESH_BINARY, blockSize=11, C=2
)
adaptive_gaussian = cv2.adaptiveThreshold(
gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, blockSize=11, C=2
)
Fourier Transform
Analyze images in the frequency domain for advanced filtering.
import numpy as np
import cv2
# Compute DFT
dft = cv2.dft(np.float32(gray), flags=cv2.DFT_COMPLEX_OUTPUT)
dft_shift = np.fft.fftshift(dft)
# Magnitude spectrum
magnitude = 20 * np.log(cv2.magnitude(dft_shift[:,:,0], dft_shift[:,:,1]))
# Create high-pass filter mask
rows, cols = gray.shape
crow, ccol = rows // 2, cols // 2
mask = np.ones((rows, cols, 2), np.uint8)
r = 30
center = [crow, ccol]
x, y = np.ogrid[:rows, :cols]
mask_area = (x - center[0])**2 + (y - center[1])**2 <= r*r
mask[mask_area] = 0
# Apply mask
fshift = dft_shift * mask
# Inverse DFT
f_ishift = np.fft.ifftshift(fshift)
img_back = cv2.idft(f_ishift)
img_back = cv2.magnitude(img_back[:,:,0], img_back[:,:,1])
Chapter Summary
- Digital images are represented as arrays of pixel values in various color spaces (RGB, HSV, LAB, etc.)
- Basic operations include reading, writing, resizing, cropping, and padding images
- Convolution is the foundation of image filtering, using kernels to transform pixel neighborhoods
- Blurring techniques (Gaussian, median, bilateral) reduce noise with different characteristics
- Edge detection algorithms (Sobel, Canny, Laplacian) identify boundaries in images
- Morphological operations manipulate shapes using erosion, dilation, opening, and closing
- Geometric transformations enable rotation, scaling, and perspective correction
- Intensity transformations adjust brightness, contrast, and equalize histograms
- Thresholding converts grayscale to binary using global or adaptive methods
- Fourier transforms enable frequency-domain analysis and filtering
Review Questions
- What are the advantages of using HSV color space over RGB for color-based segmentation?
- Explain the difference between average blur and Gaussian blur.
- How does the Canny edge detector differ from the Sobel operator?
- When would you use morphological opening vs. closing operations?
- What is the purpose of histogram equalization, and when might CLAHE be preferred?
- Describe the difference between affine and perspective transformations.
- Why is Otsu's method useful for automatic thresholding?
- What are the practical applications of bilateral filtering?
- How does convolution with different kernels produce different effects?
- When would you use Fourier transform in image processing?