To be able to edit code and run cells, you need to run the notebook yourself. Where would you like to run the notebook?

In the cloud (experimental)

Binder is a free, open source service that runs scientific notebooks in the cloud! It will take a while, usually 2-7 minutes to get a session.

On your computer

(Recommended if you want to store your changes.)

  1. Copy the notebook URL:
  2. Run Pluto

    (Also see: How to install Julia and Pluto)

  3. Paste URL in the Open box

Frontmatter

If you are publishing this notebook on the web, you can set the parameters below to provide HTML metadata. This is useful for search engines and social media.

Author 1
👀 Reading hidden code
begin
import Pkg
Pkg.activate(mktempdir())
# we will work with the package Images.jl, and to display images on the screen,
# we also need ImageIO and ImageMagick
Pkg.add(["Images", "ImageIO", "ImageMagick"])
# now that it is installed, we can import it inside out notebook:
using Images
end
❔
  Activating new project at `/tmp/jl_NBHhej`
    Updating registry at `~/.julia/registries/General.toml`
   Resolving package versions...
    Updating `/tmp/jl_NBHhej/Project.toml`
  [82e4d734] + ImageIO v0.6.9
  [6218d12a] + ImageMagick v1.4.2
  [916415d5] + Images v0.26.2
    Updating `/tmp/jl_NBHhej/Manifest.toml`
  [621f4979] + AbstractFFTs v1.5.0
  [79e6a3ab] + Adapt v3.7.2
  [66dad0bd] + AliasTables v1.1.3
  [ec485272] + ArnoldiMethod v0.4.0
  [4fba245c] + ArrayInterface v7.5.1
  [13072b0f] + AxisAlgorithms v1.1.0
  [39de3d68] + AxisArrays v0.4.7
  [62783981] + BitTwiddlingConvenienceFunctions v0.1.6
  [fa961155] + CEnum v0.5.0
  [2a0fbf3d] + CPUSummary v0.2.6
  [aafaddc9] + CatIndices v0.2.2
  [d360d2e6] + ChainRulesCore v1.25.1
  [9e997f8a] + ChangesOfVariables v0.1.10
  [fb6a15b2] + CloseOpenIntervals v0.1.13
  [aaaa29a8] + Clustering v0.15.8
  [35d6a980] + ColorSchemes v3.29.0
  [3da002f7] + ColorTypes v0.12.1
  [c3611d14] + ColorVectorSpace v0.11.0
  [5ae59095] + Colors v0.13.1
  [bbf7d656] + CommonSubexpressions v0.3.1
  [34da2185] + Compat v4.16.0
  [ed09eef8] + ComputationalResources v0.3.2
  [187b0558] + ConstructionBase v1.5.8
  [150eb455] + CoordinateTransformations v0.6.3
  [adafc99b] + CpuId v0.3.1
  [dc8bdbbb] + CustomUnitRanges v1.0.2
  [9a962f9c] + DataAPI v1.16.0
  [864edb3b] + DataStructures v0.18.22
  [163ba53b] + DiffResults v1.1.0
  [b552c78f] + DiffRules v1.15.1
  [b4f34e82] + Distances v0.10.12
  [ffbed154] + DocStringExtensions v0.9.5
  [4f61f5a4] + FFTViews v0.3.2
  [7a1cc6ca] + FFTW v1.9.0
  [5789e2e9] + FileIO v1.17.0
  [53c48c17] + FixedPointNumbers v0.8.5
  [f6369f11] + ForwardDiff v1.0.1
  [a2bd30eb] + Graphics v1.1.3
  [86223c79] + Graphs v1.12.1
  [2c695a8d] + HistogramThresholding v0.3.1
  [3e5b6fbb] + HostCPUFeatures v0.1.17
  [615f187c] + IfElse v0.1.1
  [2803e5a7] + ImageAxes v0.6.12
  [c817782e] + ImageBase v0.1.7
  [cbc4b850] + ImageBinarization v0.3.1
  [f332f351] + ImageContrastAdjustment v0.3.12
  [a09fc81d] + ImageCore v0.10.5
  [89d5987c] + ImageCorners v0.1.3
  [51556ac3] + ImageDistances v0.2.17
  [6a3955dd] + ImageFiltering v0.7.10
  [82e4d734] + ImageIO v0.6.9
  [6218d12a] + ImageMagick v1.4.2
  [bc367c6b] + ImageMetadata v0.9.10
  [787d08f9] + ImageMorphology v0.4.6
  [2996bd0c] + ImageQualityIndexes v0.3.7
  [80713f31] + ImageSegmentation v1.9.0
  [4e3cecfd] + ImageShow v0.3.8
  [02fcd773] + ImageTransformations v0.10.2
  [916415d5] + Images v0.26.2
  [9b13fd28] + IndirectArrays v1.0.0
  [d25df0c9] + Inflate v0.1.5
  [1d092043] + IntegralArrays v0.1.6
  [a98d9a8b] + Interpolations v0.15.1
  [8197267c] + IntervalSets v0.7.11
  [3587e190] + InverseFunctions v0.1.17
  [92d709cd] + IrrationalConstants v0.2.4
  [c8e1da08] + IterTools v1.4.0
  [033835bb] + JLD2 v0.5.12
  [692b3bcd] + JLLWrappers v1.7.0
  [b835a17e] + JpegTurbo v0.1.6
  [10f19ff3] + LayoutPointers v0.1.17
  [8cdb02fc] + LazyModules v0.3.1
  [2ab3a3ac] + LogExpFunctions v0.3.28
  [bdcacae8] + LoopVectorization v0.12.172
  [1914dd2f] + MacroTools v0.5.16
  [d125e4d3] + ManualMemory v0.1.8
  [dbb5928d] + MappedArrays v0.4.2
  [626554b9] + MetaGraphs v0.8.0
  [e1d29d7a] + Missings v1.2.0
  [e94cdb99] + MosaicViews v0.3.4
  [77ba4419] + NaNMath v1.0.3
  [b8a86587] + NearestNeighbors v0.4.21
  [f09324ee] + Netpbm v1.1.1
  [6fe1bfb0] + OffsetArrays v1.17.0
  [52e1d378] + OpenEXR v0.3.3
  [bac558e1] + OrderedCollections v1.8.1
  [f57f5aa1] + PNGFiles v0.4.4
  [5432bcbf] + PaddedViews v0.5.12
  [d96e819e] + Parameters v0.12.3
  [eebad327] + PkgVersion v0.3.3
  [1d0040c9] + PolyesterWeave v0.2.2
  [f27b6e38] + Polynomials v4.0.19
  [aea7be01] + PrecompileTools v1.2.1
  [21216c6a] + Preferences v1.4.3
  [92933f4c] + ProgressMeter v1.10.4
  [43287f4e] + PtrArrays v1.3.0
  [4b34888f] + QOI v1.0.1
  [94ee1d12] + Quaternions v0.7.6
  [b3c3ace0] + RangeArrays v0.3.2
  [c84ed2f1] + Ratios v0.4.5
  [c1ae055f] + RealDot v0.1.0
  [3cdcf5f2] + RecipesBase v1.3.4
  [189a3867] + Reexport v1.2.2
  [dee08c22] + RegionTrees v0.3.2
  [ae029012] + Requires v1.3.1
  [6038ab10] + Rotations v1.7.1
  [fdea26ae] + SIMD v3.7.1
  [94e857df] + SIMDTypes v0.1.0
  [476501e8] + SLEEFPirates v0.6.43
  [efcf1570] + Setfield v1.1.2
  [699a6c99] + SimpleTraits v0.9.4
  [47aef6b3] + SimpleWeightedGraphs v1.5.0
  [45858cf5] + Sixel v0.1.3
  [a2af1166] + SortingAlgorithms v1.2.1
  [276daf66] + SpecialFunctions v2.5.1
  [cae243ae] + StackViews v0.1.2
  [aedffcd0] + Static v0.8.9
  [0d7ed370] + StaticArrayInterface v1.6.0
  [90137ffa] + StaticArrays v1.9.13
  [1e83bf80] + StaticArraysCore v1.4.3
  [82ae8749] + StatsAPI v1.7.1
  [2913bbd2] + StatsBase v0.34.4
  [62fd8b95] + TensorCore v0.1.1
  [8290d209] + ThreadingUtilities v0.5.5
  [731e570b] + TiffImages v0.11.4
  [06e1c1a7] + TiledIteration v0.5.0
  [3bb67fe8] + TranscodingStreams v0.11.3
  [3a884ed6] + UnPack v1.0.2
  [3d5dd08c] + VectorizationBase v0.21.71
  [e3aaa7dc] + WebP v0.1.3
  [efce3f68] + WoodburyMatrices v1.0.0
  [f5851436] + FFTW_jll v3.3.11+0
  [61579ee1] + Ghostscript_jll v9.55.0+4
  [59f7168a] + Giflib_jll v5.2.3+0
  [c73af94c] + ImageMagick_jll v7.1.1048+0
  [905a6f67] + Imath_jll v3.1.11+0
  [1d5cc7b8] + IntelOpenMP_jll v2025.0.4+0
  [aacddb02] + JpegTurbo_jll v3.1.1+0
  [88015f11] + LERC_jll v3.0.0+1
  [7e76a0d4] + Libglvnd_jll v1.7.1+1
  [89763e89] + Libtiff_jll v4.5.1+1
  [d3a379c0] + LittleCMS_jll v2.16.0+0
  [856f044c] + MKL_jll v2025.0.1+1
  [18a262bb] + OpenEXR_jll v3.2.4+0
  [643b3616] + OpenJpeg_jll v2.5.4+0
  [efe28fd5] + OpenSpecFun_jll v0.5.6+0
  [ffd25f8a] + XZ_jll v5.8.1+0
  [4f6342f7] + Xorg_libX11_jll v1.8.12+0
  [0c0b7dd1] + Xorg_libXau_jll v1.0.13+0
  [a3789734] + Xorg_libXdmcp_jll v1.1.6+0
  [1082639a] + Xorg_libXext_jll v1.3.7+0
  [c7cfdc94] + Xorg_libxcb_jll v1.17.1+0
  [c5fb5394] + Xorg_xtrans_jll v1.6.0+0
  [3161d3a3] + Zstd_jll v1.5.7+1
  [b53b4c65] + libpng_jll v1.6.49+0
  [075b6546] + libsixel_jll v1.10.5+0
  [c5f90fcd] + libwebp_jll v1.4.0+0
  [1317d2d5] + oneTBB_jll v2022.0.0+0
  [0dad84c5] + ArgTools
  [56f22d72] + Artifacts
  [2a0f44e3] + Base64
  [ade2ca70] + Dates
  [8ba89e20] + Distributed
  [f43a241f] + Downloads
  [7b1f6079] + FileWatching
  [9fa8497b] + Future
  [b77e0a4c] + InteractiveUtils
  [4af54fe1] + LazyArtifacts
  [b27032c2] + LibCURL
  [76f85450] + LibGit2
  [8f399da3] + Libdl
  [37e2e46d] + LinearAlgebra
  [56ddb016] + Logging
  [d6f4376e] + Markdown
  [a63ad114] + Mmap
  [ca575930] + NetworkOptions
  [44cfe95a] + Pkg
  [de0858da] + Printf
  [3fa0cd96] + REPL
  [9a3f8284] + Random
  [ea8e919c] + SHA
  [9e88b42a] + Serialization
  [1a1011a3] + SharedArrays
  [6462fe0b] + Sockets
  [2f01184e] + SparseArrays
  [10745b16] + Statistics
  [4607b0f0] + SuiteSparse
  [fa267f1f] + TOML
  [a4e569a6] + Tar
  [8dfed614] + Test
  [cf7118a7] + UUIDs
  [4ec0a83e] + Unicode
  [e66e0078] + CompilerSupportLibraries_jll
  [deac9b47] + LibCURL_jll
  [29816b5a] + LibSSH2_jll
  [c8ffd9c3] + MbedTLS_jll
  [14a3606d] + MozillaCACerts_jll
  [4536629a] + OpenBLAS_jll
  [05823500] + OpenLibm_jll
  [83775a58] + Zlib_jll
  [8e850b90] + libblastrampoline_jll
  [8e850ede] + nghttp2_jll
  [3f19e933] + p7zip_jll
