In [1]:
using DelimitedFiles
using Gadfly, Cairo, Colors
using LinearAlgebra
using Statistics
set_default_plot_size(15cm,13cm)
color_palette = Scale.color_discrete().f(7)[[1, 4, 6]]
themes = [Theme(default_color=color, panel_fill="white", grid_color="lightgray",
background_color=parse(Colorant, "white")) for color in color_palette];
load raw point cloud data. tranpose it so that the points are in the columns of B
. not necessary, but, subtract off mean to center the point cloud.
In [2]:
B = readdlm("point_cloud.txt")
Out[2]:
In [3]:
p = plot(x=B[1, :], y=B[2, :], Geom.point, themes[1],
Guide.title("a point set, B"), themes[1],
Coord.cartesian(fixed=true),
)
Out[3]:
In [4]:
draw(PNG("ptset_B.png", 5inch, 4inch, dpi=300), p)
rotate each point in B
about the origin by an angle θ
via multiplying by a rotation matrix R_known
. then add some Gaussian noise to each point. this forms the point cloud A
.
In [5]:
rotation_matrix2d(θ::Float64) = [cos(θ) -sin(θ); sin(θ) cos(θ)]
Out[5]:
In [6]:
θ = π * 0.8
R_known = rotation_matrix2d(θ)
Out[6]:
In [7]:
ϵ = 0.01 # noise
A = R_known * B .+ ϵ * randn(size(B)...)
p = plot(x=A[1, :], y=A[2, :], Geom.point, themes[3],
Guide.title("point set, A"), themes[3],
Coord.cartesian(fixed=true),
)
Out[7]:
In [8]:
draw(PNG("ptset_A.png", 5inch, 4inch, dpi=300), p)
In [9]:
p = plot(layer(x=A[1, :], y=A[2, :], Geom.point, themes[3]),
layer(x=B[1, :], y=B[2, :], Geom.point, themes[1]),
Guide.title("orthogonal Procrustes"), themes[1],
Coord.cartesian(fixed=true),
Guide.manual_color_key("point set", ["A", "B"], [color_palette[3], color_palette[1]])
)
Out[9]:
In [10]:
draw(PNG("before_alignment.png", 5inch, 4inch, dpi=300), p)
highlight that we know the correspondence between the points in A
and the points in B
by connecting corresponding points with a gray line.
In [11]:
p = plot(layer(x=A[1, :], y=A[2, :], Geom.point, themes[3]),
layer(x=B[1, :], y=B[2, :], Geom.point, themes[1]),
[layer(x=[A[1, i], B[1, i]], y=[A[2, i], B[2, i]],
Geom.line, Theme(default_color=colorant"gray")) for i = 1:size(A)[2]]...,
Guide.title("orthogonal Procrustes"), themes[1],
Coord.cartesian(fixed=true),
Guide.manual_color_key("point set", ["A", "B"], [color_palette[3], color_palette[1]])
)
Out[11]:
In [12]:
draw(PNG("correspondence.png", 5inch, 4inch, dpi=300), p)
compute the singular value decomposition of $AB^T=U\Sigma V^T$.
In [13]:
F = svd(A * B')
Out[13]:
we can recover the orthogonal matrix R
such that, when applied to A
, the points in RA
are close to the corresponding points in B
, as $R = VU^T$.
In [14]:
R = F.V * F.U'
Out[14]:
In [15]:
isapprox(R, rotation_matrix2d(-θ), rtol=0.01)
Out[15]:
In [16]:
A_transformed = R * A
Out[16]:
voila
In [17]:
p = plot(layer(x=A_transformed[1, :], y=A_transformed[2, :], Geom.point, themes[3]),
layer(x=B[1, :], y=B[2, :], Geom.point, themes[1]),
Guide.title("orthogonal Procrustes"), themes[1],
Coord.cartesian(fixed=true),
Guide.manual_color_key("point set", ["RA", "B"], [color_palette[3], color_palette[1]])
)
Out[17]:
In [19]:
draw(PNG("after_alignment.png", 5inch, 4inch, dpi=300), p)