Actually, for checking out clearer output, refer to the follwoing Google Colab jupyter notebook output.

1. Basic Matrix/Vector Manipulation (20 points)

In Python, calculate the following by filling out p1.py. Given matrix M and vectors a,b,c such that \( M =\begin{bmatrix}1&2&3\\4&5&6\\7&8&9\\0&2&2\end{bmatrix}, a =\begin{bmatrix}1\\1\\0\end{bmatrix}, b =\begin{bmatrix}−1\\2\\5\end{bmatrix}, c =\begin{bmatrix}0\\2\\3\\2\end{bmatrix} \)

(a) Define Matrix $M$ and Vectors $a,b,c$ in Python using Numpy.

M, a, b, c = np.array([[1,2,3],[4,5,6],[7,8,9],[0,2,2]]), np.array([[1],[1],[0]]), np.array([[-1],[2],[5]]),np.array([[0],[2],[3],[2]])

(b) Find the dot product of vectors a and b (i.e. $a^\top b$).

aDotb = np.dot(a.T, b)

(c) Find the element-wise product of a and b $[a_1b_1, a_2b_2, a_3b_3]^\top$.

aProdb = np.multiply(a, b)

(d) Find $(a^\top b)Ma$.

result = np.dot(a.T, b) * np.dot(M, a)

(e) Without using a loop, multiply each row of $M$ element-wise by a. Briefly explain the logic of your code in your written report.
Making use of broadcasting

tile = np.tile(a.T, (M.shape[0], 1)) # broadcasting a
newM = np.multiply(M, tile)

(f) Without using a loop, sort all of the values of the new $M$ from (e) in increasing order and plot them in your report. Briefly explain the logic of your code in your written report.
Making use of sorting

sortedM = np.sort(M, axis=None)

2. Basic Image Manipulations (40 points)

Do the following by filling out p2.py:
(a) Read in the images, image1.jpg and image2.jpg, as color images using io.imread from the skimage package.

img1, img2 = io.imread('./image1.jpg'), io.imread('./image2.jpg')

(b) Convert the images to double precision and rescale them to stretch from minimum value 0 to maximum value 1.

def normalize_img(img):
    return (img-np.min(img))/(np.max(img)-np.min(img))
img1, img2 = img1.astype(np.double), img2.astype(np.double)
img1, img2 = normalize_img(img1), normalize_img(img2)

(c) Add the images together and re-normalize them to have minimum value 0 and maximum value 1. Save and include this image in your report.

sumImage = img1 + img2
sumImage = normalize_img(sumImage)

(d) Create a new image such that the left half of the image is the left half of image1 and the right half of the image is the right half of image2. Save and include this image in your report.

assert img1.shape==img2.shape, '2 images do not have same shape'  # MxN in grayscale, MxNx3 in color image
newImage1 = np.zeros_like(img1)
newImage1[:, img1.shape[1]//2:] = img1[:, img1.shape[1]//2:]
newImage1[:, :img1.shape[1]//2] = img2[:, :img2.shape[1]//2]

(e) Using a for loop, create a new image such that every odd numbered row is the corresponding row from image1 and the every even row is the corresponding row from image2 (Hint: Remember that indices start at 0 and not 1 in Python). Save and include this image in your report.

assert img1.shape==img2.shape, '2 images do not have same shape'  # MxN in grayscale, MxNx3 in color image
newImage2 = np.zeros_like(img1)
for i in range(img1.shape[0]//2):
    newImage2[2*i+1, :] = img1[2*i+1, :]
    newImage2[2*i, :] = img2[2*i, :]

(f) Accomplish the same task as part e without using a for-loop (the functions reshape and tile may be helpful here). Briefly explain the logic of your code in your written report.

assert img1.shape==img2.shape, '2 images do not have same shape'  # MxN in grayscale, MxNx3 in color image
newImage3 = np.zeros_like(img1)
newImage3[1::2, :] = img1[1::2, :]
newImage3[::2, :] = img2[::2, :]

(g) Convert the result from part f to a grayscale image. Save and include the grayscale image in your report.

grayVec = np.array([0.299, 0.587, 0.114])
grayImage = np.dot(img, grayVec)
print(grayImage.shape)

3.Singular Value Decomposition (40 points)

Do the following by filling out p3.py:
(a) After reading in image1 as a grayscale image (this is done for you in the code already) take the singular value decomposition of the image.

u, s, v = np.linalg.svd(img1)
v = v.T

(b) Recall from the discussion section that the best rank n approximation of a matrix is $\sum_{i=1}^n u_i \sigma_iv_i^⊤$, where $u_i$, $\sigma_i$, and $v_i$ are the $i$th left singular vector, singular value, and right singular vector respectively. Save and include the best rank 1 approximation of the (grayscale) image1 in your report.

def low_rank_approx(rank, u, s, v):
    # we recommend you implement this helper function for parts b and c
    rankapprox = np.zeros((u.shape[0], v.shape[0]))
    for i in range(rank):
        ui=np.expand_dims(u[:, i], axis=-1) 
        vi=np.expand_dims(v[:, i], axis=-1)
        rankapprox += s[i]*np.dot(ui, vi.T)
    return rankapprox
rank1approx = low_rank_approx(1, u, s, v)

(c) Save and include the best rank 20 approximation of the (grayscale) image1 in your report.

rank20approx = low_rank_approx(20, u, s, v)