To begin with, we import the libraries we will need
import numpy as np
from PIL import Image, ImageFilter
from IPython.display import display # to display images
%matplotlib inline
import matplotlib.pyplot as plt
Now we import the picture we are using: in this case, a picture from the classical "7 Samurais", by Akira Kurosawa
image = Image.open("7-samurais.jpg")
#image = image.filter(ImageFilter.SHARPEN )
display(image)
We can now import this matrix as a numpy matrix
pixels = np.asarray(image)
whose size is
np.shape(pixels)
In Python, the SVD decomposition is part of the numpy library. There are two things that we can do: the full decomposition
u_full,sigma_full, v_full = np.linalg.svd(pixels,full_matrices=True)
print("which give us matrices of size\n U_full:",np.shape(u_full),"\n Sigma_full:", np.shape(sigma_full),"\n V_full:",np.shape(v_full))
Note that Sigma is a "rank one vector".
It is also possible to get a more economic, not full decomposition
u_not_full,sigma_not_full, v_not_full = np.linalg.svd(pixels,full_matrices=False)
print("which give us matrices of size\n U_not_full:",np.shape(u_not_full),"\n Sigma_not_full:", np.shape(sigma_not_full),"\n V_not_full:",np.shape(v_not_full))
In case you have not noticed: the size of the last matrix is different.
If you want, you can check the unitary property of the matrices (u/v)_full and (u/v)_not_full:
u_full.T*u_full
Wait... shouldn't it be the identity matrix?!
Well... what happens is that the "*" symbols stands for \textit{termwise multiplication}! If you wanto do to a matrix multiplication you have to use the numpy package. For instace, we can compare the product with the identity matrix, and than compute the norm. If the numerical result is good we should get a very small number:
np.linalg.norm(np.matmul(np.conjugate(u_full).T,u_full) - np.identity(np.shape(u_full)[1]))
which is indeed the case.
In general the singular values are ordered in decreasing order of magnitude. Let's plot them:
plt.plot(sigma_full)
plt.title("Singular values")
plt.ylabel("Magnitude")
We can see that a good part of the energy of this matrix is contained in the first 30~50 singular eigenvalues.
Now let's play a bit: we shall use the best rank-k matrix to approximate our matrix. We can construct that as follows:
def reconstruct_image_function(k,u, sigma, v):
return np.matmul(u_full[:,:k]*sigma_full[:k],v_full[:k,:])
k = 2 # rank of the matrix, that is, number of non-null singular values.
reconstruct_image = reconstruct_image_function(k,u_full, sigma_full, v_full)
Notice that this matrix has the same size as the previous mnatrix, that is,
np.shape(reconstruct_image)
Now we transform this matrix of numbers to an image format, so that we can visualize it.
def display_image_function(k,image_as_array):
img = Image.fromarray(image_as_array)
img_p = img.convert('L')
print("A rank",k, "image:")
display(img_p,image)
display_image_function(k,reconstruct_image)
As you make the value of k larger you get better quality images (approximations, in the $l^2$ norm):
k=4
reconstruct_image = reconstruct_image_function(k,u_full, sigma_full, v_full)
display_image_function(k,reconstruct_image)
k=8
reconstruct_image = reconstruct_image_function(k,u_full, sigma_full, v_full)
display_image_function(k,reconstruct_image)
k=16
reconstruct_image = reconstruct_image_function(k,u_full, sigma_full, v_full)
display_image_function(k,reconstruct_image)
k=32
reconstruct_image = reconstruct_image_function(k,u_full, sigma_full, v_full)
display_image_function(k,reconstruct_image)
k=64
reconstruct_image = reconstruct_image_function(k,u_full, sigma_full, v_full)
display_image_function(k,reconstruct_image)
k=128
reconstruct_image = reconstruct_image_function(k,u_full, sigma_full, v_full)
display_image_function(k,reconstruct_image)