Skip to main content

Image compression by rank

A greyscale picture is a matrix, so the truncated SVD compresses it. Count what k layers cost to store, watch how fast a photograph’s singular values fall, choose k by the share of the energy kept, and see why noise refuses to compress.
M03.6.2L2 Intermediate25 min
In review (what this means)

The maths and code on this page are checked by automated tests on every change, but an independent reader has not signed it off yet. Spot a mistake or a confusing step? Tell us.

Before this lesson: M03.6.1 The best rank-k approximation

What you will be able to do
  • Count the numbers needed to store k layers of an m by n picture, k(m + n + 1), and find the largest k that still saves space.
  • Choose k from the share of the energy kept, and convert that share into a relative error.
  • Compress a photograph with numpy.linalg.svd and with sklearn.decomposition.TruncatedSVD, and compare the two.
  • Explain why sharp slanted edges, fine texture and noise need many layers.

The question

A greyscale photograph 427 pixels high and 640 wide is a table of 273,280 brightness values. Keep only its first 50 singular layers and you store a fifth as many numbers. How much of the picture survives, how should you choose how many layers to keep, and why do some pictures refuse to be squeezed at all?

From the last lesson

1. What is the squared Frobenius error of the truncation \(\mat{A}_k\)?

Show answer

The sum of the squares of the dropped singular values, \({\sigma_{k+1}^2 + \cdots + \sigma_r^2}\).

2. Is there a matrix of rank \(k\) that is closer to \(\mat{A}\) than \(\mat{A}_k\)?

Show answer

No. By the Eckart-Young theorem no matrix of rank at most \(k\) is closer, in the Frobenius or the spectral norm.

Intuition

A greyscale picture is a grid of pixels, each with a brightness from 0 (black) to 1 (white). Read the grid row by row and it is a matrix. This module draws its own small one: a house, a hill and the sun on a \(48 \times 48\) grid.

By the last lesson, the best rank-\(k\) copy of that matrix is its first \(k\) singular layers. So rebuild the picture from 1 layer, then 2, then 4, and so on.

Six square greyscale pictures in two rows. 1 layer: soft stripes, 97 numbers, 95.66 per cent of the energy. 2 layers: a dark blob where the roof will be, 194 numbers, 98.07 per cent. 4 layers: a blurred house with a door, 388 numbers, 99.42 per cent. 8 layers: the house, the hill and a faint sun, 776 numbers, 99.77 per cent. 16 layers: almost the original, 1,552 numbers, 99.97 per cent. The original, all 48 layers: a grey house with a dark triangular roof, a window and a door, a white sun at the top right and a grainy hill, 2,304 pixels, 100 per cent.
M03.6.2-F1

The module’s picture rebuilt from its first \(k\) layers. Each layer costs 97 numbers. Eight layers cost a third of the 2,304 pixels and already show every object.

Broad shapes come first, because they carry most of the brightness. Edges and texture come later. The rest of the lesson puts numbers on both sides of that trade: what each layer costs, and what it buys.

Build it up

Step 1. A photograph as a matrix. scikit-learn ships a colour photograph of a Chinese temple, load_sample_image('china.jpg'), as an array of 427 rows, 640 columns and 3 colour channels. Averaging the three channels and dividing by 255 gives a greyscale matrix \(\mat{A}\) of size \(427 \times 640\) with entries between 0 and 1. (We keep the photograph itself off this page, because the site draws all its own pictures. The code below works with its numbers.)

Step 2. What a layer costs. A layer \({\sigma_i\,\vect{u}_i\vect{v}_i\T}\) fills a whole \(m \times n\) grid, but to store it you need only \(\vect{u}_i\) (\(m\) numbers), \(\vect{v}_i\) (\(n\) numbers) and \(\sigma_i\) (one number).

A diagram. A single orange cell labelled sigma i, 1 number; a tall orange column of cells labelled u i, m numbers; and a long orange row of cells labelled v i transpose, n numbers. An equals sign leads to a large vermillion box labelled one layer, m by n entries. A box underneath reads: k layers cost k times (m plus n plus 1) numbers; the picture costs m n. For a 427 by 640 photo: 1,068 per layer against 273,280, so up to 255 layers save space.
M03.6.2-F2

One layer is built from \(m + n + 1\) stored numbers. The picture itself needs \(mn\).

