StableRecon / backend_utils.py
Stable-X's picture
feat: Add backend for refinement
d1dbe71
raw
history blame
7.04 kB
import numpy as np
import open3d as o3d
def improved_multiway_registration(pcds, voxel_size=0.05, max_correspondence_distance_coarse=None, max_correspondence_distance_fine=None, overlap=3, quadratic_overlap=True, use_colored_icp=True):
if max_correspondence_distance_coarse is None:
max_correspondence_distance_coarse = voxel_size * 15
if max_correspondence_distance_fine is None:
max_correspondence_distance_fine = voxel_size * 1.5
def preprocess_point_cloud(pcd, voxel_size):
pcd_down = pcd.voxel_down_sample(voxel_size)
pcd_down.estimate_normals(
o3d.geometry.KDTreeSearchParamHybrid(radius=voxel_size * 2, max_nn=30))
# Apply statistical outlier removal
cl, ind = pcd_down.remove_statistical_outlier(nb_neighbors=20, std_ratio=2.0)
pcd_down = pcd_down.select_by_index(ind)
return pcd_down
def pairwise_registration(source, target, use_colored_icp, voxel_size, max_correspondence_distance_coarse, max_correspondence_distance_fine):
current_transformation = np.identity(4) # Start with identity matrix
if use_colored_icp:
print("Apply colored point cloud registration")
voxel_radius = [5*voxel_size, 3*voxel_size, voxel_size]
max_iter = [60, 35, 20]
for scale in range(3):
iter = max_iter[scale]
radius = voxel_radius[scale]
source_down = source.voxel_down_sample(radius)
target_down = target.voxel_down_sample(radius)
source_down.estimate_normals(
o3d.geometry.KDTreeSearchParamHybrid(radius=radius * 2, max_nn=30))
target_down.estimate_normals(
o3d.geometry.KDTreeSearchParamHybrid(radius=radius * 2, max_nn=30))
try:
result_icp = o3d.pipelines.registration.registration_colored_icp(
source_down, target_down, radius, current_transformation,
o3d.pipelines.registration.TransformationEstimationForColoredICP(),
o3d.pipelines.registration.ICPConvergenceCriteria(relative_fitness=1e-6,
relative_rmse=1e-6,
max_iteration=iter))
current_transformation = result_icp.transformation
except RuntimeError as e:
print(f"Colored ICP failed at scale {scale}: {str(e)}")
print("Keeping the previous transformation")
# We keep the previous transformation, no need to reassign
transformation_icp = current_transformation
else:
print("Apply point-to-plane ICP")
try:
icp_coarse = o3d.pipelines.registration.registration_icp(
source, target, max_correspondence_distance_coarse, current_transformation,
o3d.pipelines.registration.TransformationEstimationPointToPlane())
current_transformation = icp_coarse.transformation
icp_fine = o3d.pipelines.registration.registration_icp(
source, target, max_correspondence_distance_fine,
current_transformation,
o3d.pipelines.registration.TransformationEstimationPointToPlane())
transformation_icp = icp_fine.transformation
except RuntimeError as e:
print(f"Point-to-plane ICP failed: {str(e)}")
print("Keeping the best available transformation")
transformation_icp = current_transformation
try:
information_icp = o3d.pipelines.registration.get_information_matrix_from_point_clouds(
source, target, max_correspondence_distance_fine,
transformation_icp)
except RuntimeError as e:
print(f"Failed to compute information matrix: {str(e)}")
print("Using identity information matrix")
information_icp = np.identity(6)
return transformation_icp, information_icp
def full_registration(pcds_down):
pose_graph = o3d.pipelines.registration.PoseGraph()
odometry = np.identity(4)
pose_graph.nodes.append(o3d.pipelines.registration.PoseGraphNode(odometry))
n_pcds = len(pcds_down)
pairs = []
for i in range(n_pcds - 1):
for j in range(i + 1, min(i + overlap + 1, n_pcds)):
pairs.append((i, j))
if quadratic_overlap:
q = 2**(j-i)
if q > overlap and i + q < n_pcds:
pairs.append((i, i + q))
for source_id, target_id in pairs:
transformation_icp, information_icp = pairwise_registration(
pcds_down[source_id], pcds_down[target_id], use_colored_icp,
voxel_size, max_correspondence_distance_coarse, max_correspondence_distance_fine)
print(f"Build PoseGraph: {source_id} -> {target_id}")
if target_id == source_id + 1:
odometry = np.dot(transformation_icp, odometry)
pose_graph.nodes.append(
o3d.pipelines.registration.PoseGraphNode(
np.linalg.inv(odometry)))
pose_graph.edges.append(
o3d.pipelines.registration.PoseGraphEdge(source_id,
target_id,
transformation_icp,
information_icp,
uncertain=False))
return pose_graph
# Preprocess point clouds
print("Preprocessing point clouds...")
pcds_down = [preprocess_point_cloud(pcd, voxel_size) for pcd in pcds]
print("Full registration ...")
pose_graph = full_registration(pcds_down)
print("Optimizing PoseGraph ...")
option = o3d.pipelines.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.pipelines.registration.global_optimization(
pose_graph,
o3d.pipelines.registration.GlobalOptimizationLevenbergMarquardt(),
o3d.pipelines.registration.GlobalOptimizationConvergenceCriteria(),
option)
print("Transform points and combine")
pcd_combined = o3d.geometry.PointCloud()
for point_id in range(len(pcds)):
print(pose_graph.nodes[point_id].pose)
pcds[point_id].transform(pose_graph.nodes[point_id].pose)
pcd_combined += pcds[point_id]
return pcd_combined, pose_graph