93.5 s

Singular Value Decomposition

Lorem ipsum SVD est.

👀 Reading hidden code
3.4 ms

Step 1: upload your favorite image:

👀 Reading hidden code
5.8 ms
Enable 📸
👀 Reading hidden code
7.8 ms
missing
raw_camera_data
👀 Reading hidden code
8.7 μs
Error message

MethodError: no method matching getindex(::Missing, ::String)

Stack trace

Here is what happened, the most recent locations are first:

  1. process_raw_camera_data(raw_camera_data::Missing)
    	# So to get the red values for each pixel, we take every 4th value, starting at 	# the 1st:	reds_flat = UInt8.(raw_camera_data["data"][1:4:end])	greens_flat = UInt8.(raw_camera_data["data"][2:4:end])	blues_flat = UInt8.(raw_camera_data["data"][3:4:end])
  2. Show more...
C'est la vie !
img = process_raw_camera_data(raw_camera_data)
👀 Reading hidden code
---

👀 Reading hidden code
79.2 μs
Error message

Another cell defining img contains errors.

color .- img
👀 Reading hidden code
---

The camera image can be used as a variable inside Julia! Let's have a look at its type:

👀 Reading hidden code
28.3 ms
Error message

Another cell defining img contains errors.

Everything is going to be okay!
typeof(img)
👀 Reading hidden code
---
Error message

