Customized visualization

The usage of Open3D convenient visualization functions draw_geometries and draw_geometries_with_custom_animation is straightforward. Everything can be done with the GUI. Press h inside the visualizer window to see helper information. For more details, see Visualization.

This tutorial focuses on more advanced functionalities to customize the behavior of the visualizer window. Please refer to examples/python/visualization/customized_visualization.py to try the following examples.

Mimic draw_geometries() with Visualizer class

37
38
39
40
41
42
43
44


def custom_draw_geometry_with_rotation(pcd):

    def rotate_view(vis):
        ctr = vis.get_view_control()
        ctr.rotate(10.0, 0.0)
        return False

This function produces exactly the same functionality as the convenience function draw_geometries.

../../_images/custom.png

Class Visualizer has a couple of variables such as a ViewControl and a RenderOption. The following function reads a predefined RenderOption stored in a json file.

70
71
72
73
74
75
76
    def capture_depth(vis):
        depth = vis.capture_depth_float_buffer()
        plt.imshow(np.asarray(depth))
        plt.show()
        return False

    def capture_image(vis):

Outputs:

../../_images/normal.png

Change field of view

To change field of view of the camera, it is first necessary to get an instance of the visualizer control. To modify the field of view, use change_field_of_view.

47
48
49
50
51
52
53
54
55
56
                                                              rotate_view)


def custom_draw_geometry_load_option(pcd, render_option_path):
    vis = o3d.visualization.Visualizer()
    vis.create_window()
    vis.add_geometry(pcd)
    vis.get_render_option().load_from_json(render_option_path)
    vis.run()
    vis.destroy_window()

The field of view (FoV) can be set to a degree in the range [5,90]. Note that change_field_of_view adds the specified FoV to the current FoV. By default, the visualizer has an FoV of 60 degrees. Calling the following code

custom_draw_geometry_with_custom_fov(pcd, 90.0)

will add the specified 90 degrees to the default 60 degrees. As it exceeds the maximum allowable FoV, the FoV is set to 90 degrees.

../../_images/fov_90.png

The following code

custom_draw_geometry_with_custom_fov(pcd, -90.0)

will set FoV to 5 degrees, because 60 - 90 = -30 is less than 5 degrees.

../../_images/fov_5.png

Callback functions

59
60
61
62
63
64
65
66
67
def custom_draw_geometry_with_key_callback(pcd, render_option_path):

    def change_background_to_black(vis):
        opt = vis.get_render_option()
        opt.background_color = np.asarray([0, 0, 0])
        return False

    def load_render_option(vis):
        vis.get_render_option().load_from_json(render_option_path)

Function draw_geometries_with_animation_callback registers a Python callback function rotate_view as the idle function of the main loop. It rotates the view along the x-axis whenever the visualizer is idle. This defines an animation behavior.

../../_images/rotate_small.gif
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
        plt.show()
        return False

    key_to_callback = {}
    key_to_callback[ord("K")] = change_background_to_black
    key_to_callback[ord("R")] = load_render_option
    key_to_callback[ord(",")] = capture_depth
    key_to_callback[ord(".")] = capture_image
    o3d.visualization.draw_geometries_with_key_callbacks([pcd], key_to_callback)


def custom_draw_geometry_with_camera_trajectory(pcd, render_option_path,
                                                camera_trajectory_path):
    custom_draw_geometry_with_camera_trajectory.index = -1
    custom_draw_geometry_with_camera_trajectory.trajectory =\
        o3d.io.read_pinhole_camera_trajectory(camera_trajectory_path)
    custom_draw_geometry_with_camera_trajectory.vis = o3d.visualization.Visualizer(
    )
    image_path = os.path.join(test_data_path, 'image')
    if not os.path.exists(image_path):
        os.makedirs(image_path)
    depth_path = os.path.join(test_data_path, 'depth')
    if not os.path.exists(depth_path):
        os.makedirs(depth_path)

    def move_forward(vis):
        # This function is called within the o3d.visualization.Visualizer::run() loop
        # The run loop calls the function, then re-render
        # So the sequence in this function is to:
        # 1. Capture frame

Callback functions can also be registered upon key press event. This script registered four keys. For example, pressing k changes the background color to black.

../../_images/key_k.png

Capture images in a customized animation

109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
        # 4. (Re-render)
        ctr = vis.get_view_control()
        glb = custom_draw_geometry_with_camera_trajectory
        if glb.index >= 0:
            print("Capture image {:05d}".format(glb.index))
            depth = vis.capture_depth_float_buffer(False)
            image = vis.capture_screen_float_buffer(False)
            plt.imsave(os.path.join(depth_path, '{:05d}.png'.format(glb.index)),
                       np.asarray(depth),
                       dpi=1)
            plt.imsave(os.path.join(image_path, '{:05d}.png'.format(glb.index)),
                       np.asarray(image),
                       dpi=1)
            # vis.capture_depth_image("depth/{:05d}.png".format(glb.index), False)
            # vis.capture_screen_image("image/{:05d}.png".format(glb.index), False)
        glb.index = glb.index + 1
        if glb.index < len(glb.trajectory.parameters):
            ctr.convert_from_pinhole_camera_parameters(
                glb.trajectory.parameters[glb.index], allow_arbitrary=True)
        else:
            custom_draw_geometry_with_camera_trajectory.vis.\
                register_animation_callback(None)
        return False

    vis = custom_draw_geometry_with_camera_trajectory.vis
    vis.create_window()
    vis.add_geometry(pcd)
    vis.get_render_option().load_from_json(render_option_path)
    vis.register_animation_callback(move_forward)
    vis.run()
    vis.destroy_window()


