Libraries: Numpy (1.5 weeks)

Contents

Libraries: Numpy (1.5 weeks)#

Python Programming for Engineers#

Tel-Aviv University / 0509-1820 / Fall 2025-2026#

Agenda: Numpy#

  • Intorduction

  • Matrix operations

  • Image processing

    • Basic operations

    • Binary segmentation - Self learning

    • Ternary segmentation - Self learning

    • Image gradient

    • Image brightening

    • Morphological operators

    • Denoising

Matrix Operations#

import numpy#

import numpy as np 
import matplotlib.pyplot as plt
import imageio.v2 as imageio
import utils.load_auxilary_files # This will load the files used for this notebook
['orders_4_2024.csv', 'witcher_2.csv', 'evil_morty_change_noised.png', 'StudentsGrades.csv', 'dog.png', 'notebook_resources.zip', 'countries-of-the-world_out.csv', 'erosion_2.png', 'dialation_2.png', 'ship.png', 'ex1.csv', 'ex2.csv', 'countries-of-the-world.csv', 'koala.png', 'orders_2_2024.csv', 'hello', 'dialation.png', '__MACOSX', 'baby.png', 'evil_morty_change_3.png', 'evil_morty_segmentation.png', 'woman_noised.png', 'orders_3_2024.csv', 'evil_morty_change.png', 'erosion.png', 'evil_morty_1.png', 'dog_noised.png', 'witcher_1.csv', 'products2.csv']

NumPy Functions#

Numpy common functions

Stacking/appending matrices vertically (add rows)#

a = np.array([[1,2,3],[4,5,6]])
b = np.array([[7,8,9],[1,1,1]])
print(np.vstack((a,b)))
[[1 2 3]
 [4 5 6]
 [7 8 9]
 [1 1 1]]

Stacking/appending matrices horizontally (add columns)#

a = np.array([[1,2,3],[4,5,6]])
b = np.array([[7,8,9],[1,1,1]])
print(np.hstack((a,b)))
[[1 2 3 7 8 9]
 [4 5 6 1 1 1]]

Image processing with numpy#

Grayscale image#

A table of integer values, aka pixels, each in the range of 0…255:#

  • 0 = Black

  • 255 = White

Examples for image processing tasks#

  • Binary segmentation

    • Pixel>threshold -> white, else black

  • Ternary segmentation

    • Mapping the whole brightness range to a fixed number of values

  • Image gradient

    • Difference between neighboring pixels

  • Image brightening

    • Making each pixel brighter

  • Morphological operators

    • Erosion – more black, less white

    • Dilation – more white, less black

  • Denoising

    • recovering a clean image from a noisy one

All these tasks can be achieved using matrix operation!#

Image processing - basic operations#

Reading an image from disk:#

import imageio.v2 as imageio
im = imageio.imread('files/evil_morty_1.png')
print(im)

print(type(im), im.shape)
[[120 105  97 ... 201 202 203]
 [110 116 115 ... 202 201 202]
 [109 106 109 ... 200 201 202]
 ...
 [215 215 217 ... 238 238 238]
 [215 216 216 ... 238 238 238]
 [215 216 216 ... 238 238 238]]
<class 'numpy.ndarray'> (342, 611)

Creating an “empty”(?) image matrix:#

height=20
width=20

im = np.zeros((height,width), dtype=np.uint8) 

Important: each pixel is in the range 0-255 (type np.uint8) #

  • Numerical operations cause overflow, e.g.:

  • Therefore, before doing numerical operations on image pixels, convert the whole image or a specific pixel to int 32-bit using np.int_

Image gradient#

  • Given an image we may want to locate points of major change in intensity.

  • We need to create a gradient image, so that every pixel is the difference between the corresponding pixel in the original image and a neighboring pixel.

  • We can compute the gradient with reference to each of the image dimensions.

How do we create such an image?#

Shift up and subtract images#

Implement the function create_gradient(im) that gets an image and returns the vertical gradient image as detailed above.

hint: Create the matrix upshifted_im (im without the first row and add a row of zeros). Then create the matrix diff.

import numpy as np
import matplotlib.pyplot as plt
import imageio.v2 as imageio
im = imageio.imread('files/evil_morty_segmentation.png')
def create_gradient(im):
    pass
gradient = create_gradient(im)
plt.figure(figsize=(15,15))
plt.imshow(gradient, cmap=plt.cm.gray)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[10], line 3
      1 gradient = create_gradient(im)
      2 plt.figure(figsize=(15,15))
----> 3 plt.imshow(gradient, cmap=plt.cm.gray)

File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/matplotlib/pyplot.py:3784, in imshow(X, cmap, norm, aspect, interpolation, alpha, vmin, vmax, colorizer, origin, extent, interpolation_stage, filternorm, filterrad, resample, url, data, **kwargs)
   3762 @_copy_docstring_and_deprecators(Axes.imshow)
   3763 def imshow(
   3764     X: ArrayLike | PIL.Image.Image,
   (...)   3782     **kwargs,
   3783 ) -> AxesImage:
-> 3784     __ret = gca().imshow(
   3785         X,
   3786         cmap=cmap,
   3787         norm=norm,
   3788         aspect=aspect,
   3789         interpolation=interpolation,
   3790         alpha=alpha,
   3791         vmin=vmin,
   3792         vmax=vmax,
   3793         colorizer=colorizer,
   3794         origin=origin,
   3795         extent=extent,
   3796         interpolation_stage=interpolation_stage,
   3797         filternorm=filternorm,
   3798         filterrad=filterrad,
   3799         resample=resample,
   3800         url=url,
   3801         **({"data": data} if data is not None else {}),
   3802         **kwargs,
   3803     )
   3804     sci(__ret)
   3805     return __ret

    [... skipping hidden 1 frame]

File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/matplotlib/axes/_axes.py:6377, in Axes.imshow(self, X, cmap, norm, aspect, interpolation, alpha, vmin, vmax, colorizer, origin, extent, interpolation_stage, filternorm, filterrad, resample, url, **kwargs)
   6374 if aspect is not None:
   6375     self.set_aspect(aspect)
-> 6377 im.set_data(X)
   6378 im.set_alpha(alpha)
   6379 if im.get_clip_path() is None:
   6380     # image does not already have clipping set, clip to Axes patch

File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/matplotlib/image.py:709, in _ImageBase.set_data(self, A)
    707 if isinstance(A, PIL.Image.Image):
    708     A = pil_to_array(A)  # Needed e.g. to apply png palette.
--> 709 self._A = self._normalize_image_array(A)
    710 self._imcache = None
    711 self.stale = True

File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/matplotlib/image.py:672, in _ImageBase._normalize_image_array(A)
    670 A = cbook.safe_masked_invalid(A, copy=True)
    671 if A.dtype != np.uint8 and not np.can_cast(A.dtype, float, "same_kind"):
--> 672     raise TypeError(f"Image data of dtype {A.dtype} cannot be "
    673                     f"converted to float")
    674 if A.ndim == 3 and A.shape[-1] == 1:
    675     A = A.squeeze(-1)  # If just (M, N, 1), assume scalar and apply colormap.

TypeError: Image data of dtype object cannot be converted to float
_images/3d46d4e15eb7f2ee385d06d373570d3ed098975b3a3ecb1f033438e3e19f9389.png
def create_gradient(im):
    # Create upshifted_im
    zero_row = np.zeros((1,im.shape[1]), dtype=np.int_)
    upshifted_im = np.vstack((im[1:,:], zero_row))
    
    # Create diff
    diff = np.int_(upshifted_im) - np.int_(im)
    abs_diff = np.abs(diff)
    return abs_diff

gradient = create_gradient(im)
plt.figure(figsize=(15,15))
plt.imshow(gradient, cmap=plt.cm.gray)
<matplotlib.image.AxesImage at 0x29399848d70>
_images/619b703e8d4ba002117f4f00b7c0d61a0c5a0a3dc3c69aa867529916d75642c1.png

Can it also be computed using for loops?#

Brighening the gradient#

Problem: We can hardly see the gradient image.#

  • let’s brighten it.

Implement the function brighten_gradient(gr) that brightens each pixel 5-fold (times 5). This will significantly brighten every pixel that is not completely black.

def brighten_gradient(gr):
    # Write your code here
    pass
# Solution
def brighten_gradient(gr):
    brighten_gr = gr * 5
    more_than_255 = gr > 255
    gr[more_than_255] = 255
    return gr

# Better solution
def brighten_gradient(gr):
    return np.minimum(gr * 5, 255)

brighten_gr = brighten_gradient(gradient)

fig,ax=plt.subplots(1,3, figsize=(100,40))
ax[0].imshow(im, cmap=plt.cm.gray)
ax[1].imshow(gradient, cmap=plt.cm.gray)
ax[2].imshow(brighten_gr, cmap=plt.cm.gray)
<matplotlib.image.AxesImage at 0x2939992f610>
_images/617fbffaabc9634b7bf1f2a1290bd63da2ba6296fec7f7831641ad3aef8ed720.png

Morph by neighbors#

  • Morphology is a technique for the analysis and processing of geometrical structures

  • Morph-by-neighbors (or, morphological) operators change a pixel according to its neighborhood

Erosion#

  • Take binary image ; produce more black, less white.

    • Edges of white areas (“foreground”) erode away, so black areas (“background”) grow.

Dilation – inverse of erosion.#

  • Boundaries of black areas erode away, so white areas grow.

Implementation#

#Top-down design:
# the "operator" variable is a specific morph function
def morph_by_neighborhood(im, operator, dx=5, dy=5):
    new_im = np.zeros(im.shape)
    for x in range(im.shape[0]):
        for y in range(im.shape[1]):
            neighborhood = get_neighborhood(im, x, y, dx, dy)
            new_im[x, y] = operator(neighborhood)
    return new_im
def get_neighborhood(im,x,y,dx,dy):
    return im[max(x-dx,0):min(x+dx+1,im.shape[0]), max(y-dy,0):min(y+dy+1,im.shape[1])]
def erosion(neighborhood):
    return np.min(neighborhood)

def dialation(neighborhood):
    return np.max(neighborhood)

Erosion - examples#

fname="files/erosion.png"
im = imageio.imread(fname)
im_transformed=morph_by_neighborhood(im, erosion) # Can pass np.min instead of "erosion"
fig,ax=plt.subplots(1,2)
ax[0].imshow(im, cmap=plt.cm.gray)
ax[1].imshow(im_transformed, cmap=plt.cm.gray)
<matplotlib.image.AxesImage at 0x2939bb3f610>
_images/2e5f7043ef7d33be05cd37cb9b4004340776f55dae15b95d314562c49847a4b0.png
fname="files/erosion_2.png"
im = imageio.imread(fname)
im_transformed=morph_by_neighborhood(im, np.min)
fig,ax=plt.subplots(1,2)
ax[0].imshow(im, cmap=plt.cm.gray)
ax[1].imshow(im_transformed, cmap=plt.cm.gray)
<matplotlib.image.AxesImage at 0x2939bc302d0>
_images/c209472072bf8131a4dcea50c20558729a4f2607483e92c601122fdc99bcb038.png

Dialation - examples#

fname="files/dialation.png"
im = imageio.imread(fname)
im_transformed=morph_by_neighborhood(im, dialation) # Can pass np.max instead of "erosion"
fig,ax=plt.subplots(1,2)
ax[0].imshow(im, cmap=plt.cm.gray)
ax[1].imshow(im_transformed, cmap=plt.cm.gray)
<matplotlib.image.AxesImage at 0x2939bce8f50>
_images/fbd5cd6f06726edf433ff88c725359b83125bff6cd7116f4ffa8ceccbf96b346.png
fname="files/dialation_2.png"
im = imageio.imread(fname)
im_transformed=morph_by_neighborhood(im, np.max)
fig,ax=plt.subplots(1,2)
ax[0].imshow(im, cmap=plt.cm.gray)
ax[1].imshow(im_transformed, cmap=plt.cm.gray)
<matplotlib.image.AxesImage at 0x2939bed5bd0>
_images/41286912a147ea401ffea650aa6b40d5bd38e96a32b25a8cb7ee7b5fcefd57c0.png

Detect Edges with Dilation#

  • Thicken white edges a bit, using dilation

  • Compute difference from original image

im1 = imageio.imread('files/erosion_2.png')
im2 = morph_by_neighborhood(im1,dialation, 1, 1)
imDiff = im2 - im1
fig,ax=plt.subplots(1,3)
ax[0].imshow(im1, cmap=plt.cm.gray)
ax[1].imshow(im2, cmap=plt.cm.gray)
ax[2].imshow(imDiff, cmap=plt.cm.gray)
<matplotlib.image.AxesImage at 0x2939bfb5a90>
_images/8c96b5cce20b313f5b574057dadf02130f89468f10d2cf2ee08a26932f45a229.png

Denoising#

We want to “clean” this pictures from noise. Any suggestions?#

im = imageio.imread('files/evil_morty_change_noised.png')

First attempt: smoothing using mean#

  • Use the morph_by_neighborhood framework with mean (average):

def denoise_mean(im, dx=5, dy=5):
    return morph_by_neighborhood(im, np.mean, dx, dy)
im_transformed=denoise_mean(im)
fig,ax=plt.subplots(1,2, figsize=(10,10))
ax[0].imshow(im, cmap=plt.cm.gray)
ax[1].imshow(im_transformed, cmap=plt.cm.gray)
<matplotlib.image.AxesImage at 0x2939bd43750>
_images/61767eabbb4bd351b050f3b05af6e31f0389c1d44d753c9e2ea44b5f139f4016.png

Second attempt: smoothing using median#

  • Use the morph_by_neighborhood framework with median:

def denoise_median(im, dx=5, dy=5):
    return morph_by_neighborhood(im, np.median, dx, dy)
im_transformed=denoise_median(im)
fig,ax=plt.subplots(1,2, figsize=(10,10))
ax[0].imshow(im, cmap=plt.cm.gray)
ax[1].imshow(im_transformed, cmap=plt.cm.gray)
<matplotlib.image.AxesImage at 0x2939c07de50>
_images/9a99d3197b5a89b90068f060aea406ed2775d935fb586b94edc16e197c1c7749.png

Can we do better?#

  • We did not exploit what we know about the ‘noise’

  • The ‘noise’ can only be almost white or almost black

    • So change only “too” bright or “too” dark pixels!

    • “too” bright and “too” black are determined by predefined thresholds (W, B, respectively)

solution outline#

  • Pixels not too bright or too dark remain with no change.

  • Otherwise, examine the pixel’s neighborhood neighborhood.

    • too dark:

      • Get the median of neighborhood, but ignore ‘too dark’ pixels

    • too bright:

      • Get the median of neighborhood, but ignore ‘too bright’ pixels