Step 3. How fast the singular values fall. For the photograph, \(\sigma_1 \approx 327\), but \(\sigma_{50}\) is already down to about 4, and the last ones are below 0.1. Since the energy is the sum of the \(\sigma_i^2\), a handful of layers holds almost all of it.

Two plots for the greyscale photograph. Left, singular values on a log scale against the layer number from 1 to 427: the curve starts at sigma 1 equal to 327, falls steeply to sigma 50 equal to 4.3 (orange up to there), then declines slowly through 1 and drops below 0.1 near the end (purple, labelled dropped). Right, the share of the energy kept against the number of layers kept, from 90 to 100 per cent: k equal to 1 gives 91.5 per cent, k equal to 10 gives 97.4 per cent, k equal to 50 gives 98.9 per cent, just under a dashed line at 99 per cent, and the curve flattens towards 100 per cent.
M03.6.2-F3

The singular values of the photograph fall by a factor of about 75 in the first 50 layers. The share of the energy kept rises steeply, then crawls.

The formal statement

Storage. Keeping \(k\) layers of an \(m \times n\) matrix stores \({k(m + n + 1)}\) numbers in place of \(mn\), a fraction

\[ \frac{k\,(m + n + 1)}{mn} . \]

It saves space only while \({k(m + n + 1)} < mn\), that is while \(k < \dfrac{mn}{m + n + 1}\). For the photograph that bound is \(273{,}280/1{,}068 \approx 255.9\): up to 255 layers save space. For the \(48 \times 48\) picture it is \(2{,}304/97 \approx 23.8\).

Energy kept and error. By Result 2 of the last lesson, the squared error of \(\mat{A}_k\) is the energy that was dropped. Divide by the whole energy \(\norm{\mat{A}}_F^2\):

\[ \begin{aligned} \frac{\norm{\mat{A} - \mat{A}_k}_F^2}{\norm{\mat{A}}_F^2} &= \frac{\sigma_{k+1}^2 + \cdots + \sigma_r^2}{\sigma_1^2 + \cdots + \sigma_r^2} \\ &= 1 - \cparam{\text{share kept}} . \end{aligned} \]

So the relative error \(\norm{\mat{A} - \mat{A}_k}_F / \norm{\mat{A}}_F\) is the square root of the share dropped. Keeping 99% of the energy means a relative error of \(\sqrt{0.01} = 10\%\).

Choosing \(k\). Pick a share \(s\), say 99%, and take the smallest \(k\) whose layers hold at least that share of the energy. For the photograph that is a little over 50 layers.

Why must the share be set so high? The first layer is large because a picture is mostly of one overall brightness. A flat grey picture whose every pixel has the average brightness \(b\) of the photograph would be the rank-one matrix \({b\,\vect{1}\vect{1}\T}\), and it alone would hold 74% of the photograph’s energy. So a share of 90% says almost nothing about detail; the information is in the last few per cent.

Worked example

A square frame. The \(4 \times 4\) picture with ones round the edge and zeros inside is

\[ \mat{A} = \begin{bmatrix} 1 & 1 & 1 & 1 \\ 1 & 0 & 0 & 1 \\ 1 & 0 & 0 & 1 \\ 1 & 1 & 1 & 1 \end{bmatrix} . \]

Its rank. Every row is \(\vect{p}\T = (1, 1, 1, 1)\) or \(\vect{q}\T = (1, 0, 0, 1)\), so the rank is 2. Its energy is the number of ones, 12.

Its first layer. \(\mat{A}\) is symmetric, so we look for an eigenvector of the form \(\vect{w} = (1, t, t, 1)\). Multiplying,

\[ \mat{A}\vect{w} = (2 + 2t,\ 2,\ 2,\ 2 + 2t) . \]

This is \(\lambda\vect{w}\) when \({2 + 2t} = \lambda\) and \(2 = \lambda t\). Substituting \(t = 2/\lambda\) into the first gives \(\lambda^2 - 2\lambda - 4 = 0\), so \(\lambda = 1 \pm \sqrt5\). The larger, \(\sigma_1 = 1 + \sqrt5 \approx 3.236\), has \(t = {2/(1 + \sqrt5)} = {(\sqrt5 - 1)/2} \approx 0.618\). The other, \(1 - \sqrt5\), is negative, so \(\sigma_2 = \sqrt5 - 1 \approx 1.236\) (M03.5.4). Check: \(\sigma_1^2 + \sigma_2^2 = 12\), the energy.

