Open3d学习计划——高级篇 4(多视角配准配准)
Open3d学习计划——高级篇 4(多视角配准配准)
多视角配准是在全局空间中对齐多个几何形状的过程。比较有代表性的是,输入是一组几何形状
{
P
i
}
\lbrace P_i\rbrace
{Pi}(可以是点云或者RGBD图像)。输出是一组刚性变换
{
T
i
}
\lbrace T_i \rbrace
{Ti},是的变换后的点云
{
T
i
P
i
}
\lbrace T_i P_i \rbrace
{TiPi} 可以在全局空间中对齐。
Open3d通过姿态图估计提供了多视角配准的接口。具体的技术细节请参考[Choi2015].
输入
教程代码的第一部分是从三个文件中读取三个点云数据,这三个点云将被降采样和可视化,可以看出他们三个是不对齐的。
def load_point_clouds(voxel_size=0.0):
pcds = []
for i in range(3):
pcd = o3d.io.read_point_cloud("../../TestData/ICP/cloud_bin_%d.pcd" % i)
pcd_down = pcd.voxel_down_sample(voxel_size=voxel_size)
pcds.append(pcd_down)
return pcds
voxel_size = 0.02
pcds_down = load_point_clouds(voxel_size)
o3d.visualization.draw_geometries(pcds_down)
姿态图
姿态图有两个关键的基础:节点和边。节点是与姿态矩阵
T
i
T_i
Ti关联的一组几何体
P
i
P_i
Pi,通过该矩阵能够将
P
i
P_i
Pi转换到全局空间。集和
{
T
i
}
\lbrace T_i \rbrace
{Ti}是一组待优化的未知的变量。PoseGraph.nodes
是PoseGraphNode
的列表。我们设
P
0
P_0
P0的空间是全局空间。因此
T
0
T_0
T0是单位矩阵。其他的姿态矩阵通过累加相邻节点之间的变换来初始化。相邻节点通常都有着大规模的重叠并且能够通过Point-to-plane ICP来配准。
姿态图的边连接着两个重叠的节点(几何形状)。每个边都包含着能够将源几何 P i P_i Pi 和目标几何 P j P_j Pj对齐的变换矩阵 T i , j T_{i,j} Ti,j。本教程使用Point-to-plane ICP来估计变换矩阵。在更复杂的情况中,成对的配准问题一般是通过全局配准来解决的。
[Choi2015] 观察到,成对的配准容易出错。甚至错误的匹配会大于正确的匹配,因此,他们将姿态图的边分为两类。Odometry edges连接着邻域节点,使用局部配准的方式比如ICP就可以对齐他们。Loop closure edges连接着非邻域的节点。该对齐是通过不太可靠的全局配准找到的。在Open3d中,这两类边缘通过PoseGraphEdge
初始化程序中的uncertain
参数来确定。
除了旋转矩阵
T
i
T_i
Ti以外,用户也可以去设置每一条边的信息矩阵
A
i
A_i
Ai。如果是通过get_information_matrix_from_point_clouds
设置的信息矩阵
A
i
A_i
Ai,那么姿态图的边的损失将以 line process weight 近似于两组节点对应点集的RMSE。有关详细细节请参考[Choi2015] 和
the Redwood registration benchmark。
下面的脚本创造了具有三个节点和三个边的姿态图。这些边里,两个是odometry edges(uncertain = False
),一个是loop closure edge(uncertain = True
)。
def pairwise_registration(source, target):
print("Apply point-to-plane ICP")
icp_coarse = o3d.registration.registration_icp(
source, target, max_correspondence_distance_coarse, np.identity(4),
o3d.registration.TransformationEstimationPointToPlane())
icp_fine = o3d.registration.registration_icp(
source, target, max_correspondence_distance_fine,
icp_coarse.transformation,
o3d.registration.TransformationEstimationPointToPlane())
transformation_icp = icp_fine.transformation
information_icp = o3d.registration.get_information_matrix_from_point_clouds(
source, target, max_correspondence_distance_fine,
icp_fine.transformation)
return transformation_icp, information_icp
def full_registration(pcds, max_correspondence_distance_coarse,
max_correspondence_distance_fine):
pose_graph = o3d.registration.PoseGraph()
odometry = np.identity(4)
pose_graph.nodes.append(o3d.registration.PoseGraphNode(odometry))
n_pcds = len(pcds)
for source_id in range(n_pcds):
for target_id in range(source_id + 1, n_pcds):
transformation_icp, information_icp = pairwise_registration(
pcds[source_id], pcds[target_id])
print("Build o3d.registration.PoseGraph")
if target_id == source_id + 1: # odometry case
odometry = np.dot(transformation_icp, odometry)
pose_graph.nodes.append(
o3d.registration.PoseGraphNode(np.linalg.inv(odometry)))
pose_graph.edges.append(
o3d.registration.PoseGraphEdge(source_id,
target_id,
transformation_icp,
information_icp,
uncertain=False))
else: # loop closure case
pose_graph.edges.append(
o3d.registration.PoseGraphEdge(source_id,
target_id,
transformation_icp,
information_icp,
uncertain=True))
return pose_graph
print("Full registration ...")
max_correspondence_distance_coarse = voxel_size * 15
max_correspondence_distance_fine = voxel_size * 1.5
with o3d.utility.VerbosityContextManager(o3d.utility.VerbosityLevel.Debug) as cm:
pose_graph = full_registration(pcds_down,
max_correspondence_distance_coarse,
max_correspondence_distance_fine)
Full registration …
Apply point-to-plane ICP
Build o3d.registration.PoseGraph
Apply point-to-plane ICP
Build o3d.registration.PoseGraph
Apply point-to-plane ICP
Build o3d.registration.PoseGraph
Open3d使用函数global_optimization
进行姿态图估计,可以选择两种类型的优化算法,分别是GlobalOptimizationGaussNewton
和GlobalOptimizationLevenbergMarquardt
。比较推荐后一种的原因是因为它具有比较好的收敛性。GlobalOptimizationConvergenceCriteria
类可以用来设置最大迭代次数和别的优化参数。
GlobalOptimizationOption
定于了两个参数。max_correspondence_distance
定义了对应阈值。edge_prune_threshold
是修剪异常边缘的阈值。reference_node
是被视为全局空间的节点ID。
print("Optimizing PoseGraph ...")
option = o3d.registration.GlobalOptimizationOption(
max_correspondence_distance=max_correspondence_distance_fine,
edge_prune_threshold=0.25,
reference_node=0)
with o3d.utility.VerbosityContextManager(o3d.utility.VerbosityLevel.Debug) as cm:
o3d.registration.global_optimization(
pose_graph, o3d.registration.GlobalOptimizationLevenbergMarquardt(),
o3d.registration.GlobalOptimizationConvergenceCriteria(), option)
Optimizing PoseGraph …
全局优化在姿态图上执行两次。 第一遍将考虑所有边缘的情况优化原始姿态图的姿态,并尽量区分不确定边缘之间的错误对齐。这些错误对齐将会产生小的 line process weights,他们将会在第一遍被剔除。第二遍将会在没有这些边的情况下运行,产生更紧密地全局对齐效果。在这个例子中,所有的边都将被考虑为真实的匹配,所以第二遍将会立即终止。
可视化操作
使用```draw_geometries``函数可视化变换点云。
print("Transform points and display")
for point_id in range(len(pcds_down)):
print(pose_graph.nodes[point_id].pose)
pcds_down[point_id].transform(pose_graph.nodes[point_id].pose)
o3d.visualization.draw_geometries(pcds_down)
Transform points and display
[[ 1.00000000e+00 -2.50509994e-19 0.00000000e+00 0.00000000e+00]
[-3.35636805e-20 1.00000000e+00 1.08420217e-19 -8.67361738e-19]
[-1.08420217e-19 -1.08420217e-19 1.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00]]
[[ 0.8401689 -0.14645453 0.52217554 0.34785474]
[ 0.00617659 0.96536804 0.2608187 -0.39427149]
[-0.54228965 -0.2159065 0.81197679 1.7300472 ]
[ 0. 0. 0. 1. ]]
[[ 0.96271237 -0.07178412 0.2608293 0.3765243 ]
[-0.00196124 0.96227508 0.27207136 -0.48956598]
[-0.27051994 -0.26243801 0.92625334 1.29770817]
[ 0. 0. 0. 1. ]]
得到合并的点云
PointCloud
是可以很方便的使用+
来合并两组点云成为一个整体。合并之后,将会使用voxel_down_sample
进行重新采样。建议在合并之后对点云进行后处理,因为这样可以减少重复的点后者较为密集的点。
pcds = load_point_clouds(voxel_size)
pcd_combined = o3d.geometry.PointCloud()
for point_id in range(len(pcds)):
pcds[point_id].transform(pose_graph.nodes[point_id].pose)
pcd_combined += pcds[point_id]
pcd_combined_down = pcd_combined.voxel_down_sample(voxel_size=voxel_size)
o3d.io.write_point_cloud("multiway_registration.pcd", pcd_combined_down)
o3d.visualization.draw_geometries([pcd_combined_down])
Note:
尽管这个教程展示的点云的多视角配准,但是相同的处理步骤可以应用于RGBD图像,请参看 Make fragments 示例。
关于翻译大家有更好的意见欢迎评论一起学习!!!
欢迎大家关注“点云PCL”公众号,进入群聊一起学习。
上一篇: Hive语句执行优化-简化UDF执行过程