Non-blocking visualization#

draw_geometries() is a useful function to get a quick overview of static geometries. However, this function holds a process until a visualization window is closed. This is not optimal when geometry is updated and needs to be visualized without closing the window. This tutorial introduces an example to customize the rendering loop.

Review draw_geometries#

draw_geometries has the following rendering-loop (see Visualizer::Run() for the C++ implementation):

while(true):
    if (geometry has changed):
        re-bind geometry to shaders
    if (view parameters have changed):
        re-render the scene
    if (any user mouse/keyboard input):
        respond to it and set flags for re-rendering

Note that both binding geometry and rendering are costly operations, thus they are executed in a lazy way. There are two flags that control them individually. The functions update_geometry and update_renderer set these flags. After rebinding/rendering, these flags are cleared once again.

This rendering loop can be readily customized. For example, a custom loop can be made in this way to visualize ICP registration:

vis = Visualizer()
vis.create_window()
vis.add_geometry(geometry)
for i in range(icp_iteration):
    # do ICP single iteration
    # transform geometry using ICP
    vis.update_geometry(geometry)
    vis.poll_events()
    vis.update_renderer()

The full script implementing this idea is displayed below.

 8# examples/python/visualization/non_blocking_visualization.py
 9
10import open3d as o3d
11import numpy as np
12
13
14def prepare_data():
15    pcd_data = o3d.data.DemoICPPointClouds()
16    source_raw = o3d.io.read_point_cloud(pcd_data.paths[0])
17    target_raw = o3d.io.read_point_cloud(pcd_data.paths[1])
18    source = source_raw.voxel_down_sample(voxel_size=0.02)
19    target = target_raw.voxel_down_sample(voxel_size=0.02)
20
21    trans = [[0.862, 0.011, -0.507, 0.0], [-0.139, 0.967, -0.215, 0.7],
22             [0.487, 0.255, 0.835, -1.4], [0.0, 0.0, 0.0, 1.0]]
23    source.transform(trans)
24    flip_transform = [[1, 0, 0, 0], [0, -1, 0, 0], [0, 0, -1, 0], [0, 0, 0, 1]]
25    source.transform(flip_transform)
26    target.transform(flip_transform)
27    return source, target
28
29
30def demo_non_blocking_visualization():
31    o3d.utility.set_verbosity_level(o3d.utility.VerbosityLevel.Debug)
32
33    source, target = prepare_data()
34    vis = o3d.visualization.Visualizer()
35    vis.create_window()
36    vis.add_geometry(source)
37    vis.add_geometry(target)
38    threshold = 0.05
39    icp_iteration = 100
40    save_image = False
41
42    for i in range(icp_iteration):
43        reg_p2l = o3d.pipelines.registration.registration_icp(
44            source, target, threshold, np.identity(4),
45            o3d.pipelines.registration.TransformationEstimationPointToPlane(),
46            o3d.pipelines.registration.ICPConvergenceCriteria(max_iteration=1))
47        source.transform(reg_p2l.transformation)
48        vis.update_geometry(source)
49        vis.poll_events()
50        vis.update_renderer()
51        if save_image:
52            vis.capture_screen_image("temp_%04d.jpg" % i)
53    vis.destroy_window()
54
55    o3d.utility.set_verbosity_level(o3d.utility.VerbosityLevel.Info)
56
57
58if __name__ == '__main__':
59    demo_non_blocking_visualization()

The following sections explain this script.

Prepare example data#

14def prepare_data():
15    pcd_data = o3d.data.DemoICPPointClouds()
16    source_raw = o3d.io.read_point_cloud(pcd_data.paths[0])
17    target_raw = o3d.io.read_point_cloud(pcd_data.paths[1])
18    source = source_raw.voxel_down_sample(voxel_size=0.02)
19    target = target_raw.voxel_down_sample(voxel_size=0.02)
20
21    trans = [[0.862, 0.011, -0.507, 0.0], [-0.139, 0.967, -0.215, 0.7],
22             [0.487, 0.255, 0.835, -1.4], [0.0, 0.0, 0.0, 1.0]]
23    source.transform(trans)
24    flip_transform = [[1, 0, 0, 0], [0, -1, 0, 0], [0, 0, -1, 0], [0, 0, 0, 1]]
25    source.transform(flip_transform)
26    target.transform(flip_transform)
27    return source, target

This part reads two point clouds and downsamples them. The source point cloud is intentionally transformed for the misalignment. Both point clouds are flipped for better visualization.

Initialize Visualizer class#

30def demo_non_blocking_visualization():
31    o3d.utility.set_verbosity_level(o3d.utility.VerbosityLevel.Debug)
32
33    source, target = prepare_data()
34    vis = o3d.visualization.Visualizer()
35    vis.create_window()
36    vis.add_geometry(source)
37    vis.add_geometry(target)
38    threshold = 0.05
39    icp_iteration = 100
40    save_image = False

These lines make an instance of the visualizer class, open a visualizer window, and add two geometries to the visualizer.

Transform geometry and visualize it#

42    for i in range(icp_iteration):
43        reg_p2l = o3d.pipelines.registration.registration_icp(
44            source, target, threshold, np.identity(4),
45            o3d.pipelines.registration.TransformationEstimationPointToPlane(),
46            o3d.pipelines.registration.ICPConvergenceCriteria(max_iteration=1))
47        source.transform(reg_p2l.transformation)
48        vis.update_geometry(source)
49        vis.poll_events()
50        vis.update_renderer()
51        if save_image:
52            vis.capture_screen_image("temp_%04d.jpg" % i)
53    vis.destroy_window()

This script calls registration_icp for every iteration. Note that it explicitly forces only one ICP iteration via ICPConvergenceCriteria(max_iteration = 1). This is a trick to retrieve a slight pose update from a single ICP iteration. After ICP, source geometry is transformed accordingly.

The next part of the script is the core of this tutorial. update_geometry informs the vis that the related geometries are updated. Finally, the visualizer renders a new frame by calling poll_events and update_renderer. After any for-loop iterations, destroy_window closes the window.

The result looks like the image below.

../../_images/visualize_icp_iteration.gif