With \(\vect{u}_1 = \vect{v}_1 = \vect{w}/\norm{\vect{w}}\) and \(\norm{\vect{w}}^2 = 2 + 2t^2 = 5 - \sqrt5\), the first layer is \({\sigma_1\,\vect{w}\vect{w}\T / (5 - \sqrt5)}\). Its entries are \(1.17\) in the corners, \(0.72\) along the edges and \(0.45\) in the middle.

Three 4 by 4 grids of grey pixels with their numbers written on them. Left, the frame, rank 2: ones round the edge, black zeros in the middle. Middle, layer 1 with sigma 1 equal to 1 plus root 5: 1.17 in the corners, 0.72 along the edges and 0.45 in the centre, a blurred frame. Right, what is missing, with sigma 2 equal to root 5 minus 1: minus 0.17 in the corners, 0.28 along the edges, minus 0.45 in the centre. Underneath: layer 1 alone is 9 numbers instead of 16 and 87.3 per cent of the energy; both layers cost 18 numbers, more than the 16 of the picture.
M03.6.2-F4

The frame, its first layer and what that layer misses. One layer blurs the frame into a soft square. The second layer, which costs as much again, puts the hole back.

The bookkeeping. One layer stores \({4 + 4 + 1} = 9\) numbers in place of 16 and keeps \({\sigma_1^2/12} = {(6 + 2\sqrt5)/12} \approx 87.3\%\) of the energy, a relative error of \(\sqrt{0.127} \approx 36\%\). Two layers rebuild the frame exactly but cost 18 numbers: here the bound \(16/9 \approx 1.8\) says that only one layer saves space.

Code

The photograph, from scratch with NumPy: its singular values, the storage and the energy kept.

import numpy as np
from sklearn.datasets import load_sample_image

A = load_sample_image("china.jpg").mean(axis=2) / 255      # greyscale, entries from 0 to 1
m, n = A.shape
U, s, Vt = np.linalg.svd(A, full_matrices=False)
share = np.cumsum(s ** 2) / np.sum(s ** 2)

print("shape:", A.shape, "  sigma_1:", round(s[0], 1), "  sigma_50:", round(s[49], 2))
for k in (1, 10, 50, 100):
    stored = k * (m + n + 1)
    print(f"k = {k:3d}: {stored:7,d} numbers ({stored / (m * n):5.1%}),"
          f" energy kept {share[k - 1]:.2%}, relative error {np.sqrt(1 - share[k - 1]):.1%}")