def my_median(nbrs, min_val=15, max_val=225):
    good_nbrs = nbrs[(nbrs > min_val) & (nbrs < max_val)]
    if good_nbrs.size>0: #There are relevant neighbors
        return np.median(good_nbrs)

    #"else": i.e., no relevant neighbors
    center = (nbrs.shape[0]//2, nbrs.shape[1]//2)
    return nbrs[center]
def clear_noise(nbrs, min_val=15, max_val=225):
    center = (nbrs.shape[0]//2, nbrs.shape[1]//2)
    if not (min_val < nbrs[center] < max_val): #Abnormal pixel
        return my_median(nbrs, min_val, max_val)

    #"else": i.e., a good one, return it
    return nbrs[center]
def denoise_bounded_median(im, dx=2, dy=2):
    fig,ax=plt.subplots(1,4, figsize=(50,10))
    ax[0].imshow(im, cmap=plt.cm.gray)
    num_repeats = 3
    for i in range(num_repeats):
        im = morph_by_neighborhood(im, clear_noise, dx, dy)
        ax[i+1].imshow(im, cmap=plt.cm.gray)
    return im
denoised_image=denoise_bounded_median(im)
_images/ce3fa8b2790b3e2fb45bb9b219fb5d774a8ab385f600cbde5315b48eb9f51e8b.png
original_image = imageio.imread('files/evil_morty_change.png')
fig,ax=plt.subplots(1,2, figsize=(20,10))
ax[0].imshow(original_image, cmap=plt.cm.gray)
ax[1].imshow(denoised_image, cmap=plt.cm.gray)
<matplotlib.image.AxesImage at 0x2939dfb6490>
_images/cc99c2216f44275c5e21579908fba8b9a21d6a5deea6dd54dcebd12d715463ef.png

Self Learning#

Binary Segmentation#

Goal: grayscale image \(\rightarrow\) black and white image#

  • pixel > threshold \(\rightarrow\) change to white (255)

  • pixel <= threshold \(\rightarrow\) change to black (0)

Motivation: for example, save space#

Implementation#

def segmentation(im, threshold):
    # Write your code here
    pass

Solution#

def segmentation(im, threshold):
    # find the pixels that are > threshold
    over_threshold = im > threshold

    # make them white (255)
    binary_image = over_threshold * 255
    
    return binary_image

Example#

im = imageio.imread('files/evil_morty_1.png')
print(im)
[[120 105  97 ... 201 202 203]
 [110 116 115 ... 202 201 202]
 [109 106 109 ... 200 201 202]
 ...
 [215 215 217 ... 238 238 238]
 [215 216 216 ... 238 238 238]
 [215 216 216 ... 238 238 238]]
print(type(im))
print(im.shape, im.dtype)
print(np.min(im), np.max(im))
<class 'numpy.ndarray'>
(342, 611) uint8
1 255
plt.figure()  # opens a new figure window
plt.imshow(im, cmap=plt.cm.gray)
<matplotlib.image.AxesImage at 0x2939def0050>
_images/3c29a5f58eae28ab4eecffd57fb90119cc1e4742a9186aa54daef44850fd7e54.png
threshold = 128 #Later we’ll learn how to optimize this threshold
bin_im = segmentation(im, threshold)

Show the binary image#

plt.figure()
plt.imshow(bin_im, cmap=plt.cm.gray)
<matplotlib.image.AxesImage at 0x2939ed2bd90>
_images/035ec7a89a20099b420c1bc133053efa9eb1da37c6f29afe87b8020b3a1a4571.png

Ternary segmentation#

Ternary segmentation – like binary segmentation but with 3 values.#

  • Transform grayscale to black/gray/white.

  • First, find 2 thresholds (e.g, th1, th2) such that:

    • 1/3 pixels turn white 1/3 turn gray 1/3 turn black

  • For example: If the values of the image pixels are 1 to 90 then:

    • first th1 = 30; th2 = 60

  • Then for each pixel p assign:

    • 0 if \(p \le th1\)

    • 128 if \(th1<p \le th2\)

    • 255 if \(th2 < p\)

Solution outline#

We will break the problem into smaller problems:#

  1. Find 2 thresholds from cumulative histogram.
    a. Create a histogram.
    b. Create a cumulative histogram.
    c. Find the thresholds

  2. Transform the image using the thresholds.

2. Assume we have a find_thrds(im) function#

(we will implement it later, don’t worry).#

Implement a ternary_segmentation function:#

  • Input: a grayscale image

  • Output: a new image where every pixel has one of 3 gray levels: 0, 128 or 255

def ternary_segmentation(im):
    pass
# Solution
def ternary_segmentation(im):
    th1, th2 = find_thrds(im)
    newIm = np.tile(0, im.shape)
    newIm[im > th1] = 128
    newIm[im > th2] = 255
    return newIm

1. Find thresholds#

1.a. Image histogram#

Implement the function build_hist(im):#

  • Input: grayscale image.

  • Output: histogram of gray levels in image.

Our histogram is list with 256 values:#

  • Index = gray level

  • Value = number of pixels with that gray level

def build_hist(im):
    pass
# Solution
def build_hist(im):
    hist = [0]*256
    for r in range(im.shape[0]):
        for c in range(im.shape[1]):
            gray_level = im[r, c]
            hist[gray_level] += 1
    return hist

#Same as:
#return np.bincount(im.flatten())

1.b. Cumulative histogram#

Implement the function cumulative sum(hist):

  • Input: histogram
  • Output: list of the cumulative sums of nums
| |
def cumulative_sum(hist):
    pass
# Solution
def cumulative_sum(hist):
    if not hist: return []
    result = [hist[0]]
    for i in range(1,len(hist)):
        prev_sum = result[i-1]   
        result.append(prev_sum + hist[i])
    return result

#Same as:
#return [sum(hist[:i+1]) for i in range(len(hist))] 
#Or:    
#return list(np.cumsum(hist)) 

1.c. Finding the thresholds#

Given an image, find two thresholds th1, th2 such that:#

  • \(\frac{1}{3} \) of all pixels \(\le\) th1 \(\le \frac{2}{3}\) of all pixels

  • \(\frac{2}{3} \) of all pixels \(\le\) th2 \(\le \frac{1}{3}\) of all pixels

  • In reality, the threshold is approximated by the minimum value that is greater than the minimal fraction of pixels

Solution: use histogram + cumulative_sum#

  • For example: for a 5X5 image with 7 gray levels (0-6):

hist = [1,5,4,8,3,3,1]
hist_cumulative_sum=cumulative_sum(hist) # [1,6,10,18,21,24,25]
sum_of_pixels=sum(hist)
print(f'histogram: {hist}')
print(f'cumulative histogram {hist_cumulative_sum}')
print(f'{hist_cumulative_sum[1]/sum_of_pixels} < 1/3 < {hist_cumulative_sum[2]/sum_of_pixels} --> th1=2') # th1=2
print(f'{hist_cumulative_sum[2]/sum_of_pixels} < 2/3 < {hist_cumulative_sum[3]/sum_of_pixels} --> th2=3') # th1=3
histogram: [1, 5, 4, 8, 3, 3, 1]
cumulative histogram [1, 6, 10, 18, 21, 24, 25]
0.24 < 1/3 < 0.4 --> th1=2
0.4 < 2/3 < 0.72 --> th2=3

Implementation of thresholds finding#

def find_thrds(im):
    hist = build_hist(im)
    cum_lst = cumulative_sum(hist)
    i = 0
    while cum_lst[i] / im.size < 1 / 3:
        i += 1
    th1 = i
    while cum_lst[i] / im.size < 2 / 3:
        i += 1
    th2 = i
    return th1, th2
#Or simply use:
#return np.percentile(im, 33), np.percentile(im, 66)
im = imageio.imread('files/evil_morty_segmentation.png') 
import matplotlib.pyplot as plt 
im1_transformed=ternary_segmentation(im)
fig,ax=plt.subplots()
ax.imshow(im1_transformed, cmap=plt.cm.gray)
<matplotlib.image.AxesImage at 0x2939f0c9e50>
_images/50a959dfab343725891d019532a02c06d1e097158cf5dbae22cf778a39d61d28.png

Question from previous exams#

Open Exam 2023-2024 semester A Moed B and answer questions 3.1 - 3.4 (including 3.4).

Solutions are shown below.

3.A

def convert_to_binary(im):
    res = np.zeros(im.shape)
    flt = im.flatten()
    bc = np.bincount(flt)
    am = np.argmax(bc)
    res[im >= am] = 255
    return res
arr=np.array([[1,2,5],[4,4,3]])
convert_to_binary(arr)
array([[  0.,   0., 255.],
       [255., 255.,   0.]])

3.B

def encode_row_helper(im_row):
    im_row_shift=np.hstack((np.array([255]), im_row))
    idxs=np.nonzero(np.diff(im_row_shift))[0]
    return idxs

def encode_row(im_row):
    idxs = encode_row_helper(im_row)
    idx_diff = np.diff(np.hstack((idxs, [len(im_row)])))
    return np.hstack(([idxs[0]],idx_diff))

im_row =[255,0,255,255]
print(encode_row(im_row))
[1 1 2]

3.C

def encode(im):
    ar = np.array([])
    for a in range(im.shape[0]):
        ar = np.hstack((ar, encode_row(im[a]), [-1]))
    return ar[:-1]

im_bin=np.array([[0,0,255,255],
[255,0,255,255],
[0,0,255,255]])
encode(im_bin)
array([ 0.,  2.,  2., -1.,  1.,  1.,  2., -1.,  0.,  2.,  2.])

3.D

def calc_ratio(im,im_enc):
    return np.sum(im_enc[im_enc != -1]) / im.size

im=np.array([[0, 1, 2, 3],
             [2, 1, 2, 3],
             [0, 1, 2, 3]])

im_bin = convert_to_binary(im)
im_enc = encode(im_bin)
print(calc_ratio(im_bin,im_enc))
1.0

Numpy: summary#

Many more operations can be performed on images, in particular:#

  • Numpy is very convenient library for numerical manipulations

  • Can be used for geometric transformations: rotation, flipping, resizing, etc.

  • Many application, including image processsing, which we covered thouroughly.