Finn’s experiment to delve into the realm of unsupervised learning to explore its potential in compressing image data. Through systematic experimentation with singular value decomposition (SVD), Finn aims to uncover the effectiveness of this technique in reducing the size of image files.
Author
Finn Ellingwood
Published
May 3, 2023
Unsupervised Learning with Linear Algebra
In this blog post, I try to experiment with some machine learning approaches to compress some data.
But first we need some data, so I implemented the code below to grab us some from the internet. And specifically, we’re gonna need some Data, Crusher, Troi Picard, La Forge, Worf, and Riker.
import PIL, urllibfrom matplotlib import pyplot as pltimport numpy as npnp.random.seed(1701) # for the NCC-1701def read_image(url):return np.array(PIL.Image.open(urllib.request.urlopen(url)))url ="https://static.wikia.nocookie.net/memoryalpha/images/2/23/Star_Trek_TNG_cast.jpg/revision/latest?cb=20200323081735&path-prefix=en"img = read_image(url)plt.imshow(img); plt.axis("off")plt.savefig('TNG.png', pad_inches=0)
# Set the figure size for the plotplt.figure(figsize=(15, 5))# Function to convert an image to grayscale as supplied from the blog docsdef to_greyscale(img):return1- np.dot(img[..., :3], [0.2989, 0.5870, 0.1140])# Convert the image to grayscalegrey_img = to_greyscale(img)# Plotting the original imageplt.subplot(121)plt.imshow(img)plt.axis("off")plt.title("original")# Plotting the grayscale imageplt.subplot(122)plt.imshow(grey_img, cmap="Greys")plt.axis("off")plt.title("greyscale")# Save the figure as an image fileplt.savefig('TNG.png', pad_inches=0)
Compression
As you can see, I converted the image to greyscale to help with the later process of compression.
For the sake of this exercise, it will be a lot easier to do with a bunch of single brightness values defining the difference from white to black than working with three separate color values.
Plus it gives it a cool retro vibe :)
Now let’s get into how I am going to compress this image. I am going to have Captain Picard explain:
Captain’s Log, Stardate 100944.83:
We have made a remarkable discovery regarding the compression of grayscale images. Singular Value Decomposition (SVD) has proven to be a valuable tool in this endeavor. Allow me to elucidate the process:
Step 1: Prepare the Image Prepare the grayscale image for compression. This involves converting it to a grayscale format, focusing solely on the shades of gray that shape its essence. The stellar code provided above has been implemented to do this for us.
Step 2: Engage SVD Engage the extraordinary power of Singular Value Decomposition. SVD dissects the image into three significant components: U, Sigma, and V. Each element holds crucial information about the image’s structure.
Step 3: Select the Singular Values Decisions must be made regarding the number of singular values to retain. This parameter, denoted as K, governs the balance between compression and image fidelity. Choose wisely, as the optimal K will define the outcome.
Step 4: Precise Approximation Utilize the chosen K to create an approximation of the image. By retaining only the most significant singular values and their corresponding components, we achieve a compressed representation of the image while minimizing information loss.
Step 5: Reconstruct the Image Reconstruct the compressed image by combining the retained singular values, their components, and the V matrix. Witness the image emerge, akin to the restoration of a cherished artifact.
Step 6: Assess the Results Evaluate the compression achieved, considering storage requirements and image quality. Balance is key, for a successful compression strikes a harmonious chord between compactness and faithful representation.
Through this application of Singular Value Decomposition, we unveil a new avenue for grayscale image compression. This technique empowers us to reduce data while preserving the image’s essential characteristics. A remarkable voyage indeed.
Captain Jean-Luc Picard, out.
import numpy as npimport warningsdef svd_reconstruct(img, num_sv=10, comp_factor=0, threshold=0):# Get the dimensions of the image m, n = img.shape data = np.array(img) # Convert the image to a numpy array# Calculate the number of singular values (num_sv) if a compression factor (comp_factor) is specifiedif comp_factor !=0: num_sv =int((comp_factor * n * m) / (n + m +1))# Check if num_sv is larger than the dimensions of the image and adjust if necessaryif num_sv > m or num_sv > n: warnings.warn("WARNING: num_sv does not fit dimensions of img") num_sv =min(n, m)# Perform singular value decomposition (SVD) on the image data U, sigma, V = np.linalg.svd(data)# Create a diagonal matrix D with singular values D = np.zeros_like(data, dtype=float) D[:min(data.shape), :min(data.shape)] = np.diag(sigma)# Determine the components for approximation based on whether a threshold is specified or notif threshold ==0: U_ = U[:, :num_sv] D_ = D[:num_sv, :num_sv] V_ = V[:num_sv, :]else:# Count the number of singular values larger than the threshold new_num_sv = np.count_nonzero(D > threshold)# Adjust new_num_sv if it exceeds the image dimensionsif new_num_sv > m or new_num_sv > n: new_num_sv =min(n, m)# Use new_num_sv if it is smaller than the specified components (num_sv)if new_num_sv < num_sv: num_sv = new_num_sv U_ = U[:, :num_sv] D_ = D[:num_sv, :num_sv] V_ = V[:num_sv, :]# Reconstruct the image using the selected components data_ = U_ @ D_ @ V_# Calculate the storage size and compression ratio storage = (m * num_sv + num_sv + num_sv * n) fraction = storage / (n * m)return data_, round(fraction *100, 2)
I implemented the code above using the examples provided in blog post documentation and help hours. I call the K constant “num_sv” as it represents the number of singular values to be retained and used in our compression factors. Otherwise, the comments are pretty self explanatory, where the svd_reconstruct function takes a grayscale image and performs singular value decomposition on it. It allows for compression of the image by retaining a specified number of singular values or by applying a threshold to select significant singular values, and returns the reconstructed image along with the storage size and compression ratio. Next let’s look at a quick comparison.
def compare_svd_reconstruct(img, num_sv=10, comp_factor=0, threshold=0):# Convert the input image to a numpy array A = np.array(img)# Reconstruct the image using singular value decomposition (SVD) A_, storage_percent = svd_reconstruct(A, num_sv, comp_factor=comp_factor, threshold=threshold)# Get the dimensions of the original image m, n = A.shape# Create a figure with two subplots for comparing the original and reconstructed images fig, axarr = plt.subplots(1, 2, figsize=(18, 9))# Display the original image in the first subplot axarr[0].imshow(A, cmap="Greys") axarr[0].axis("off") axarr[0].set(title="original greyscale image")# Display the reconstructed image in the second subplot axarr[1].imshow(A_, cmap="Greys") axarr[1].axis("off") img_title ="reconstructed image\n"+str(storage_percent) +"% storage" axarr[1].set(title=img_title)
compare_svd_reconstruct(to_greyscale(img), 50)
compare_svd_reconstruct(to_greyscale(img), 50)
We can see here that this number of components used to compress the image (50) is probably not enough. So let’s run some experiments to see what might be a good number of singular values for our compression.
def svd_experiment(img): rows =5 columns =3# Create a figure with subplots for displaying the results fig, axarr = plt.subplots(rows, columns, figsize=(20, 25))# Iterate over the rows and columns of the subplotsfor i inrange(rows):for j inrange(columns):# Determine the number of components (k) based on the current row and column k = (i *4+ j +1) *15# Reconstruct the image using the specified number of components A_, storage_percent = svd_reconstruct(img, k)# Create a title for the subplot indicating the number of components and storage percentage img_title =str(k) +" components\n"+str(storage_percent) +"% storage"# Display the reconstructed image in the current subplot axarr[i][j].imshow(A_, cmap="Greys") axarr[i][j].axis("off") axarr[i][j].set(title=img_title)# Adjust the spacing between subplots fig.tight_layout()
Now we’re getting somewhere. The first few tests clearly have too few SVs, but by the 2nd row, much of the image is already there. And by the 3rd or 4th row, the compression artifacts created by our method are much less distracting and the image is difficult to discern from the original without pixel peeping. This is particularly noticeable on the crew’s clothing, where the compression has a hard time doing smooth gradients. By around 225 components would be where I draw the line:
compare_svd_reconstruct(to_greyscale(img), 225)
Indeed, by utilizing singular value decomposition (SVD) for image compression, we can significantly reduce the size of the image while preserving its essential features. This method works well for compressing this image of TNG crew, but we may have to tweak it in the future for other uses. In my estimate, achieving a compression down to approximately 23% of the original size demonstrates the effectiveness of SVD-based compression in reducing storage requirements without sacrificing essential image quality.