print("largest k that saves space:", (m * n) // (m + n + 1))
print("smallest k keeping 99% of the energy:", int(np.argmax(share >= 0.99)) + 1)
print("a flat picture of the mean brightness holds", round(m * n * A.mean() ** 2 / np.sum(A ** 2), 2))
shape: (427, 640)   sigma_1: 327.2   sigma_50: 4.34
k =   1:   1,068 numbers ( 0.4%), energy kept 91.49%, relative error 29.2%
k =  10:  10,680 numbers ( 3.9%), energy kept 97.43%, relative error 16.0%
k =  50:  53,400 numbers (19.5%), energy kept 98.94%, relative error 10.3%
k = 100: 106,800 numbers (39.1%), energy kept 99.46%, relative error 7.4%
largest k that saves space: 255
smallest k keeping 99% of the energy: 54
a flat picture of the mean brightness holds 0.74

The relative error of 10% at \(k = 50\) comes from the formula, and the last assertion checks it against the rebuilt matrix directly.

scikit-learn’s TruncatedSVD computes only the first \(k\) layers, by a randomised method. Its fit_transform returns \(\mat{U}_k\mat{\Sigma}_k\) (one row per image row) and inverse_transform multiplies back by \(\mat{V}_k\T\).

from sklearn.decomposition import TruncatedSVD

svd50 = TruncatedSVD(n_components=50, random_state=0)
scores = svd50.fit_transform(A)                 # 427 by 50: U_k times Sigma_k
rebuilt = svd50.inverse_transform(scores)       # back to 427 by 640
print("largest relative gap in sigma_1..sigma_50:", round(np.max(np.abs(svd50.singular_values_ - s[:50]) / s[:50]), 4))
print("relative error, randomised:", round(np.linalg.norm(A - rebuilt) / np.linalg.norm(A), 4),
      "  exact:", round(np.sqrt(1 - share[49]), 4))
largest relative gap in sigma_1..sigma_50: 0.0113
relative error, randomised: 0.1029   exact: 0.1028

The randomised singular values are within a few per cent of the exact ones, and its rebuilt picture is a hair worse than \(\mat{A}_{50}\): by Eckart-Young, no rank-50 matrix can be better.

Explore

M03.6-X1

Rank slider

Drag the slider, or press and drag on the bars, to keep \(k\) layers. Find the \(k\) at which the roof stops looking like a staircase of stripes, then press “What is missing” to see where the error lives. Note the \(k\) at which storage passes the 2,304 pixels.

A static picture in place of the interactive: the module's picture rebuilt from 6 layers beside a bar chart of its 48 singular values on a log scale, the first 6 bars orange and the rest purple, with the line: stored 6 times 97 equals 582 numbers against 2,304; energy kept 99.64 per cent.

Pitfalls

Two rows of three panels. Top row, marked with a cross and titled Noise: no shortcut: a 48 by 48 picture of random grey speckle, the same picture rebuilt from 8 layers, still mostly speckle, with error 25 per cent, and its singular values on a log scale: one large first value, then a nearly flat run falling slowly, orange for the first 8 and purple after. Bottom row, marked with a tick and titled A picture: shapes repeat: the house picture, its 8-layer rebuild with error 5 per cent, and singular values that fall steeply over the first 8 layers. A note reads: same brightness, same spread of greys; only the picture has a few strong layers.
M03.6.2-F5

Wrong beside right. Noise with the same average and spread of greys as the picture keeps an error of 25% after 8 layers; the picture is down to 5%.

“Every image can be compressed by low rank.”

Only images whose rows repeat each other’s patterns. In random noise no row predicts another, so after the first layer, which holds the average brightness, the singular values fall slowly and evenly, with no small group of strong layers. For the noise of the figure \(\sigma_2 \approx 3.05\) and \(\sigma_9 \approx 2.28\), so each layer buys little more than the next. Sharp slanted edges are hard too: a single diagonal line of white pixels on black is the identity matrix, whose singular values are all 1, so no layer matters more than another.

“99% of the energy kept means the picture is 99% right.”

The share is of squared sizes, and most of it is plain brightness: a flat grey picture would already hold 74% of the photograph’s energy. The honest number is the relative error, the square root of the share dropped. At 99% kept it is 10%.

“Keeping more layers always saves space, just less of it.”

Past \(mn/(m + n + 1)\) layers the factors cost more than the pixels. For the \(4 \times 4\) frame that happens at the second layer, for the \(48 \times 48\) picture at the 24th, for the photograph at the 256th.

Check yourself

Recall

1. How many numbers does it take to store \(k\) layers of an \(m \times n\) picture?

Show answer

\(k(m + n + 1)\): for each layer, the \(m\) entries of \(\vect{u}_i\), the \(n\) entries of \(\vect{v}_i\) and the number \(\sigma_i\).

2. Up to which \(k\) does truncation save space?

Show answer

While \(k(m + n + 1) < mn\), that is for \(k < mn/(m + n + 1)\).

3. How do you turn the share of the energy kept into a relative error?

Show answer

The relative error \(\norm{\mat{A} - \mat{A}_k}_F/\norm{\mat{A}}_F\) is the square root of \(1 - \text{share kept}\).

4. Why does the first layer of a photograph hold so much of the energy?

Show answer

Because most of a picture’s energy is its overall brightness, and a picture of constant brightness is a rank-one matrix. The first layer captures that and little detail.

5. What does TruncatedSVD(n_components=k).fit_transform(A) return?

Show answer

The \(m \times k\) matrix \(\mat{U}_k\mat{\Sigma}_k\), computed by a randomised method; inverse_transform multiplies it by \(\mat{V}_k\T\) to give (close to) \(\mat{A}_k\).

Exercises

1. ★ A greyscale picture is \(1{,}000 \times 2{,}000\). How many numbers do 100 layers cost, what fraction of the picture is that, and what is the largest \(k\) that saves space?

Show solution

One layer costs \({1{,}000 + 2{,}000 + 1} = 3{,}001\) numbers, so 100 layers cost \(300{,}100\). The picture has \(2{,}000{,}000\) pixels, so the fraction is about \(15\%\). Space is saved while \(3{,}001\,k < 2{,}000{,}000\), that is \(k < 666.4\): up to 666 layers.

2. ★★ A picture of a single white diagonal line on black is the \(n \times n\) identity matrix. What share of the energy does \(\mat{A}_k\) keep, and what is the relative error? Is \(\mat{A}_k\) unique?

Show solution

\(\mat{I} = \mat{I}\,\mat{I}\,\mat{I}\T\) is an SVD, so all \(n\) singular values are 1 and the energy is \(n\). Any \(k\) layers keep \(k/n\) of it, and the relative error is \(\sqrt{1 - k/n}\): for \(n = 48\) and \(k = 8\), about \(91\%\). Because \(\sigma_k = \sigma_{k+1}\), there is a tie, and \(\mat{A}_k\) is not unique: any \(k\) orthonormal directions do equally well.

3. ★★★ A picture has brightness \(b_{ij} = f_i + g_j\): a shading down the rows plus a shading across the columns. Show that its rank is at most 2, and find two layers that build it.

Show solution

Let \(\vect{f} = (f_1, \dots, f_m)\), \(\vect{g} = (g_1, \dots, g_n)\), and let \(\vect{1}\) be a vector of ones of the right length. Entry \((i, j)\) of \({\vect{f}\,\vect{1}\T}\) is \(f_i\), and entry \((i, j)\) of \({\vect{1}\,\vect{g}\T}\) is \(g_j\). So

\[ \mat{B} = \vect{f}\,\vect{1}\T + \vect{1}\,\vect{g}\T , \]

a sum of two rank-one matrices. Its column space lies in the span of \(\vect{f}\) and \(\vect{1}\), so its rank is at most 2. These two layers are not perpendicular in general, so they are not the SVD layers, but they show that two singular values are enough: \(\sigma_3 = \sigma_4 = \cdots = 0\). The sky of the module’s picture, a gentle shading from top to bottom, is this kind of picture.

Summary card

Keep a few layers, store a few numbers, lose a little energy.

The anchor figure again: the module's picture of a house rebuilt from 1, 2, 4, 8 and 16 layers beside the original, with the numbers stored and the share of the energy kept under each.
M03.6.2-F1

Broad shapes first, edges later.

  • Storage. \(k\) layers cost \(k(m + n + 1)\) numbers against \(mn\).
  • Break-even. It saves space only for \(k < mn/(m + n + 1)\).
  • Energy kept. \({(\sigma_1^2 + \cdots + \sigma_k^2)}/{(\sigma_1^2 + \cdots + \sigma_r^2)}\).
  • Relative error. \(\norm{\mat{A} - \mat{A}_k}_F/\norm{\mat{A}}_F = \sqrt{1 - \text{share kept}}\).

Sources and next steps

Sources. Each was opened and checked against this lesson on 15 November 2026.

  • Marc Deisenroth, Aldo Faisal and Cheng Soon Ong, Mathematics for Machine Learning, Section 4.6. The Stonehenge example: a \(1{,}432 \times 1{,}910\) greyscale image, whose rank-5 approximation needs only \({5 \cdot (1{,}432 + 1{,}910 + 1)} = 16{,}715\) numbers, “just above 0.6% of the original”: the storage count of this lesson.
  • David Austin, Understanding Linear Algebra, Using singular value decompositions, subsection “Image compressing and denoising”: an image stored as an array of brightness values is rebuilt “using a small number of singular values and vectors”.
  • scikit-learn, load_sample_image: china.jpg is returned as an array of shape \((427, 640, 3)\) and type uint8; and TruncatedSVD: it does not centre the data, its default solver is the randomised algorithm with n_iter=5, and components_ holds the right singular vectors.
  • NumPy, numpy.linalg.svd: full_matrices=False returns the thin factors, and the singular values come in descending order.

Next. M03.6.3 PCA from the covariance matrix turns from pictures to clouds of data points, and finds the directions in which they spread most.

This lesson unlocks

M03.6.3 PCA from the covariance matrix

Your progress is saved in this browser only. No account, no tracking.
Back to top