Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cpp/pybind/t/pipelines/registration/registration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ void pybind_registration_definitions(py::module &m) {
"doppler_outlier_threshold"_a,
"outlier_rejection_min_iteration"_a,
"geometric_robust_loss_min_iteration"_a,
"doppler_robust_loss_min_iteration"_a, "goemetric_kernel"_a,
"doppler_robust_loss_min_iteration"_a, "geometric_kernel"_a,
"doppler_kernel"_a, "transform_vehicle_to_sensor"_a)
.def(py::init([](const double lambda_doppler) {
return new TransformationEstimationForDopplerICP(
Expand Down
Binary file added docs/jupyter/t_pipelines/images/doppler_icp.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
302 changes: 297 additions & 5 deletions docs/jupyter/t_pipelines/t_icp_registration.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,11 @@
" - Colored ICP.\n",
" - Requires `target` point-cloud to have `normals` attribute (of same dtype as `position` attribute).\n",
" - Requires `source` and `target` point-clouds to have `colors` attribute (of same dtype as `position` attribute).\n",
"- **o3d.t.pipelines.registration.TransformationEstimationForDopplerICP(period, lambda_doppler, ...)**\n",
" - Doppler ICP.\n",
" - Requires `target` point-cloud to have `normals` attribute (of same dtype as `position` attribute).\n",
" - Requires `source` point-cloud to have `dopplers` and `directions` attributes (of same dtype as `position` attribute).\n",
" - Requires calibration parameters: `period` (time between scans) and `transform_vehicle_to_sensor` (4x4 transformation matrix).\n",
"- **o3d.t.pipelines.registration.TransformationEstimationForGeneralizedICP(robust_kernel, epsilon)** [To be added].\n",
" - Generalized ICP.\n"
]
Expand Down Expand Up @@ -383,7 +388,7 @@
"# Example callback_after_iteration lambda function:\n",
"callback_after_iteration = lambda updated_result_dict : print(\"Iteration Index: {}, Fitness: {}, Inlier RMSE: {},\".format(\n",
" updated_result_dict[\"iteration_index\"].item(),\n",
" updated_result_dict[\"fitness\"].item(), \n",
" updated_result_dict[\"fitness\"].item(),\n",
" updated_result_dict[\"inlier_rmse\"].item()))"
]
},
Expand Down Expand Up @@ -645,10 +650,10 @@
"\n",
"# Save iteration wise `fitness`, `inlier_rmse`, etc. to analyse and tune result.\n",
"callback_after_iteration = lambda loss_log_map : print(\"Iteration Index: {}, Scale Index: {}, Scale Iteration Index: {}, Fitness: {}, Inlier RMSE: {},\".format(\n",
" loss_log_map[\"iteration_index\"].item(), \n",
" loss_log_map[\"scale_index\"].item(), \n",
" loss_log_map[\"scale_iteration_index\"].item(), \n",
" loss_log_map[\"fitness\"].item(), \n",
" loss_log_map[\"iteration_index\"].item(),\n",
" loss_log_map[\"scale_index\"].item(),\n",
" loss_log_map[\"scale_iteration_index\"].item(),\n",
" loss_log_map[\"fitness\"].item(),\n",
" loss_log_map[\"inlier_rmse\"].item()))"
]
},
Expand Down Expand Up @@ -1261,6 +1266,293 @@
"\n",
"draw_registration_result(source, target, reg_multiscale_icp.transformation)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Doppler ICP Registration\n",
"This tutorial demonstrates an ICP variant that uses both geometry and Doppler velocity measurements for registration. It implements the algorithm of [\\[Hexsel2022\\]](https://arxiv.org/abs/2201.11944). Doppler ICP leverages instantaneous radial velocity measurements from range sensors (such as FMCW LiDAR) to improve registration accuracy and achieve faster convergence compared to classical point-to-plane ICP. The Doppler information provides additional constraints that help disambiguate motion, making this algorithm more accurate, robust, and faster-converging than geometric-only ICP methods, especially in feature-denied environments like hallways, tunnels, highways, and bridges. "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"![Comparison of tunnel reconstructions using point-to-plane ICP (left) and Doppler ICP (right) with measurements collected by an FMCW LiDAR.](images/doppler_icp.jpg)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Understanding Doppler ICP\n",
"\n",
"Doppler ICP combines geometric and Doppler velocity constraints in a joint optimization objective:\n",
"\n",
"\\begin{equation}\n",
"E(\\mathbf{T}) = (1-\\lambda)E_{G}(\\mathbf{T}) + \\lambda E_{D}(\\mathbf{T})\n",
"\\end{equation}\n",
"\n",
"where $\\mathbf{T}$ is the transformation matrix to be estimated, $\\lambda \\in [0,1]$ is a weight parameter, $E_{G}$ is the geometric term, and $E_{D}$ is the Doppler term.\n",
"\n",
"The geometric term $E_{G}$ is the same as the [Point-to-plane ICP](../pipelines/icp_registration.ipynb#Point-to-plane-ICP) objective:\n",
"\n",
"\\begin{equation}\n",
"E_{G}(\\mathbf{T}) = \\sum_{(\\mathbf{p},\\mathbf{q})\\in\\mathcal{K}}\\big((\\mathbf{p} - \\mathbf{T}\\mathbf{q})\\cdot\\mathbf{n}_{\\mathbf{p}}\\big)^{2},\n",
"\\end{equation}\n",
"\n",
"where $\\mathcal{K}$ is the correspondence set and $\\mathbf{n}_{\\mathbf{p}}$ is the normal of point $\\mathbf{p}$.\n",
"\n",
"The Doppler term $E_{D}$ measures the consistency between the measured Doppler velocity and the velocity predicted by the estimated transformation:\n",
"\n",
"\\begin{equation}\n",
"E_{D}(\\mathbf{T}) = \\sum_{(\\mathbf{p},\\mathbf{q})\\in\\mathcal{K}}\\big(d_{\\mathbf{q}} - \\mathbf{v}(\\mathbf{T})\\cdot\\hat{\\mathbf{r}}_{\\mathbf{q}}\\big)^{2},\n",
"\\end{equation}\n",
"\n",
"where $d_{\\mathbf{q}}$ is the measured Doppler velocity at point $\\mathbf{q}$, $\\mathbf{v}(\\mathbf{T})$ is the velocity implied by transformation $\\mathbf{T}$, and $\\hat{\\mathbf{r}}_{\\mathbf{q}}$ is the unit direction vector from the sensor to point $\\mathbf{q}$.\n",
"\n",
"By jointly optimizing these complementary constraints, Doppler ICP achieves **faster convergence** compared to classical point-to-plane ICP that solely relies on geometric residuals. The Doppler term provides additional information that helps guide the optimization toward the correct solution more efficiently, particularly in feature-denied environments where geometry alone is ambiguous.\n",
"\n",
"For more details, refer to [\\[Hexsel2022\\]](https://arxiv.org/abs/2201.11944)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Requirements\n",
"\n",
"Doppler ICP requires:\n",
"\n",
"- **Source point cloud** with:\n",
" - `dopplers` attribute: Measured Doppler or radial velocities for each point.\n",
" - `directions` attribute: Unit direction vectors from sensor to each point (normalized point positions). The direction vectors must correspond the raw range measurements (without any motion compensation or undistortion applied).\n",
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing 'to' between 'correspond' and 'the'. Should be 'must correspond to the raw range measurements'.