Another cell defining img contains errors.

md"It's a 2D array, and its elements are of the type _$(eltype(img))_"
👀 Reading hidden code
---

Pixels

To get a single pixel from the image, we just need to get a value from the 2D array. This is done with 2D indexing:

👀 Reading hidden code
23.5 ms
Error message

Another cell defining img contains errors.

firstpixel = img[1,1]
👀 Reading hidden code
---
Error message

Another cell defining img contains errors.

typeof(firstpixel)
👀 Reading hidden code
---

To get its values, we use the functions red, green, blue. These are part of the Images.jl package.

👀 Reading hidden code
304 μs
Error message

Another cell defining img contains errors.

red(firstpixel), green(firstpixel), blue(firstpixel)
👀 Reading hidden code
---
Error message

Another cell defining img contains errors.

red.(img)
👀 Reading hidden code
---

You can use img_data like any other Julia 2D array! For example, here is the top left corner of your image:

👀 Reading hidden code
235 μs
Error message

Another cell defining img contains errors.

topleft = let
# the first coordinate is vertical, the second is horizontal (it's a matrix!)
half_height = size(img)[1] ÷ 2
half_width = size(img)[2] ÷ 2
img[1:half_height, 1:half_width]
end
👀 Reading hidden code
---

Step 2: running the SVD

The Julia standard library package LinearAlgebra contains a method to compute the SVD.

👀 Reading hidden code
302 μs
using LinearAlgebra
👀 Reading hidden code
315 μs
Error message

Another cell defining img contains errors.

bw = Gray.(img)
👀 Reading hidden code
---
Error message

Another cell defining img contains errors.

📚 = svd(bw);
👀 Reading hidden code
---

Let's look at the result.

👀 Reading hidden code
213 μs
Error message

Another cell defining img contains errors.

📚
👀 Reading hidden code
---

Let's verify the identity

A=UΣV

👀 Reading hidden code
1.9 ms
Error message

Another cell defining img contains errors.

bw_reconstructed = 📚.U * Diagonal(📚.S) * 📚.V'
👀 Reading hidden code
---

Are they equal?

👀 Reading hidden code
282 μs
Error message

Another cell defining img contains errors.

bw == bw_reconstructed
👀 Reading hidden code
---

It looks like they are not equal - how come?

Since we are using a computer, the decomposition and multiplication both introduce some numerical errors. So instead of checking whether the reconstructed matrix is equal to the original, we can check how close they are to each other.

👀 Reading hidden code
409 μs

One way to quantify the distance between two matrices is to look at the point-wise difference. If the sum of all differences is close to 0, the matrices are almost equal.

