Iterative Closest Point (ICP) implementation on python

Learn iterative closest point (icp) implementation on python with practical examples, diagrams, and best practices. Covers python, opencv, nearest-neighbor development techniques with visual explan...

Implementing Iterative Closest Point (ICP) in Python

Hero image for Iterative Closest Point (ICP) implementation on python

Learn how to implement the Iterative Closest Point (ICP) algorithm in Python for 3D point cloud registration, covering core concepts, alignment steps, and practical code examples.

The Iterative Closest Point (ICP) algorithm is a widely used method to register two point clouds, meaning to find the optimal rigid transformation (rotation and translation) that aligns one point cloud (source) to another (target). This article will guide you through the fundamental principles of ICP and provide a Python implementation, demonstrating how to align 3D data effectively. We'll cover the core steps: finding correspondences, estimating transformation, and iteratively refining the alignment.

Understanding the ICP Algorithm

ICP operates on the principle of iteratively minimizing the distance between corresponding points in two datasets. It assumes that one point cloud (the source) needs to be transformed to align with another (the target). The algorithm proceeds in a loop, refining the transformation at each step until a convergence criterion is met. This criterion is typically a small change in the transformation parameters or a minimal error between the aligned point clouds.

flowchart TD
    A[Start]
    B{Initialize Transformation (Identity)}
    C[Find Closest Points (Correspondences)]
    D[Estimate Transformation (Rotation & Translation)]
    E{Check Convergence?}
    F[Apply Transformation to Source Cloud]
    G[End]

    A --> B
    B --> C
    C --> D
    D --> E
    E -->|No| F
    F --> C
    E -->|Yes| G

Flowchart of the Iterative Closest Point (ICP) algorithm

Key Steps of ICP Implementation

Implementing ICP involves several critical components. We'll break down each step, focusing on how to achieve them using Python and common libraries like NumPy for numerical operations and SciPy for spatial data structures (specifically, KDTree for efficient nearest neighbor searches).

1. Finding Correspondences (Nearest Neighbors)

The first step in each iteration is to find the closest point in the target point cloud for every point in the source point cloud. This establishes the 'correspondences' that the algorithm will use to calculate the transformation. We use scipy.spatial.KDTree for efficient nearest neighbor queries.

import numpy as np
from scipy.spatial import KDTree

def find_closest_points(source_points, target_points):
    """
    Finds the closest point in target_points for each point in source_points.
    Returns indices of closest points in target_points and their distances.
    """
    kdtree = KDTree(target_points)
    distances, indices = kdtree.query(source_points)
    return indices, distances

# Example usage:
# source_cloud = np.random.rand(100, 3)
# target_cloud = np.random.rand(100, 3)
# closest_indices, _ = find_closest_points(source_cloud, target_cloud)

2. Estimating Transformation (SVD Method)

Once correspondences are established, the next step is to calculate the rigid transformation (rotation matrix R and translation vector t) that best maps the source points to their corresponding target points. A common and robust method for this is using Singular Value Decomposition (SVD).

def estimate_transformation(source_points, target_points_matched):
    """
    Estimates the rigid transformation (R, t) between two sets of corresponding points.
    Uses the SVD method.
    """
    # Calculate centroids
    centroid_source = np.mean(source_points, axis=0)
    centroid_target = np.mean(target_points_matched, axis=0)

    # Center the points
    centered_source = source_points - centroid_source
    centered_target = target_points_matched - centroid_target

    # Calculate covariance matrix H
    H = centered_source.T @ centered_target

    # Perform SVD
    U, S, Vt = np.linalg.svd(H)

    # Calculate Rotation matrix R
    R = Vt.T @ U.T

    # Handle reflection case (if R is a reflection matrix)
    if np.linalg.det(R) < 0:
        Vt[2, :] *= -1
        R = Vt.T @ U.T

    # Calculate Translation vector t
    t = centroid_target - R @ centroid_source

    return R, t

# Example usage:
# R, t = estimate_transformation(source_cloud, target_cloud[closest_indices])

3. Iterative Refinement and Convergence

The ICP algorithm repeatedly applies the above two steps. After estimating the transformation, the source point cloud is transformed, and the process is repeated. The iteration stops when the change in transformation (or the mean squared error between corresponding points) falls below a certain threshold, or a maximum number of iterations is reached.

def icp(source_cloud, target_cloud, max_iterations=100, tolerance=1e-6):
    """
    Performs the Iterative Closest Point (ICP) algorithm.
    """
    transformed_source = np.copy(source_cloud)
    prev_error = float('inf')

    for i in range(max_iterations):
        # Step 1: Find closest points
        closest_indices, distances = find_closest_points(transformed_source, target_cloud)
        target_matched = target_cloud[closest_indices]

        # Step 2: Estimate transformation
        R, t = estimate_transformation(transformed_source, target_matched)

        # Apply transformation to the source cloud
        transformed_source = (R @ transformed_source.T).T + t

        # Calculate current error (mean squared distance)
        current_error = np.mean(distances**2)

        # Check for convergence
        if abs(prev_error - current_error) < tolerance:
            print(f"ICP converged after {i+1} iterations. Error: {current_error:.6f}")
            break

        prev_error = current_error
        print(f"Iteration {i+1}: Error = {current_error:.6f}")

    return transformed_source, R, t

# Full example:
# Generate some synthetic data
# np.random.seed(0)
# target_points = np.random.rand(100, 3) * 10
# R_true = np.array([[0.99, -0.05, 0.1], [0.05, 0.99, -0.05], [-0.1, 0.05, 0.99]])
# t_true = np.array([1, 2, 3])
# source_points_untransformed = (R_true @ target_points.T).T + t_true + np.random.randn(100, 3) * 0.1

# aligned_cloud, final_R, final_t = icp(source_points_untransformed, target_points)
# print("\nFinal Rotation Matrix:\n", final_R)
# print("Final Translation Vector:\n", final_t)