Suggested change
" - `directions` attribute: Unit direction vectors from sensor to each point (normalized point positions). The direction vectors must correspond the raw range measurements (without any motion compensation or undistortion applied).\n",
" - `directions` attribute: Unit direction vectors from sensor to each point (normalized point positions). The direction vectors must correspond to the raw range measurements (without any motion compensation or undistortion applied).\n",

Copilot uses AI. Check for mistakes.
" \n",
"- **Target point cloud** with:\n",
" - `normals` attribute: Surface normals for each point.\n",
" \n",
"- **Calibration parameters**:\n",
" - `period`: Time period (in seconds) between source and target scans.\n",
" - `transform_vehicle_to_sensor`: 4x4 transformation matrix from vehicle frame to sensor frame. This is optional."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"print(\"1. Load Doppler ICP dataset\")\n",
"import json\n",
"\n",
"demo_sequence = o3d.data.DemoDopplerICPSequence()\n",
"\n",
"# Load point clouds\n",
"source_idx = 0\n",
"target_idx = 1\n",
"source = o3d.t.io.read_point_cloud(demo_sequence.paths[source_idx])\n",
"target = o3d.t.io.read_point_cloud(demo_sequence.paths[target_idx])\n",
"\n",
"# Load calibration parameters\n",
"with open(demo_sequence.calibration_path) as f:\n",
" calibration_data = json.load(f)\n",
"\n",
"transform_vehicle_to_sensor = np.array(\n",
" calibration_data['transform_vehicle_to_sensor']).reshape(4, 4)\n",
"period = calibration_data['period']\n",
"\n",
"print(f\"Loaded source: {demo_sequence.paths[source_idx]}\")\n",
"print(f\"Loaded target: {demo_sequence.paths[target_idx]}\")\n",
"print(f\"Time period between scans: {period:.4f} seconds\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"print(\"2. Prepare point clouds for Doppler ICP\")\n",
"\n",
"# Setup device\n",
"dtype = o3d.core.float32\n",
"device = o3d.core.Device('CPU:0')\n",
"\n",
"# Downsample point clouds\n",
"source_in_S = source.uniform_down_sample(5)\n",
"target_in_S = target.uniform_down_sample(5)\n",
"\n",
"# Transform from sensor frame (S) to vehicle frame (V)\n",
"source_in_V = source_in_S.to(device).transform(transform_vehicle_to_sensor)\n",
"target_in_V = target_in_S.to(device).transform(transform_vehicle_to_sensor)\n",
"\n",
"# Compute normals for target (required for point-to-plane ICP)\n",
"target_in_V.estimate_normals(radius=10.0, max_nn=30)\n",
"\n",
"# Compute direction vectors for source (required for Doppler ICP)\n",
"# Directions are unit vectors from sensor to each point (in sensor frame)\n",
"directions = source_in_S.point.positions.numpy()\n",
"norms = np.tile(np.linalg.norm(directions, axis=1), (3, 1)).T\n",
"directions = directions / norms\n",
"source_in_V.point['directions'] = o3d.core.Tensor(directions, dtype, device)\n",
"\n",
"# Note: The source point cloud should already have 'dopplers' attribute\n",
"# from the dataset. If not, you would need to add it here.\n",
"print(\"Source point cloud attributes:\", source_in_V.point.keys())\n",
"print(\"Target point cloud attributes:\", target_in_V.point.keys())"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Setting baseline with point-to-plane registration\n",
"\n",
"We first run Point-to-plane ICP as a baseline approach for comparison."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Setup robust kernel\n",
"kernel = treg.robust_kernel.RobustKernel(\n",
" treg.robust_kernel.RobustKernelMethod.TukeyLoss,\n",
" scaling_parameter=0.5)\n",
"\n",
"# Setup convergence criteria\n",
"criteria = treg.ICPConvergenceCriteria(\n",
" relative_fitness=1e-6,\n",
" relative_rmse=1e-6,\n",
" max_iteration=200)\n",
"\n",
"# Point-to-plane ICP estimator\n",
"estimator_p2l = treg.TransformationEstimationPointToPlane(kernel)\n",
"\n",
"# Initial transformation\n",
"init_transform = o3d.core.Tensor(np.eye(4), device=device, dtype=dtype)\n",
"\n",
"# Run point-to-plane ICP\n",
"max_neighbor_distance = 0.3\n",
"print(\"Apply Point-to-Plane ICP\")\n",
"s = time.time()\n",
"\n",
"reg_p2l = treg.icp(source_in_V, target_in_V, max_neighbor_distance,\n",
" init_transform, estimator_p2l, criteria)\n",
"\n",
"p2l_time = time.time() - s\n",
"print(f\"Time taken by Point-to-Plane ICP: {p2l_time:.4f} seconds\")\n",
"print(f\"Fitness: {reg_p2l.fitness:.6f}\")\n",
"print(f\"Inlier RMSE: {reg_p2l.inlier_rmse:.6f}\")\n",
"print(f\"Iterations: {reg_p2l.num_iterations}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Doppler ICP Registration\n",
"\n",
"Now we run Doppler ICP using the same parameters. The Doppler term provides additional constraints that help improve registration accuracy and achieve faster convergence compared to point-to-plane ICP.\n",
"\n",
"The `TransformationEstimationForDopplerICP` class provides several parameters to control the registration behavior:\n",
"\n",
"#### Core Parameters\n",
"\n",
"- **`period`** (double, default: 0.1): Time period in seconds between the source and target point clouds. This is used to compute the velocity implied by the transformation. For sequential scans, multiply the base period by the frame difference (e.g., `period * (target_idx - source_idx)`).\n",
"\n",
"- **`lambda_doppler`** (double, default: 0.01): Weight parameter `λ ∈ [0, 1]` that balances the geometric and Doppler terms in the objective function `(1-λ)E_G + λE_D`. A smaller value (e.g., 0.01) emphasizes geometric constraints, while a larger value increases the influence of Doppler measurements. Emprirically 0.01 is a good default value.\n",
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected spelling of 'Emprirically' to 'Empirically'.

Suggested change
"- **`lambda_doppler`** (double, default: 0.01): Weight parameter `λ ∈ [0, 1]` that balances the geometric and Doppler terms in the objective function `(1-λ)E_G + λE_D`. A smaller value (e.g., 0.01) emphasizes geometric constraints, while a larger value increases the influence of Doppler measurements. Emprirically 0.01 is a good default value.\n",
"- **`lambda_doppler`** (double, default: 0.01): Weight parameter `λ ∈ [0, 1]` that balances the geometric and Doppler terms in the objective function `(1-λ)E_G + λE_D`. A smaller value (e.g., 0.01) emphasizes geometric constraints, while a larger value increases the influence of Doppler measurements. Empirically 0.01 is a good default value.\n",

Copilot uses AI. Check for mistakes.
"\n",
"#### Outlier Rejection Parameters\n",
"\n",
"- **`reject_dynamic_outliers`** (bool, default: false): Whether to reject dynamic point outlier correspondences. When enabled, correspondences with large Doppler errors (indicating moving objects) are pruned from the optimization.\n",
"\n",
"- **`doppler_outlier_threshold`** (double, default: 2.0): Threshold for Doppler error (in m/s) above which correspondences are considered outliers and rejected. Only used when `reject_dynamic_outliers` is enabled and `outlier_rejection_min_iteration` is reached.\n",
"\n",
"- **`outlier_rejection_min_iteration`** (size_t, default: 2): Number of ICP iterations after which outlier rejection is enabled. This allows the algorithm to establish initial correspondences before applying outlier filtering."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Setup Doppler ICP estimator\n",
"# Compute actual period: time between scans (multiply base period by frame difference)\n",
"actual_period = period * (target_idx - source_idx)\n",
"\n",
"# Convert calibration transform to tensor\n",
"transform_vehicle_to_sensor_tensor = o3d.core.Tensor(\n",
" transform_vehicle_to_sensor, dtype=dtype, device=device)\n",
"\n",
"# Create Doppler ICP estimator with all parameters\n",
"estimator_dicp = treg.TransformationEstimationForDopplerICP(\n",
" # Core parameters\n",
" period=actual_period, # Time period (seconds) between source and target scans\n",
" lambda_doppler=0.01, # Weight λ ∈ [0,1] for Doppler term: (1-λ)E_G + λE_D\n",
"\n",
" # Outlier rejection parameters\n",
" reject_dynamic_outliers=False, # Set True to reject moving object correspondences\n",
" doppler_outlier_threshold=2.0, # Doppler error threshold (m/s) for outlier rejection\n",
" outlier_rejection_min_iteration=2, # Enable outlier rejection after N iterations\n",
"\n",
" # Robust loss parameters\n",
" geometric_robust_loss_min_iteration=0, # Enable robust loss for geometry from start (iteration 0)\n",
" doppler_robust_loss_min_iteration=2, # Enable robust loss for Doppler after 2 iterations\n",
"\n",
" # Robust kernels for outlier rejection\n",
" geometric_kernel=kernel, # Robust kernel for geometric term (TukeyLoss with scale=0.5)\n",
" doppler_kernel=kernel, # Robust kernel for Doppler term (same as geometric)\n",
"\n",
" # Calibration\n",
" transform_vehicle_to_sensor=transform_vehicle_to_sensor_tensor # Vehicle-to-sensor transform\n",
")\n",
"\n",
"# Run Doppler ICP\n",
"print(\"Apply Doppler ICP\")\n",
"s = time.time()\n",
"\n",
"reg_dicp = treg.icp(source_in_V, target_in_V, max_neighbor_distance,\n",
" init_transform, estimator_dicp, criteria)\n",
"\n",
"dicp_time = time.time() - s\n",
"print(f\"Time taken by Doppler ICP: {dicp_time:.4f} seconds\")\n",
"print(f\"Fitness: {reg_dicp.fitness:.6f}\")\n",
"print(f\"Inlier RMSE: {reg_dicp.inlier_rmse:.6f}\")\n",
"print(f\"Iterations: {reg_dicp.num_iterations}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Compare results\n",
"print(\"\\nComparison:\")\n",
"print(f\"{'Method':<25} {'Fitness':<12} {'RMSE':<12} {'Iterations':<12} {'Time (s)':<12}\")\n",
"print(\"-\" * 75)\n",
"print(f\"{'Point-to-Plane ICP':<25} {reg_p2l.fitness:<12.6f} {reg_p2l.inlier_rmse:<12.6f} {reg_p2l.num_iterations:<12} {p2l_time:<12.4f}\")\n",
"print(f\"{'Doppler ICP':<25} {reg_dicp.fitness:<12.6f} {reg_dicp.inlier_rmse:<12.6f} {reg_dicp.num_iterations:<12} {dicp_time:<12.4f}\")\n",
"\n",
"# Display transformations\n",
"np.set_printoptions(suppress=True, precision=4)\n",
"print(\"\\nPoint-to-Plane ICP transformation:\")\n",
"print(reg_p2l.transformation.numpy())\n",
"print(\"\\nDoppler ICP transformation:\")\n",
"print(reg_dicp.transformation.numpy())"
]
}
],
"metadata": {
Expand Down
1 change: 1 addition & 0 deletions docs/tutorial/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Reference
.. [Curless1996] B. Curless and M. Levoy. A volumetric method for building complex models from range images. In SIGGRAPH, 1996.
.. [Edelsbrunner1983] H. Edelsbrunner and D. G. Kirkpatrick and R. Seidel: On the shape of a set of points in the plane, IEEE Transactions on Information Theory, 29 (4): 551–559, 1983
.. [Ester1996] M. Ester and H.-P. Kriegel and J Sander and X. Xu, A density-based algorithm for discovering clusters in large spatial databases with noise, KDD, 1996.
.. [Hexsel2022] B. Hexsel and H. Vhavle and Y. Chen, DICP: Doppler Iterative Closest Point Algorithm, RSS, 2022.
.. [Katz2007] S. Katz and A. Tal and R. Basri, Direct visibility of point sets, SIGGRAPH, 2007.
.. [Kazhdan2006] M. Kazhdan and M. Bolitho and H. Hoppe: Poisson surface reconstruction, Eurographics, 2006.
.. [Knapitsch2017] A. Knapitsch and J. Park and Q. Zhou and V. Koltun: Tanks and Temples: Benchmarking Large-Scale Scene Reconstruction, ACM Transactions on Graphics, vol 36 (4), 2017
Expand Down
2 changes: 1 addition & 1 deletion examples/python/pipelines/doppler_icp_registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ def run_doppler_icp(args):
outlier_rejection_min_iteration=2,
geometric_robust_loss_min_iteration=0,
doppler_robust_loss_min_iteration=2,
goemetric_kernel=kernel,
geometric_kernel=kernel,
doppler_kernel=kernel,
transform_vehicle_to_sensor=transform_vehicle_to_sensor)

Expand Down
Loading