👀 Reading hidden code
323 μs
Error message

UndefVarError: img_data_reconstructed not defined

Stack trace

Here is what happened, the most recent locations are first:

  1. p1_dist = sum(abs.(img_data_reconstructed - img_data))
Probably not your fault!
p1_dist = sum(abs.(img_data_reconstructed - img_data))
👀 Reading hidden code
---

There are other ways to compare two matrices, such methods are called matrix norms.

👀 Reading hidden code
308 μs

The 👀-norm

👀 Reading hidden code
186 μs

Another popular matrix norm is the 👀-norm: you turn both matrices into a picture, and use your 👀 to see how close they are:

👀 Reading hidden code
311 μs
Error message

UndefVarError: BWImage not defined

Stack trace

Here is what happened, the most recent locations are first:

  1. [BWImage(img_data), BWImage(img_data_reconstructed)]
[BWImage(img_data), BWImage(img_data_reconstructed)]
👀 Reading hidden code
---

How similar are these images?

👀 Reading hidden code
247 ms
missing
👀_dist
👀 Reading hidden code
12.7 μs

In some applications, like image compression, this is the most imporant norm.

👀 Reading hidden code
306 μs

Step 3: compression

👀 Reading hidden code
219 μs
Error message

Another cell defining img contains errors.

Oh no! 🙀
@bind keep HTML("<input type='range' max='$(length(📚.S))' value='10'>")
👀 Reading hidden code
---
Error message

Another cell defining img contains errors.

md"Showing the **first $(keep) singular pairs**."
👀 Reading hidden code
---
Error message

Another cell defining img contains errors.

Gray.(
📚.U[:,1:keep] *
Diagonal(📚.S[1:keep]) *
📚.V'[1:keep,:]
)
👀 Reading hidden code
---
Error message

Another cell defining img contains errors.

[Gray.(
📚.U[:,1:keep] *
Diagonal(📚.S[1:keep]) *
📚.V'[1:keep,:]
) for keep in 0:20]
👀 Reading hidden code
---

👀 Reading hidden code
65.2 μs

Store fewer bytes

