PointCloudRegistration.jl

PointCloudRegistration.jl is a package for performing rigid and nonrigid registration of point clouds, also known as superimposing or aligning.

Introductory example

To understand what point cloud registration is and why we need it, let's have a look at these two cute cats:

julia> using PointCloudRegistration
julia> source, target = PointCloudRegistration.Assets.load_cats();

They are both represented as weighted (indicated by marker size) point clouds. As it is common in the context of registration, we call one of them the source and the other the target. While the cats actually only differ in the position of their tails, the two point clouds are considerably more different. If you were to compare them point coordinate by point coordinate, you would pick up lots of differences that are not related to the tail position. Point cloud registration aims at resolving this issue.

We can inspect the numerical representation of the cats:

julia> source2-dimensional point cloud with 510 points of eltype Float64
 -2.05844   9.90266  117.956  127.528   …   -85.0363    -4.09962   24.9562
 -1.5976   34.2892   118.629   67.0558     -127.447   -142.378    108.29
and weights
 510-element reinterpret(Int64, ::Vector{UInt8})
 87  102  95  108  114  124  130  101  …  94  87  101  110  87  78  115  78
julia> target2-dimensional point cloud with 241 points of eltype Float64 -505.793 -564.474 -484.949 … -463.414 -649.245 -517.484 -2.14602 29.1641 -31.2827 -103.69 -33.2674 -49.8494 and weights 241-element reinterpret(Int64, ::Vector{UInt8}) 180 219 216 234 174 206 188 176 … 228 191 164 133 214 195 196

To perform registration, we can first find a rigid transformation that rotates and translates the source such that both point clouds sit on top of each other:

julia> rigid_transformation = rigid_registration(source, target)AffineMap([0.6324517738018479 -0.7745997378097261; 0.7745997378097262 0.6324517738018479], [-498.28618592635087, -1.5379611642502358])

We can apply rigid_transformation to source and obtain a new point cloud:

julia> source2 = rigid_transformation(source)2-dimensional point cloud with 510 points of eltype Float64
 -498.351    -518.584   -515.575  -469.573  …  -453.347  -390.593   -566.384
   -4.14284    27.8189   164.857   139.654     -148.011   -94.7609    86.2812
and weights
 510-element reinterpret(Int64, ::Vector{UInt8})
 87  102  95  108  114  124  130  101  …  94  87  101  110  87  78  115  78

Plotting both source2 and target shows that the two are aligned well:

Often, we are already done here. However, we might be interested in explaining more of the differences between the point clouds, for which we turn to non-rigid registration:

julia> nonrigid_displacement =  nonrigid_registration(
         source2,
         target,
         DistancePreserving(; max_edge_length = 45, sensitivity = 1.4, rel_deviation = 1e-4),
       )[ Info: before loop
2-dimensional displacement with 510 vectors of eltype Float64
  4.1192    4.58852   0.648691  1.32036  …   3.65235   3.67159   4.11854
 -1.47035  -1.46731  -0.968431  1.38896     -1.93874  -1.73408  -2.40291

As we can see, this requires a bit more domain knowledge and fine-tuning of parameters compared to rigid registration. Applying the result to source2 provides a new point cloud:

julia> source3 = nonrigid_displacement(source2)2-dimensional point cloud with 510 points of eltype Float64
 -494.231    -513.995   -514.926  -468.252  …  -449.694  -386.921   -562.265
   -5.61318    26.3516   163.889   141.043     -149.95    -96.4949    83.8783
and weights
 510-element reinterpret(Int64, ::Vector{UInt8})
 87  102  95  108  114  124  130  101  …  94  87  101  110  87  78  115  78

The plot shows the final result:

Plotting the nonrigid_displacement reveals the tail movement: