Сообщество Engee

SVD - разложение для сжатия изображений

Автор
avatar-igarajaigaraja
Notebook

Singular value decomposition (SVD) как способ сжатия изображения

Матрица - как линейный оператор

Создадим набор точек в виде круга

In [ ]:
using LinearAlgebra
using Plots

circ_range = -1:0.025:1

# создаждим вектор векторов, содержащих координаты точек, входящих в круг 
circle = [[i,j] for i = circ_range for j = circ_range if i^2 + j^2 <= 1]

# выбираем точки с координатами [x, 0] и [0,y] для отображения базиса
x_zero_indxs = findall(x-> x[1] == 0 && x[2] >= 0,circle)
y_zero_indxs = findall(x-> x[2]== 0 && x[1] >= 0,circle)

# превращаем вектор векторов в матрицу 
circle = stack(circle)
2×5025 Matrix{Float64}:
 -1.0  -0.975  -0.975  -0.975  -0.975  …  0.975  0.975  0.975  0.975  1.0
  0.0  -0.2    -0.175  -0.15   -0.125     0.125  0.15   0.175  0.2    0.0
In [ ]:
function plot_circle(circle;xlm=[],ylm=[])
    scatter(circle[1,:],circle[2,:],aspect_ratio=:equal)
    scatter!(circle[1,x_zero_indxs],circle[2,x_zero_indxs],aspect_ratio=:equal)
    if (isempty(xlm) || isempty(ylm))
    scatter!(circle[1,y_zero_indxs],circle[2,y_zero_indxs], color=:yellow,aspect_ratio=:equal)
    else
    scatter!(circle[1,y_zero_indxs],circle[2,y_zero_indxs],
    color=:yellow, xlims=xlm, ylims=ylm, aspect_ratio=:equal)
    end
end
plot_circle (generic function with 2 methods)
In [ ]:
plot_circle(circle)

Обратим внимание на наши базисы. Сейчас базис x = a y -

Теперь умножим наш набор точек на матрицу . \text{И} \text{теперь} \text{можем} \text{заметить}, \text{что} \text{координаты} \text{нового} \text{базиса} x \text{в} \text{предыдущих} = . И вся окружность вытянулось по горизонтали

In [ ]:
M = [2 0;
     0 1]
circle_x_2 = M * circle
plot_circle(circle_x_2)
In [ ]:
# То же для y
M = [1 0;
     0 2]
circle_y_2 = M * circle
plot_circle(circle_y_2)
In [ ]:
# Обратите внимание на координаты базисов и матрицу M
M = [2 1;
     1 2]
circle_xy_2 = M * circle
plot_circle(circle_xy_2)
In [ ]:
# Создадим анимации
function step!(M)
    return (M * circle)
end

@gif for i=0:0.05:2
    plot_circle(step!([ i 0;
                        0 i]),
                xlm=[-2, 2],ylm=[-2, 2])
end
┌ Info: Saved animation to /tmp/jl_xkBIzyrer4.gif
└ @ Plots /home/ilya/.julia/packages/Plots/kLeqV/src/animation.jl:156
No description has been provided for this image
In [ ]:
# Такого рода движение называется отражением

@gif for i=0:0.05:2
    
    plot_circle(step!([ 3-i 0;
                        0 1-i]))
end
┌ Info: Saved animation to /tmp/jl_f2lm5GZ2hW.gif
└ @ Plots /home/ilya/.julia/packages/Plots/kLeqV/src/animation.jl:156
No description has been provided for this image
In [ ]:
@gif for i=0:0.1:2 * π
    plot_circle(step!([ cos(i) -sin(i);
                        sin(i)  cos(i)]), xlm=[-1,1], ylm=[-1,1])
end
┌ Info: Saved animation to /tmp/jl_zk5UtQtGHe.gif
└ @ Plots /home/ilya/.julia/packages/Plots/kLeqV/src/animation.jl:156
No description has been provided for this image
In [ ]:
M = [2 1; 1 2]
(U, Σ, V) = svd(M) # разложим матрицу M 

# используем foreach вместо map, так как нам не нужен вывод значений,
# а не возвращаемое значение display
foreach([U,Σ,V]) do x
    display(x)
end
2×2 Matrix{Float64}:
 -0.707107  -0.707107
 -0.707107   0.707107
2-element Vector{Float64}:
 2.9999999999999996
 1.0000000000000002
2×2 adjoint(::Matrix{Float64}) with eltype Float64:
 -0.707107  -0.707107
 -0.707107   0.707107
