|
1 | 1 | # ---
|
2 |
| -# cover: assets/color_separations_svd.png |
3 |
| -# title: Color separations and the SVD |
| 2 | +# cover: assets/color_separations_svd.gif |
| 3 | +# title: Image Compression using SVD |
4 | 4 | # ---
|
5 | 5 |
|
6 | 6 | # This demonstration shows how to work with color channels to explore image compression
|
7 | 7 | # using the Singular Value Decomposition (SVD).
|
8 | 8 |
|
9 |
| -using Images, TestImages, LinearAlgebra |
| 9 | +using Images, TestImages, MosaicViews |
| 10 | +using LinearAlgebra |
10 | 11 |
|
11 |
| -img = testimage("mandrill") |
12 |
| -channels = channelview(float.(img)) |
| 12 | +img = float.(testimage("mandrill")) |
| 13 | +channels = channelview(img) |
13 | 14 |
|
14 | 15 | function rank_approx(F::SVD, k)
|
15 | 16 | U, S, V = F
|
16 | 17 | M = U[:, 1:k] * Diagonal(S[1:k]) * V[:, 1:k]'
|
17 |
| - M = min.(max.(M, 0.0), 1.) |
| 18 | + clamp01!(M) |
18 | 19 | end
|
19 |
| -#md nothing #hide |
| 20 | +nothing #hide #md |
20 | 21 |
|
21 |
| -#- |
22 |
| -## after julia v1.1: svd.(eachslice(channels; dims=1)) |
| 22 | +# For each channel, we do SVD decomposition, and then reconstruct the channel using only the K |
| 23 | +# largest singular values. |
| 24 | + |
| 25 | +# The image is compressed because for each channel we only need to save two small matrices and one |
| 26 | +# vector -- truncated part of `(U, S, V)`. For example, if the original image is gray image of size |
| 27 | +# `(512, 512)`, and we rebuild the image with $50$ singular values, then we only need to save |
| 28 | +# $2 \times 512 \times 50 + 50$ numbers to rebuild the image, while original image has |
| 29 | +# $512 \times 512$ numbers. Hence this gives us a compression ratio $19.55\%$ if we don't consider |
| 30 | +# the storage type. |
| 31 | + |
| 32 | +## after julia v1.1: |
| 33 | +## svdfactors = svd.(eachslice(channels; dims=1)) |
23 | 34 | svdfactors = (svd(channels[1,:,:]), svd(channels[2,:,:]), svd(channels[3,:,:]))
|
24 | 35 | imgs = map((10, 50, 100)) do k
|
25 |
| - colorview(RGB, |
26 |
| - rank_approx(svdfactors[1], k), |
27 |
| - rank_approx(svdfactors[2], k), |
28 |
| - rank_approx(svdfactors[3], k)) |
| 36 | + colorview(RGB, rank_approx.(svdfactors, k)...) |
29 | 37 | end
|
30 | 38 |
|
31 |
| -vcat([img imgs[1]], [imgs[2] imgs[3]]) |
| 39 | +mosaicview(img, imgs...; nrow=1, npad=10) |
| 40 | + |
| 41 | +# From left to right: original image, reconstructed images using 10, 50, 100 largest singular values. |
| 42 | +# We can see that $50$ largest singular values are capable of rebuilding a pretty good image. |
| 43 | + |
| 44 | +# --- save covers --- #src |
| 45 | +using ImageMagick #src |
| 46 | +imgs = map(10:5:50) do k #src |
| 47 | + colorview(RGB, rank_approx.(svdfactors, k)...) #src |
| 48 | +end #src |
| 49 | +ImageMagick.save("assets/color_separations_svd.gif", cat(imgs...; dims=3); fps=2) #src |
32 | 50 |
|
33 |
| -cover = hcat(imgs[1]) #src |
34 |
| -save("assets/color_separations_svd.png", cover) #src |
|
0 commit comments