👀 Reading hidden code
177 μs
#compressed_size(keep), uncompressed_size()
👀 Reading hidden code
8.2 μs
uncompressed_size (generic function with 1 method)
function uncompressed_size()
num_el = length(img_data)
return num_el * 8 ÷ 8
end
👀 Reading hidden code
459 μs
compressed_size (generic function with 1 method)
function compressed_size(keep)
num_el = (
length(📚.U[:,1:keep]) +
length(📚.S[1:keep]) +
length(📚.V'[1:keep,:])
)
return num_el * 16 ÷ 8
end
👀 Reading hidden code
772 μs
#BWImage(Float16.(F.U)[:,1:keep] * Diagonal(Float16.(F.S[1:keep])) * Float16.(F.V)'[1:keep,:])
👀 Reading hidden code
8.9 μs

JPEG works in a similar way

👀 Reading hidden code
182 μs

Individual pairs

👀 Reading hidden code
170 μs
Error message

Another cell defining img contains errors.

@bind pair_index HTML("<input type='range' min='1' max='$(length(📚.S))' value='10'>")
👀 Reading hidden code
---
Error message

Another cell defining img and pair_index contains errors.

You got this!
Gray.(normalize_mat((
📚.U[:,pair_index:pair_index] *
Diagonal(📚.S[pair_index:pair_index]) *
📚.V'[pair_index:pair_index,:]
), Inf))
👀 Reading hidden code
---
normalize_mat (generic function with 2 methods)
normalize_mat(A, p=2) = A ./ norm(A, p)
👀 Reading hidden code
655 μs

Going further

More stuff to learn about SVD

To keep things simple (and dependency-free), this notebook only works with downscaled black-and-white images that you pick using the button. For color, larger images, or images from your disk, you should look into the Images.jl package!

👀 Reading hidden code
518 μs

Appendix

👀 Reading hidden code
204 μs
Enable 📸
camera_input()
👀 Reading hidden code
19.1 μs
camera_input (generic function with 1 method)
function camera_input(;maxsize=200, default_url="https://i.imgur.com/VGPeJ6s.jpg")
"""
<span class="pl-image">
<style>

.pl-image video {
max-width: 250px;
}
.pl-image prompt {
max-width: 250px;
}

.pl-image video.takepicture {
animation: pictureflash 200ms linear;
}

@keyframes pictureflash {
0% {
filter: grayscale(1.0) contrast(2.0);
}

100% {
filter: grayscale(0.0) contrast(1.0);
}
}
</style>

<div id="video-container" title="Click to take a picture">
<video playsinline autoplay></video>
<div id="prompt">Enable 📸</div>
</div>

<script>
// based on https://github.com/fonsp/printi-static (by the same author)

const span = this.currentScript.parentElement
const video = span.querySelector("video")

const maxsize = $(maxsize)

const send_source = (source, src_width, src_height) => {
const scale = Math.min(1.0, maxsize / src_width, maxsize / src_height)

const width = Math.floor(src_width * scale)
const height = Math.floor(src_height * scale)

const canvas = html`<canvas width=\${width} height=\${height}>`
const ctx = canvas.getContext("2d")
ctx.drawImage(source, 0, 0, width, height)

span.value = {
width: width,
height: height,
data: ctx.getImageData(0, 0, width, height).data,
}
span.dispatchEvent(new CustomEvent("input"))
}


navigator.mediaDevices.getUserMedia({
audio: false,
video: {
facingMode: "environment",
},
}).then(function(stream) {

window.stream = stream
video.srcObject = stream
window.cameraConnected = true
video.controls = false
video.play()
video.controls = false

}).catch(function(error) {
console.log(error)
});

span.querySelector("#video-container").onclick = function() {
const cl = video.classList
cl.remove("takepicture")
void video.offsetHeight
cl.add("takepicture")
video.play()
video.controls = false
console.log(video)
send_source(video, video.videoWidth, video.videoHeight)
};


const img = html`<img crossOrigin="anonymous">`
img.onload = () => {
send_source(img, img.width, img.height)
}
img.src = "$(default_url)"


</script>
</span>
""" |> HTML
end
👀 Reading hidden code
1.4 ms
process_raw_camera_data (generic function with 1 method)
function process_raw_camera_data(raw_camera_data)
# the raw image data is a long byte array, we need to transform it into something
# more "Julian" - something with more _structure_.
# The encoding of the raw byte stream is:
# every 4 bytes is a single pixel
# every pixel has 4 values: Red, Green, Blue, Alpha
# (we ignore alpha for this notebook)
# So to get the red values for each pixel, we take every 4th value, starting at
# the 1st:
reds_flat = UInt8.(raw_camera_data["data"][1:4:end])
greens_flat = UInt8.(raw_camera_data["data"][2:4:end])
blues_flat = UInt8.(raw_camera_data["data"][3:4:end])
# but these are still 1-dimensional arrays, nicknamed 'flat' arrays
# We will 'reshape' this into 2D arrays:
width = raw_camera_data["width"]
height = raw_camera_data["height"]
# shuffle and flip to get it in the right shape
reds = reshape(reds_flat, (width, height))' / 255.0
greens = reshape(greens_flat, (width, height))' / 255.0
blues = reshape(blues_flat, (width, height))' / 255.0
# we have our 2D array for each color
# Let's create a single 2D array, where each value contains the R, G and B value of
# that pixel
RGB.(reds, greens, blues)
end
👀 Reading hidden code
1.3 ms