if __name__ == "__main__":
    sample_data = o3d.data.DemoCustomVisualization()
    pcd_flipped = o3d.io.read_point_cloud(sample_data.point_cloud_path)
    # Flip it, otherwise the pointcloud will be upside down
    pcd_flipped.transform([[1, 0, 0, 0], [0, -1, 0, 0], [0, 0, -1, 0],
                           [0, 0, 0, 1]])

    print("1. Customized visualization to mimic DrawGeometry")
    custom_draw_geometry(pcd_flipped)

    print("2. Changing field of view")
    custom_draw_geometry_with_custom_fov(pcd_flipped, 90.0)
    custom_draw_geometry_with_custom_fov(pcd_flipped, -90.0)

    print("3. Customized visualization with a rotating view")
    custom_draw_geometry_with_rotation(pcd_flipped)

    print("4. Customized visualization showing normal rendering")
    custom_draw_geometry_load_option(pcd_flipped,

This function reads a camera trajectory, then defines an animation function move_forward to travel through the camera trajectory. In this animation function, both color image and depth image are captured using Visualizer.capture_depth_float_buffer and Visualizer.capture_screen_float_buffer respectively. The images are saved as png files.

The captured image sequence:

../../_images/image_small.gif

The captured depth sequence:

../../_images/depth_small.gif

Applying texture maps to objects

This example function uses the rendering class to load an object apply texture maps to it. This example can apply any of the albedo, normal, ao, metallic, and roughness textures present in the object directory.

This function takes the object model directory as the input and loads the object from the directory. It then looks for the available textures in the diretory and applies them to the object.

Before you apply a texture, you must ensure that the texture has the expected properties as supported by the defaultLit shader:

  • albedo: This must be a 3 or 4 channel image, where the RGB components are the albedo. If there is a 4th channel, then it is the alpha channel for transparency. The defaultLit does not support transparency. You must use defaultLitTransparency instead of defaultLit to use transparency, that is similar to defaultLit except that it used alpha channel for transparency.

  • normal: This must be a 3 channel image, where RGB components represent the normal in tangent space at each pixel.

  • roughness: This can be 1, 2, 3, or 4 channel image. However, only the 1st channel (Red) is used and indicates the applicable roughness. The value ranges from 0, smooth and highly glossy, to 1, very rough and diffuse.

  • metallic: This can be 1, 2,3 , or 4 channel image. However, only the 1st channel (Red) is used and indicates the applicable metallic finish. The value ranges from 0, non-metallic, to 1, metallic. Note that the values in between are generally not physically realistic.

  • ao: This can be 1, 2 ,3, or 4 channel image. However, only the 1st channel (Red) is used and indicates the ambient occlusion. The value ranges from 0, indicating all pixels are fully shadowed and not exposed to indirect lighting, to 1, indicating all pixels are fully lit by the indirect lighting.

  • reflectance: This can be 1, 2 ,3, or 4 channel image. However, only the 1st channel (Red) is used and indicates the reflectance or refraction of the image. The value ranges from 0, indicating not reflective, to 1, indicating highly-reflective. Generally, physically accurate materials do not have a value below 0.35.

  • anisotropy: This can be 1, 2 ,3, or 4 channel image. However, only the 1st channel (Red) is used and indicates the anistropic reflectance or refraction of the image. This is used to simulate materials with anisotropic reflectance like brushed metal.

  • ao_rough_metal: This must be a 3 channel image. If this is set then roughness, metallic, and ao texture maps are ignored. Instead, the the ao_rough_metal is used where the Red channel has the ao map, the Green channel has the roughness map, and the Blue channel has the metallic map.

  • anisotropy: This can be 1, 2 ,3, or 4 channel image. However, only the 1st channel (Red) is used to simulate materials with anisotropic reflectance like a brushed metal.

Below is a sample python code that derives a texture path, checks if a texture is available, and then loads the texture.

import open3d as o3d
import open3d.visualization.gui as gui
import open3d.visualization.rendering as rendering
import sys, os

def main():
    if len(sys.argv) < 2:
        print ("Usage: texture-model.py [model directory]\n\t This example will load [model directory].obj and any of albedo, normal, ao, metallic and roughness textures present.")
        exit()

    # Derive the object path set the model, material, and shader
    model_dir = sys.argv[1]
    model_name = os.path.join(model_dir, os.path.basename(model_dir) + ".obj")
    model = o3d.io.read_triangle_mesh(model_name)
    material = o3d.visualization.rendering.Material()
    material.shader = "defaultLit"

    # Derive the texture paths
    albedo_name = os.path.join(model_dir, "albedo.png")
    normal_name = os.path.join(model_dir, "normal.png")
    ao_name = os.path.join(model_dir, "ao.png")
    metallic_name = os.path.join(model_dir, "metallic.png")
    roughness_name = os.path.join(model_dir, "roughness.png")

    # Check if the textures are available and loads the texture. For example, if metallic exists then load metallic texture
    if os.path.exists(albedo_name):
        material.albedo_img = o3d.io.read_image(albedo_name)
    if os.path.exists(normal_name):
        material.normal_img = o3d.io.read_image(normal_name)
    if os.path.exists(ao_name):
        material.ao_img = o3d.io.read_image(ao_name)
    if os.path.exists(metallic_name):
        material.base_metallic = 1.0
        material.metallic_img = o3d.io.read_image(metallic_name)
    if os.path.exists(roughness_name):
        material.roughness_img = o3d.io.read_image(roughness_name)

    # Draw an object named cube using the available model and texture
    o3d.visualization.draw([{"name": "cube", "geometry": model, "material": material}])

if __name__ == "__main__":
 main()