{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "nbsphinx": "hidden"
   },
   "outputs": [],
   "source": [
    "import open3d as o3d\n",
    "import numpy as np\n",
    "import os\n",
    "import sys\n",
    "\n",
    "# monkey patches visualization and provides helpers to load geometries\n",
    "sys.path.append('..')\n",
    "import open3d_tutorial as o3dtut\n",
    "# change to True if you want to interact with the visualization windows\n",
    "o3dtut.interactive = not \"CI\" in os.environ"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Multiway registration\n",
    "Multiway registration is the process of aligning multiple pieces of geometry in a global space. Typically, the input is a set of geometries (e.g., point clouds or RGBD images) $\\{\\mathbf{P}_{i}\\}$. The output is a set of rigid transformations $\\{\\mathbf{T}_{i}\\}$, so that the transformed point clouds $\\{\\mathbf{T}_{i}\\mathbf{P}_{i}\\}$ are aligned in the global space.\n",
    "\n",
    "Open3D implements multiway registration via pose graph optimization. The backend implements the technique presented in [\\[Choi2015\\]](../reference.html#choi2015)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Input\n",
    "The first part of the tutorial code reads three point clouds from files. The point clouds are downsampled and visualized together. They are misaligned."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def load_point_clouds(voxel_size=0.0):\n",
    "    pcds = []\n",
    "    demo_icp_pcds = o3d.data.DemoICPPointClouds()\n",
    "    for path in demo_icp_pcds.paths:\n",
    "        pcd = o3d.io.read_point_cloud(path)\n",
    "        pcd_down = pcd.voxel_down_sample(voxel_size=voxel_size)\n",
    "        pcds.append(pcd_down)\n",
    "    return pcds"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "voxel_size = 0.02\n",
    "pcds_down = load_point_clouds(voxel_size)\n",
    "o3d.visualization.draw_geometries(pcds_down,\n",
    "                                  zoom=0.3412,\n",
    "                                  front=[0.4257, -0.2125, -0.8795],\n",
    "                                  lookat=[2.6172, 2.0475, 1.532],\n",
    "                                  up=[-0.0694, -0.9768, 0.2024])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Pose graph\n",
    "A pose graph has two key elements: nodes and edges. A node is a piece of geometry $\\mathbf{P}_{i}$ associated with a pose matrix $\\mathbf{T}_{i}$ which transforms $\\mathbf{P}_{i}$ into the global space. The set $\\{\\mathbf{T}_{i}\\}$ are the unknown variables to be optimized. `PoseGraph.nodes` is a list of `PoseGraphNode`. We set the global space to be the space of $\\mathbf{P}_{0}$. Thus $\\mathbf{T}_{0}$ is the identity matrix. The other pose matrices are initialized by accumulating transformation between neighboring nodes. The neighboring nodes usually have large overlap and can be registered with [Point-to-plane ICP](../pipelines/icp_registration.ipynb#point-to-plane-ICP).\n",
    "\n",
    "A pose graph edge connects two nodes (pieces of geometry) that overlap. Each edge contains a transformation matrix $\\mathbf{T}_{i,j}$ that aligns the source geometry $\\mathbf{P}_{i}$ to the target geometry $\\mathbf{P}_{j}$. This tutorial uses [Point-to-plane ICP](../pipelines/icp_registration.ipynb#point-to-plane-ICP) to estimate the transformation. In more complicated cases, this pairwise registration problem should be solved via [Global registration](global_registration.ipynb).\n",
    "\n",
    "[\\[Choi2015\\]](../reference.html#choi2015) has observed that pairwise registration is error-prone. False pairwise alignments can outnumber correctly aligned pairs. Thus, they partition pose graph edges into two classes. **Odometry edges** connect temporally close, neighboring nodes. A local registration algorithm such as ICP can reliably align them. **Loop closure edges** connect any non-neighboring nodes. The alignment is found by global registration and is less reliable. In Open3D, these two classes of edges are distinguished by the `uncertain` parameter in the initializer of `PoseGraphEdge`.\n",
    "\n",
    "In addition to the transformation matrix $\\mathbf{T}_{i}$, the user can set an information matrix $\\mathbf{\\Lambda}_{i}$ for each edge. If $\\mathbf{\\Lambda}_{i}$ is set using function `get_information_matrix_from_point_clouds`, the loss on this pose graph edge approximates the RMSE of the corresponding sets between the two nodes, with a line process weight. Refer to Eq (3) to (9) in [\\[Choi2015\\]](../reference.html#choi2015) and [the Redwood registration benchmark](http://redwood-data.org/indoor/registration.html) for details.\n",
    "\n",
    "The script creates a pose graph with three nodes and three edges. Among the edges, two of them are odometry edges (`uncertain = False`) and one is a loop closure edge (`uncertain = True`)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def pairwise_registration(source, target):\n",
    "    print(\"Apply point-to-plane ICP\")\n",
    "    icp_coarse = o3d.pipelines.registration.registration_icp(\n",
    "        source, target, max_correspondence_distance_coarse, np.identity(4),\n",
    "        o3d.pipelines.registration.TransformationEstimationPointToPlane())\n",
    "    icp_fine = o3d.pipelines.registration.registration_icp(\n",
    "        source, target, max_correspondence_distance_fine,\n",
    "        icp_coarse.transformation,\n",
    "        o3d.pipelines.registration.TransformationEstimationPointToPlane())\n",
    "    transformation_icp = icp_fine.transformation\n",
    "    information_icp = o3d.pipelines.registration.get_information_matrix_from_point_clouds(\n",
    "        source, target, max_correspondence_distance_fine,\n",
    "        icp_fine.transformation)\n",
    "    return transformation_icp, information_icp\n",
    "\n",
    "\n",
    "def full_registration(pcds, max_correspondence_distance_coarse,\n",
    "                      max_correspondence_distance_fine):\n",
    "    pose_graph = o3d.pipelines.registration.PoseGraph()\n",
    "    odometry = np.identity(4)\n",
    "    pose_graph.nodes.append(o3d.pipelines.registration.PoseGraphNode(odometry))\n",
    "    n_pcds = len(pcds)\n",
    "    for source_id in range(n_pcds):\n",
    "        for target_id in range(source_id + 1, n_pcds):\n",
    "            transformation_icp, information_icp = pairwise_registration(\n",
    "                pcds[source_id], pcds[target_id])\n",
    "            print(\"Build o3d.pipelines.registration.PoseGraph\")\n",
    "            if target_id == source_id + 1:  # odometry case\n",
    "                odometry = np.dot(transformation_icp, odometry)\n",
    "                pose_graph.nodes.append(\n",
    "                    o3d.pipelines.registration.PoseGraphNode(\n",
    "                        np.linalg.inv(odometry)))\n",
    "                pose_graph.edges.append(\n",
    "                    o3d.pipelines.registration.PoseGraphEdge(source_id,\n",
    "                                                             target_id,\n",
    "                                                             transformation_icp,\n",
    "                                                             information_icp,\n",
    "                                                             uncertain=False))\n",
    "            else:  # loop closure case\n",
    "                pose_graph.edges.append(\n",
    "                    o3d.pipelines.registration.PoseGraphEdge(source_id,\n",
    "                                                             target_id,\n",
    "                                                             transformation_icp,\n",
    "                                                             information_icp,\n",
    "                                                             uncertain=True))\n",
    "    return pose_graph"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"Full registration ...\")\n",
    "max_correspondence_distance_coarse = voxel_size * 15\n",
    "max_correspondence_distance_fine = voxel_size * 1.5\n",
    "with o3d.utility.VerbosityContextManager(\n",
    "        o3d.utility.VerbosityLevel.Debug) as cm:\n",
    "    pose_graph = full_registration(pcds_down,\n",
    "                                   max_correspondence_distance_coarse,\n",
    "                                   max_correspondence_distance_fine)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Open3D uses the function `global_optimization` to perform pose graph optimization. Two types of optimization methods can be chosen: `GlobalOptimizationGaussNewton` or `GlobalOptimizationLevenbergMarquardt`. The latter is recommended since it has better convergence property. Class `GlobalOptimizationConvergenceCriteria` can be used to set the maximum number of iterations and various optimization parameters.\n",
    "\n",
    "Class `GlobalOptimizationOption` defines a couple of options. `max_correspondence_distance` decides the correspondence threshold. `edge_prune_threshold` is a threshold for pruning outlier edges. `reference_node` is the node id that is considered to be the global space."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"Optimizing PoseGraph ...\")\n",
    "option = o3d.pipelines.registration.GlobalOptimizationOption(\n",
    "    max_correspondence_distance=max_correspondence_distance_fine,\n",
    "    edge_prune_threshold=0.25,\n",
    "    reference_node=0)\n",
    "with o3d.utility.VerbosityContextManager(\n",
    "        o3d.utility.VerbosityLevel.Debug) as cm:\n",
    "    o3d.pipelines.registration.global_optimization(\n",
    "        pose_graph,\n",
    "        o3d.pipelines.registration.GlobalOptimizationLevenbergMarquardt(),\n",
    "        o3d.pipelines.registration.GlobalOptimizationConvergenceCriteria(),\n",
    "        option)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The global optimization performs twice on the pose graph. The first pass optimizes poses for the original pose graph taking all edges into account and does its best to distinguish false alignments among uncertain edges. These false alignments have small line process weights, and they are pruned after the first pass. The second pass runs without them and produces a tight global alignment. In this example, all the edges are considered as true alignments, hence the second pass terminates immediately."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Visualize optimization\n",
    "The transformed point clouds are listed and visualized using `draw_geometries`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"Transform points and display\")\n",
    "for point_id in range(len(pcds_down)):\n",
    "    print(pose_graph.nodes[point_id].pose)\n",
    "    pcds_down[point_id].transform(pose_graph.nodes[point_id].pose)\n",
    "o3d.visualization.draw_geometries(pcds_down,\n",
    "                                  zoom=0.3412,\n",
    "                                  front=[0.4257, -0.2125, -0.8795],\n",
    "                                  lookat=[2.6172, 2.0475, 1.532],\n",
    "                                  up=[-0.0694, -0.9768, 0.2024])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Make a combined point cloud\n",
    "`PointCloud` has a convenience operator `+` that can merge two point clouds into a single one. In the code below, the points are uniformly resampled using `voxel_down_sample` after merging. This is recommended post-processing after merging point clouds since it can relieve duplicated or over-densified points."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pcds = load_point_clouds(voxel_size)\n",
    "pcd_combined = o3d.geometry.PointCloud()\n",
    "for point_id in range(len(pcds)):\n",
    "    pcds[point_id].transform(pose_graph.nodes[point_id].pose)\n",
    "    pcd_combined += pcds[point_id]\n",
    "pcd_combined_down = pcd_combined.voxel_down_sample(voxel_size=voxel_size)\n",
    "o3d.io.write_point_cloud(\"multiway_registration.pcd\", pcd_combined_down)\n",
    "o3d.visualization.draw_geometries([pcd_combined_down],\n",
    "                                  zoom=0.3412,\n",
    "                                  front=[0.4257, -0.2125, -0.8795],\n",
    "                                  lookat=[2.6172, 2.0475, 1.532],\n",
    "                                  up=[-0.0694, -0.9768, 0.2024])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<div class=\"alert alert-info\">\n",
    "    \n",
    "**Note:**\n",
    "\n",
    "Although this tutorial demonstrates multiway registration for point clouds, the same procedure can be applied to RGBD images. See [Make fragments](../reconstruction_system/make_fragments.rst) for an example.\n",
    "\n",
    "</div>"
   ]
  }
 ],
 "metadata": {
  "celltoolbar": "Edit Metadata",
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