In [ ]:
# Матрица V - матрица поворота или отражения
plot_circle(V * circle)
In [ ]:
# diagm(Σ) - матрица масштабирования
plot_circle(diagm(Σ) * V * circle)
In [ ]:
# Матрица V - матрица поворота или отражения
plot_circle(U * diagm(Σ) * V * circle)
# получили тот же результат, что и просто от матрицы M
In [ ]:
using Images
using TestImages

Рассмотрим другой смысл использования SVD.
Изображение представляет собой прямоугольную матрицу (в нашем случае 3 прямоугольные):

In [ ]:
img = float.(testimage("mandrill"))
No description has been provided for this image
In [ ]:
channels = channelview(img) # представим нашу картину в виде цветовых каналов
3×512×512 reinterpret(reshape, Float32, ::Array{RGB{Float32},2}) with eltype Float32:
[:, :, 1] =
 0.643137  0.470588  0.388235  0.25098   …  0.47451   0.494118  0.0352941
 0.588235  0.490196  0.290196  0.309804     0.580392  0.662745  0.0431373
 0.278431  0.243137  0.121569  0.188235     0.607843  0.658824  0.0470588

[:, :, 2] =
 0.247059  0.529412  0.517647  0.588235  …  0.482353  0.458824  0.0392157
 0.223529  0.380392  0.462745  0.564706     0.611765  0.592157  0.0470588
 0.121569  0.129412  0.180392  0.223529     0.588235  0.592157  0.0431373

[:, :, 3] =
 0.294118   0.215686   0.235294  0.443137  …  0.486275  0.47451   0.0431373
 0.168627   0.137255   0.160784  0.384314     0.588235  0.533333  0.0588235
 0.0392157  0.0901961  0.141176  0.137255     0.545098  0.521569  0.0470588

;;; … 

[:, :, 510] =
 0.458824  0.478431  0.462745  0.294118  …  0.431373  0.286275  0.0196078
 0.466667  0.54902   0.364706  0.301961     0.286275  0.329412  0.0313726
 0.266667  0.384314  0.352941  0.180392     0.235294  0.266667  0.0196078

[:, :, 511] =
 0.552941  0.533333  0.341176  0.356863  …  0.352941  0.388235  0.00784314
 0.666667  0.623529  0.356863  0.282353     0.364706  0.270588  0.0196078
 0.396078  0.501961  0.301961  0.2          0.27451   0.337255  0.0

[:, :, 512] =
 0.701961  0.470588  0.376471  0.243137  …  0.317647  0.313726  0.0156863
 0.737255  0.541176  0.313726  0.305882     0.313726  0.247059  0.0196078
 0.462745  0.290196  0.192157  0.184314     0.235294  0.278431  0.00784314
In [ ]:
# Посмотрим, как картинка раскладывается на RGB
# Заметно, что по каналу Red (левая нижняя картинка)
# нос и глаза очень красные (белый ~ max red)

chnl_views = map(channel -> colorview(Gray,
eachslice(channels;dims=1)[channel]),[1,2,3])
mosaicview(img,chnl_views...; nrow=2, npad=10)
No description has been provided for this image

Создадим функцию, которая будет отбирать только первые k сингулярных чисел.
Таким образом, нам нужно будет хранить не картинку, а 2 матрицы и 1 диагональ. И чтобы восстановить картину нам надо будет их просто перемножить.

$ M = U \cdot \Sigma \cdot V$

In [ ]:
function rank_approx(F::SVD, k)
    U, Σ, V = F
    M = U[:, 1:k] * Diagonal(Σ[1:k]) * V[:, 1:k]'

    println("size of U, Σ, V non-zero elements = $(sizeof(U[:, 1:k]) + sizeof(Σ[1:k]) + sizeof(V[:, 1:k]'))")
    clamp01!(M)
end
rank_approx (generic function with 1 method)

Посмотрим, как изменилось изображение и сколько весят SVD-элементы

In [ ]:
println("size of original image = $(sizeof(img))")
svdfactors = svd.(eachslice(channels; dims=1))
imgs = map((10, 50, 100)) do k
    colorview(RGB, rank_approx.(svdfactors, k)...)
end

mosaicview(img, imgs...; nrow=2, npad=10)
size of original image = 3145728
size of U, Σ, V non-zero elements = 20528
size of U, Σ, V non-zero elements = 20528
size of U, Σ, V non-zero elements = 20528
size of U, Σ, V non-zero elements = 102608
size of U, Σ, V non-zero elements = 102608
size of U, Σ, V non-zero elements = 102608
size of U, Σ, V non-zero elements = 205208
size of U, Σ, V non-zero elements = 205208
size of U, Σ, V non-zero elements = 205208
No description has been provided for this image