Search code examples
pythonnumpymatplotlibanimationsvd

Python SVD fix the number of eigenvalues to rebuild the image?


I am trying to rebuild an image that I previously decomposed with SVD. The image is this:

enter image description here

I successfully decomposed the image with this code:

from PIL import Image
import numpy as np
import matplotlib.pyplot as plt

img = Image.open('steve.jpg')
img = np.mean(img, 2)

U,s,V = np.linalg.svd(img)

s an array of the singular values of the image. The more singular values I take, the more the reconstructed image is similar to the original one.
For example, if I take 20 singular values:

n = 20
S = np.zeros(np.shape(img))
for i in range(0, n):
    S[i, i] = s[i]

recon_img = U@S@V

plt.imshow(recon_img)
plt.axis('off')
plt.show()

enter image description here

I would like to fix the minumum number of singular values in order to get a good result: an image pretty similary to the original one. Moreover, I would like to see how much the result changes when I take a higher number of singular values. I tried with an animation without success:

from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation

img = Image.open('steve.jpg')
img = np.mean(img, 2)

U,s,V = np.linalg.svd(img)

fig = plt.figure()

def update(i):
    S = np.zeros(np.shape(img))
    n = 20
    for i in range(0, n):
        S[i, i] = s[i]

    recon_img = U@S@V

    plt.imshow(recon_img)
    plt.axis('off')

ani = FuncAnimation(fig = fig, func = update, frames = 20, interval = 10)
plt.show()

Solution

  • If you plot the s singular values you can see a very steep decreasing curve, better if you use a log scale for the y axis:

    plt.semilogy(s, 'k-')
    

    enter image description here

    As you can see, the first 50 singular values are the most important ones: almost everyone more that 1000. Values from the ~50th to the ~250th are an order of magnitude lower and their values decreases slowly: the slope of the curve is contained (remember the logarithmic y scale). That beeing said I would take the first 50 elements to rebulid your image.


    Regarding the animation: while the animation updates frame by frame, the counter i is increased by 1. In your code, you mistakenly use i to slice the s and define S; you should rename the counter.
    Moreover, as animation goes on, you need to take an increasing number of singular values, this is set by n which you keep constant frame by frame. You need to update n at each loop, so you can use it as the counter.
    Furthermore, you need the erase the previous plotted image, so you need to add a plt.gca().cla() at the beginning of the update function.
    Check the code below for reference:

    from PIL import Image
    import numpy as np
    import matplotlib.pyplot as plt
    from matplotlib.animation import FuncAnimation
    
    img = Image.open('steve.jpg')
    img = np.mean(img, 2)
    
    U,s,V = np.linalg.svd(img)
    
    fig, ax = plt.subplots(1, 2, figsize = (4, 4))
    
    ax[0].imshow(img)
    ax[0].axis('off')
    ax[0].set_title('Original')
    
    def init():
        ax[1].cla()
        ax[1].imshow(np.zeros(np.shape(img)))
        ax[1].axis('off')
        ax[1].set_title('Reconstructed\nn = 00')
    
    
    def update(n):
        ax[1].cla()
        S = np.zeros(np.shape(img))
        for i in range(0, n):
            S[i, i] = s[i]
    
        recon_img = U@S@V
    
        ax[1].imshow(recon_img)
        ax[1].axis('off')
        ax[1].set_title(f'Reconstructed\nn = {n:02}')
    
    ani = FuncAnimation(fig = fig, func = update, frames = 50, init_func = init, interval = 10)
    ani.save('ani.gif', writer = 'imagemagick')
    
    plt.show()
    

    which gives this animation:

    enter image description here

    As you can see, the first 50 elements are enough to rebuild you image pretty well. The rest of the elements adds some noise and changes a little the background.