Register Point Clouds Using Rotation Sampler ICP
SUMMARY
Register Point Clouds Using Rotation Sampler ICP finds the best alignment between two point clouds by trying many candidate rotations on a grid of Euler angles, refining each one with point-to-point ICP.
It samples rotations on a regular grid over Euler angles from x/y/z_min_deg to x/y/z_max_deg (relative to initial_transformation_matrix), runs ICP starting from each sampled rotation, and keeps whichever run converged to the best fitness score. This is useful when the two point clouds are positioned close to each other but the relative rotation between them isn't known precisely enough for plain ICP to converge on its own. Compare with register_point_clouds_using_cuboid_translation_sampler_icp, which instead searches over translations. The function returns the 4x4 transform that produced the best result, not an already-moved point cloud — apply it with apply_transform_to_point_cloud.
Use this Skill when you want to register two point clouds whose relative rotation is uncertain but whose relative position is already roughly known.
The Skill
from telekinesis import vitreous
import numpy as np
transformation_matrix = vitreous.register_point_clouds_using_rotation_sampler_icp(
source_point_cloud=source_point_cloud,
target_point_cloud=target_point_cloud,
initial_transformation_matrix=np.eye(4),
x_step_size_deg=30,
y_step_size_deg=10,
z_step_size_deg=30,
x_min_deg=0,
x_max_deg=180,
y_min_deg=0,
y_max_deg=180,
z_min_deg=0,
z_max_deg=180,
early_stop_fitness_score=0.9,
min_fitness_score=0.2,
max_iterations=100,
max_correspondence_distance=2,
estimate_scaling=False,
)
aligned_point_cloud = vitreous.apply_transform_to_point_cloud(
point_cloud=source_point_cloud,
transformation_matrix=transformation_matrix,
modify_inplace=False,
)Data Transfer Notice
There is no longer a fixed limit of 1 million points per request. However, very large datasets may result in slower data transfer and processing times. We are continuously optimizing performance as part of our beta program, with ongoing improvements to enhance speed and reliability.
Example
Source and Target Point Clouds
Raw sensor input i.e. target point cloud in green and object model in red
Registered Point Clouds
Registered source point cloud (red) aligned to target point cloud (green) using rotation sampler ICP with multiple orientation initializations
The Code
"""
Demonstrates finding the best alignment by trying multiple rotations with ICP refinement.
"""
import numpy as np
from loguru import logger
import rerun as rr
from telekinesis import vitreous, datatypes
def register_point_clouds_using_rotation_sampler_icp_example():
"""
Finds best alignment by trying multiple rotations with ICP refinement.
Samples rotations in Euler angle space, runs ICP for each, and keeps the best.
"""
# ===================== Load Data ==========================================
source_url = "https://assets.telekinesis.ai/examples/v1/point_clouds/zivid_bottle_cylinder_centered.ply"
target_url = "https://assets.telekinesis.ai/examples/v1/point_clouds/zivid_bottle_segmented.ply"
source_point_cloud = datatypes.PointCloud.from_url(url=source_url, use_cache=True)
target_point_cloud = datatypes.PointCloud.from_url(url=target_url, use_cache=True)
# ===================== Run Skill ==========================================
transformation_matrix = vitreous.register_point_clouds_using_rotation_sampler_icp(
x_step_size_deg=30,
y_step_size_deg=10,
z_step_size_deg=30,
x_min_deg=0,
x_max_deg=180,
y_min_deg=0,
y_max_deg=180,
z_min_deg=0,
z_max_deg=180,
early_stop_fitness_score=0.9,
min_fitness_score=0.2,
max_iterations=100,
max_correspondence_distance=2,
estimate_scaling=False,
source_point_cloud=source_point_cloud,
target_point_cloud=target_point_cloud,
initial_transformation_matrix=np.eye(4),
)
# ===================== Log ================================================
logger.success(
f"Registered {source_point_cloud} to {target_point_cloud} using rotation sampler ICP"
)
logger.success(f"Results: {transformation_matrix}")
logger.info(f"Transformation matrix data: {transformation_matrix.data}")
logger.info(f"Transformation matrix shape: {transformation_matrix.shape}")
logger.info(f"Transformation matrix ndim: {transformation_matrix.ndim}")
logger.info(f"Transformation matrix dtype: {transformation_matrix.dtype}")
# ===================== Visualization (Optional) ===========================
aligned_source_point_cloud = vitreous.apply_transform_to_point_cloud(
point_cloud=source_point_cloud,
transformation_matrix=transformation_matrix,
modify_inplace=False,
)
rr.init("register_point_clouds_using_rotation_sampler_icp_example", spawn=True)
datatypes.visualize(source_point_cloud, entity_path="/1-before_registration_source")
datatypes.visualize(target_point_cloud, entity_path="/2-before_registration_target")
datatypes.visualize(
aligned_source_point_cloud, entity_path="/3-after_registration_source_aligned"
)
if __name__ == "__main__":
register_point_clouds_using_rotation_sampler_icp_example()Runnable examples are available in the Telekinesis examples repository.
Follow the README in that repository to set up the environment, run this specific example with:
cd telekinesis-examples
python examples/point_cloud/register_point_clouds_using_rotation_sampler_icp.pyParameter Configuration
| Key | Type | Default | Description |
|---|---|---|---|
source_point_cloud | datatypes.PointCloud | required | The point cloud to align |
target_point_cloud | datatypes.PointCloud | required | The point cloud to align to |
initial_transformation_matrix | datatypes.Mat4x4 | np.ndarray | list[list[float]] | np.eye(4) | A 4x4 transform applied to source_point_cloud before the search — the rotation search happens relative to this initial alignment |
x_step_size_deg | datatypes.Int | int | 20 | Spacing, in degrees, between sampled rotations around the x-axis |
y_step_size_deg | datatypes.Int | int | 20 | Spacing, in degrees, between sampled rotations around the y-axis |
z_step_size_deg | datatypes.Int | int | 20 | Spacing, in degrees, between sampled rotations around the z-axis |
x_min_deg | datatypes.Int | int | 0 | Lower bound, in degrees, of the rotation search around the x-axis |
x_max_deg | datatypes.Int | int | 180 | Upper bound, in degrees, of the rotation search around the x-axis |
y_min_deg | datatypes.Int | int | 0 | Lower bound, in degrees, of the rotation search around the y-axis |
y_max_deg | datatypes.Int | int | 180 | Upper bound, in degrees, of the rotation search around the y-axis |
z_min_deg | datatypes.Int | int | 0 | Lower bound, in degrees, of the rotation search around the z-axis |
z_max_deg | datatypes.Int | int | 180 | Upper bound, in degrees, of the rotation search around the z-axis |
early_stop_fitness_score | datatypes.Float | float | int | 0.5 | A fitness score in [0, 1] at which the grid search stops early instead of trying every remaining sample |
min_fitness_score | datatypes.Float | float | int | 0.9 | Minimum fitness score in [0, 1] the best result must reach to be accepted at all |
max_iterations | datatypes.Int | int | 50 | Maximum number of ICP iterations run for each sampled rotation |
max_correspondence_distance | datatypes.Float | float | int | 0.02 | Maximum distance, in meters, at which two points are considered a match during ICP |
estimate_scaling | datatypes.Bool | bool | False | Whether to also estimate and apply a uniform scale factor between the two point clouds, instead of assuming they're at the same scale |
Returns
| Type | Description |
|---|---|
datatypes.Mat4x4 | The best-scoring 4x4 transform found by the grid search — this is the transform itself, not an already-transformed point cloud. Pass it to apply_transform_to_point_cloud (as transformation_matrix) to actually move source_point_cloud's points. Use .data for the raw (4, 4) array. |
Raises
| Exception | Condition |
|---|---|
TypeError | A parameter's value does not match its expected type (see the Parameter Configuration table above) |
ValueError | initial_transformation_matrix is not shape (4, 4) (or, for a list input, doesn't contain only numeric elements) |
ConfigurationError | The TELEKINESIS_API_KEY environment variable is not set |
SerializationError | The request input failed to serialize, or the response failed to deserialize |
RequestTimeoutError | The request to the Vitreous service timed out |
TransportError | A network failure occurred before a response was received |
ClientError | The Vitreous service rejected the request due to invalid input, invalid data, or another unexpected 4xx response |
AuthenticationError | The API key was rejected as invalid or expired |
AuthenticationServiceError | The authentication service was unavailable |
ServerError | The Vitreous service returned a 5xx or otherwise unexpected error response |
How to Tune the Parameters
The register_point_clouds_using_rotation_sampler_icp Skill exposes fourteen tunable parameters that define the rotation search grid and control the ICP refinement run at each grid point.
initial_transformation_matrix
- Controls: The seed transform the rotation grid is centered on — every sampled rotation is relative to this.
- Default:
np.eye(4)(identity — no pre-alignment) - Provide a rough translation-only alignment here if
source_point_cloudandtarget_point_cloudaren't already close in position; this Skill only searches over rotation, not translation.
x_step_size_deg / y_step_size_deg / z_step_size_deg
- Controls: The spacing, in degrees, between sampled rotations around each axis.
- Units: Degrees
- Default:
20on all three axes - Increase → coarser search — fewer samples, faster, but may miss the optimal rotation
- Decrease → finer search — more samples, slower
- Typical range: 5-45 degrees — use 5-10 for a fine search, 10-20 for balanced, 20-45 for coarse
x_min_deg / x_max_deg, y_min_deg / y_max_deg, z_min_deg / z_max_deg
- Controls: The bounds, in degrees, of the rotation search along each Euler axis.
- Units: Degrees
- Default:
0/180on all three axes - The
_max_degbound should exceed the_min_degbound - Typical range: 0-360 degrees — use 0-180 to search a half-sphere of orientations, 0-360 for the full range
early_stop_fitness_score
- Controls: A fitness score in
[0, 1]at which the grid search stops early instead of trying every remaining sample. - Units: Dimensionless (fitness score)
- Default:
0.5 - Increase → accepts stopping sooner (faster), but may settle for a suboptimal alignment
- Decrease → requires a better fit before stopping early
- Typical range: 0.3-0.7
min_fitness_score
- Controls: The minimum fitness score in
[0, 1]the best result must reach to be accepted at all. - Units: Dimensionless (fitness score)
- Default:
0.9 - Increase → requires higher-quality alignment
- Decrease → accepts lower quality
- Typical range: 0.7-0.99
max_iterations
- Controls: The maximum number of ICP iterations run for each sampled rotation.
- Units: Iterations (integer)
- Default:
50 - Typical range: 10-200
max_correspondence_distance
- Controls: The maximum distance, in meters, at which two points are considered a match during ICP.
- Units: Meters
- Default:
0.02 - Set to roughly 2-5x the point spacing
- Typical range: 0.01-0.1 meters
estimate_scaling
- Controls: Whether to also estimate and apply a uniform scale factor between the two point clouds, instead of assuming they're at the same scale.
- Default:
False - Set to
Trueonly if the clouds might genuinely be at different scales.
TIP
Search a half-sphere (0-180 degrees) rather than the full 0-360 range on each axis whenever your object has no meaningful symmetry that would make the other half distinct — it halves the number of ICP runs for the same step_size_deg. If translation is also uncertain, resolve that separately (e.g. with register_point_clouds_using_cuboid_translation_sampler_icp or a known fixed translation) — this Skill only searches over rotation.
Where to Use the Skill
Common pipelines include:
- 6D pose estimation with unknown orientation – aligning parts that land on a conveyor or fixture in an arbitrary orientation
- Object alignment with unknown initial rotation – registering a reference model to a scan when the object's rotation, but not its position, is unpredictable
- Multi-view point cloud registration – merging scans whose relative rotation wasn't tracked precisely enough for a direct ICP start
- Refining a coarse rotation guess – running a finer search once a first-pass alignment narrows down the plausible orientation range
Alternative Skills
| Skill | vs. Register Point Clouds Using Rotation Sampler ICP |
|---|---|
| register_point_clouds_using_cuboid_translation_sampler_icp | Searches over translations instead of rotations. Use it when the clouds are already close in orientation but the relative translation is unknown; use this Skill when it's the other way around. |
| register_point_clouds_using_point_to_point_icp | Plain point-to-point ICP from a single initial guess, with no sampling. Use it once you already have a rotation estimate good enough for direct convergence; use this Skill first if you don't. |
| register_point_clouds_using_fast_global_registration | Feature-based global registration that doesn't require any rough pre-alignment at all. A good alternative starting point when neither the translation nor the rotation between the clouds is known. |
The SDK's register_point_clouds_using_centroid_translation is a fast, translation-only coarse-alignment step commonly run alongside this Skill (it has no doc page yet, but its docstring — and this Skill's — reference each other directly) when translation also needs establishing before or after the rotation search.
When Not to Use the Skill
Do not use Register Point Clouds Using Rotation Sampler ICP when:
- The relative rotation is already known – use
register_point_clouds_using_point_to_point_icp(orregister_point_clouds_using_point_to_plane_icp) directly instead - The translation between the clouds is unknown, not the rotation – use
register_point_clouds_using_cuboid_translation_sampler_icpinstead, which searches translations - You have no rough alignment at all, in rotation or translation – use
register_point_clouds_using_fast_global_registrationfirst, since it tolerates large initial misalignment - You need a fast result – this Skill runs a full ICP pass for every sampled rotation, so it is slower than a single ICP call
- The rotation uncertainty spans the full range at a fine step size – the grid may need too many samples to search effectively
- The point clouds may be at different scales – set
estimate_scaling=True, or pre-scale the clouds, otherwise the search assumes matching scale
TIP
Keep the rotation search range (x_min_deg/x_max_deg/y_min_deg/y_max_deg/z_min_deg/z_max_deg) as tight as your actual orientation uncertainty allows — a smaller range at a given step size means fewer ICP runs and a faster search.

