Skip to content

自定义可视化

Open3D 的便捷可视化函数 draw_geometries 和 draw_geometries_with_custom_animation 用法非常直观。所有操作都可以通过 GUI 完成。在可视化窗口内按 h 键即可查看帮助信息。更多细节请参见可视化。

本教程重点关注用于自定义可视化窗口行为的高级功能。请参考 examples/python/visualization/customized_visualization.py 来运行以下示例。

用 Visualizer 类模拟 draw_geometries()

Section titled “用 Visualizer 类模拟 draw_geometries()”
def custom_draw_geometry_with_rotation(pcd):
def rotate_view(vis):
ctr = vis.get_view_control()
ctr.rotate(10.0, 0.0)
return False
o3d.visualization.draw_geometries_with_animation_callback([pcd],
rotate_view)

这个函数实现的功能与便捷函数 draw_geometries 完全相同。

Visualizer 类有几个成员变量,例如 ViewControl 和 RenderOption。下面的函数会读取一个预先定义好的、存储在 JSON 文件中的 RenderOption。

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()

custom.png normal.png

输出:

要更改相机的视场角(Field of View),首先需要获取可视化控制器的实例。要修改视场角,可使用 change_field_of_view。

def custom_draw_geometry_with_custom_fov(pcd, fov_step):
vis = o3d.visualization.Visualizer()
vis.create_window()
vis.add_geometry(pcd)
ctr = vis.get_view_control()
print("Field of view (before changing) %.2f" % ctr.get_field_of_view())
ctr.change_field_of_view(step=fov_step)
print("Field of view (after changing) %.2f" % ctr.get_field_of_view())
vis.run()
vis.destroy_window()

视场角(FoV)可以被设置在 [5, 90] 度的范围内。请注意,change_field_of_view 会把指定的 FoV 值叠加到当前 FoV 上。默认情况下,可视化器的 FoV 为 60 度。调用如下代码:

custom_draw_geometry_with_custom_fov(pcd, 90.0)

会在默认的 60 度之上再叠加指定的 90 度。由于超出了允许的最大 FoV,最终 FoV 会被设置为 90 度。

而如下代码:

custom_draw_geometry_with_custom_fov(pcd, -90.0)

fov_90.png fov_5.png

会把 FoV 设置为 5 度,因为 60 - 90 = -30 小于 5 度。

def custom_draw_geometry_with_rotation(pcd):
def rotate_view(vis):
ctr = vis.get_view_control()
ctr.rotate(10.0, 0.0)
return False
o3d.visualization.draw_geometries_with_animation_callback([pcd],
rotate_view)

函数 draw_geometries_with_animation_callback 把一个 Python 回调函数 rotate_view 注册为主循环的空闲(idle)函数。每当可视化器处于空闲状态时,它就沿 x 轴旋转视角,由此定义了一种动画行为。

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)
return False
def capture_depth(vis):
depth = vis.capture_depth_float_buffer()
plt.imshow(np.asarray(depth))
plt.show()
return False
def capture_image(vis):
image = vis.capture_screen_float_buffer()
plt.imshow(np.asarray(image))
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)

rotate_small.gif key_k.png

回调函数也可以在按键事件发生时被触发。这个脚本注册了四个按键。例如,按 k 键会把背景色改为黑色。

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
# 2. index++, check ending criteria
# 3. Set camera
# 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()

image_small.gif depth_small.gif

这个函数读取一条相机轨迹,然后定义了一个动画函数 move_forward 来沿相机轨迹行进。在该动画函数中,分别使用 Visualizer.capture_depth_float_buffer 和 Visualizer.capture_screen_float_buffer 捕获彩色图像和深度图像。这些图像会被保存为 PNG 文件。

捕获到的图像序列:

捕获到的深度序列:

本示例函数使用渲染类来加载一个物体并为其应用纹理贴图(Texture map)。它可以应用物体目录中存在的任意一种 albedo、normal、ao、metallic 或 roughness 纹理。

这个函数以物体模型目录作为输入,并从中加载该物体。随后它会在目录中查找可用的纹理并将其应用到物体上。在应用纹理之前,必须确保纹理具有 defaultLit 着色器(shader)所支持的预期属性:

  • albedo(反照率):必须是 3 通道或 4 通道图像,其中 RGB 分量表示反照率。如果存在第 4 个通道,则它是用于透明度的 alpha 通道。defaultLit 不支持透明度。若要使用透明度,必须改用 defaultLitTransparency,它类似于 defaultLit,区别在于它使用 alpha 通道来实现透明度。
  • normal(法线):必须是 3 通道图像,其中 RGB 分量表示每个像素处切线空间(tangent space)内的法线。
  • roughness(粗糙度):可以是 1、2、3 或 4 通道图像。但只有第 1 个通道(红色)被使用,表示适用的粗糙度。取值范围为 0(光滑且高反光)到 1(非常粗糙且漫反射)。
  • metallic(金属度):可以是 1、2、3 或 4 通道图像。但只有第 1 个通道(红色)被使用,表示适用的金属质感。取值范围为 0(非金属)到 1(金属)。注意,介于两者之间的数值通常在物理上并不真实。
  • ao(环境光遮蔽,Ambient Occlusion):可以是 1、2、3 或 4 通道图像。但只有第 1 个通道(红色)被使用,表示环境光遮蔽。取值范围为 0(表示所有像素都被完全遮蔽、不受间接光照影响)到 1(表示所有像素都被间接光照完全照亮)。
  • reflectance(反射率):可以是 1、2、3 或 4 通道图像。但只有第 1 个通道(红色)被使用,表示图像的反射或折射。取值范围为 0(不反射)到 1(高反射)。通常,物理上准确的材质其值不会低于 0.35。
  • anisotropy(各向异性):可以是 1、2、3 或 4 通道图像。但只有第 1 个通道(红色)被使用,表示图像的各向异性反射或折射。它用于模拟具有各向异性反射的材质,如拉丝金属。
  • ao_rough_metal:必须是 3 通道图像。如果设置了此项,则 roughness、metallic 和 ao 纹理贴图会被忽略;转而使用 ao_rough_metal,其中红色通道为 ao 贴图,绿色通道为 roughness 贴图,蓝色通道为 metallic 贴图。

下面是一段示例 Python 代码:它推导出纹理路径,检查纹理是否可用,然后加载纹理。

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()