GUI模块并没有提供一个可视化法向量的方法,所以只能另辟蹊径,使用open3d.geometry.LineSet
来创建一个法向量的线段集。
版本:0.14.1
从点云创建线段集
def normals_lineset(pcd, normal_scale = 0.005, color = [1,0,0]): line_set = o3d.geometry.LineSet() start = np.asarray(pcd.points) end = np.asarray(pcd.points)+(np.asarray(pcd.normals) * normal_scale) points = np.concatenate((start,end)) line_set.points = o3d.utility.Vector3dVector(points) size = len(start) line_set.lines = o3d.utility.Vector2iVector(np.asarray([[i,i+size] for i in range(0,size)])) # 线段端点索引 line_set.paint_uniform_color(color) return line_set
'运行运行
- pcd:点云数据,包含顶点数据
points
和法向数据normals
- normal_scale:法向一般是归一化的,显示效果可能不理想,所以进行缩放
- color:线段颜色
加入场景
在load函数中创建并显示点云法向,也可以在Menubar添加一个item来控制法向的显示。
def load(self, file): # 加载模型 # ...
<span class="token comment"># 法向</span> normals <span class="token operator">=</span> normals_lineset<span class="token punctuation">(</span>self<span class="token punctuation">.</span>pcd<span class="token punctuation">)</span> <span class="token comment"># 材质</span> normal_mat <span class="token operator">=</span> rendering<span class="token punctuation">.</span>MaterialRecord<span class="token punctuation">(</span><span class="token punctuation">)</span> normal_mat <span class="token operator">=</span> <span class="token string">'defaultUnlit'</span> self<span class="token punctuation">.</span>_scene<span class="token punctuation">.</span>scene<span class="token punctuation">.</span>add_geometry<span class="token punctuation">(</span><span class="token string">'normal'</span><span class="token punctuation">,</span>normals<span class="token punctuation">,</span>normal_mat<span class="token punctuation">)</span> <span class="token comment"># 重绘</span> self<span class="token punctuation">.</span>_scene<span class="token punctuation">.</span>force_redraw<span class="token punctuation">(</span><span class="token punctuation">)</span>
运行结果
完整代码
import open3d as o3d
import open3d.visualization.gui as gui
import open3d.visualization.rendering as rendering
import numpy as np
import copy
def normals_lineset(pcd, normal_scale = 0.005, color = [1,0,0]):
line_set = o3d.geometry.LineSet()
start = np.asarray(pcd.points)
end = np.asarray(pcd.points)+(np.asarray(pcd.normals) * normal_scale)
points = np.concatenate((start,end))
line_set.points = o3d.utility.Vector3dVector(points)
size = len(start)
line_set.lines = o3d.utility.Vector2iVector(np.asarray([[i,i+size] for i in range(0,size)]))
line_set.paint_uniform_color(color)
return line_set
class App:
MENU_OPEN = 1
MENU_SHOW = 5
MENU_QUIT = 20
MENU_ABOUT = 21
show = True
_picked_indicates = []
_picked_points = []
_pick_num = 0
_label3d_list = []
def init(self):
gui.Application.instance.initialize()
self.window = gui.Application.instance.create_window(“Pick Points”,800,600)
w = self.window
em = w.theme.font_size
# 渲染窗口
self._scene = gui.SceneWidget()
self._scene.scene = rendering.Open3DScene(w.renderer)
self._scene.set_on_mouse(self._on_mouse_widget3d)
self._info = gui.Label(“”)
self._info.visible = False
# 布局回调函数
w.set_on_layout(self._on_layout)
w.add_child(self._scene)
w.add_child(self._info)
# ---------------Menu----------------
# 菜单栏是全局的(因为macOS上是全局的)
# 无论创建多少窗口,菜单栏只创建一次。
# ----以下只针对Windows的菜单栏创建----
if gui.Application.instance.menubar is None:
# 文件菜单栏
file_menu = gui.Menu()
file_menu.add_item(“Open”,App.MENU_OPEN)
file_menu.add_separator()
file_menu.add_item(“Quit”,App.MENU_QUIT)
# 显示菜单栏
show_menu = gui.Menu()
show_menu.add_item(“Show Geometry”,App.MENU_SHOW)
show_menu.set_checked(App.MENU_SHOW,True)
# 帮助菜单栏
help_menu = gui.Menu()
help_menu.add_item(“About”,App.MENU_ABOUT)
help_menu.set_enabled(App.MENU_ABOUT,False)
# 菜单栏
menu = gui.Menu()
menu.add_menu(“File”,file_menu)
menu.add_menu(“Show”,show_menu)
menu.add_menu(“Help”,help_menu)
gui.Application.instance.menubar = menu
#-----注册菜单栏事件------
w.set_on_menu_item_activated(App.MENU_OPEN,self._menu_open)
w.set_on_menu_item_activated(App.MENU_QUIT,self._menu_quit)
w.set_on_menu_item_activated(App.MENU_SHOW,self._menu_show)
# 鼠标事件
def _on_mouse_widget3d(self, event):
if event.type == gui.MouseEvent.Type.BUTTON_DOWN and event.is_button_down(gui.MouseButton.LEFT) and event.is_modifier_down(gui.KeyModifier.CTRL):
def depth_callback(depth_image):
x = event.x - self._scene.frame.x
y = event.y - self._scene.frame.y
depth = np.asarray(depth_image)[y, x]
if depth==1.0:
# 远平面(没有几何体)
text = “”
else:
world = self._scene.scene.camera.unproject(x, self._scene.frame.height - y, depth, self._scene.frame.width, self._scene.frame.height)
text = “({:.3f}, {:.3f}, {:.3f})”.format(world[0],world[1],world[2])
idx = self._cacl_prefer_indicate(world)
true_point = np.asarray(self.pcd.points)[idx]
self._pick_num += 1
self._picked_indicates.append(idx)
self._picked_points.append(true_point)
print(f"Pick point #{ idx} at ({ true_point[0]}, { true_point[1]}, { true_point[2]})")
def draw_point():
self._info.text = text
self._info.visible = (text != “”)
self.window.set_needs_layout()
if depth != 1.0:
label3d = self._scene.add_3d_label(true_point, “#”+str(self._pick_num))
self._label3d_list.append(label3d)
# 标记球
sphere = o3d.geometry.TriangleMesh.create_sphere(0.0025)
sphere.paint_uniform_color([1,0,0])
sphere.translate(true_point)
material = rendering.MaterialRecord()
material.shader = ‘defaultUnlit’
self._scene.scene.add_geometry(“sphere”+str(self._pick_num),sphere,material)
self._scene.force_redraw()
gui.Application.instance.post_to_main_thread(self.window, draw_point)
self._scene.scene.scene.render_to_depth_image(depth_callback)
return gui.Widget.EventCallbackResult.HANDLED
elif event.type == gui.MouseEvent.Type.BUTTON_DOWN and event.is_button_down(gui.MouseButton.RIGHT) and event.is_modifier_down(gui.KeyModifier.CTRL):
if self._pick_num > 0:
idx = self._picked_indicates.pop()
point = self._picked_points.pop()
print(f"Undo pick: #{ idx} at ({ point[0]}, { point[1]}, { point[2]})")
self._scene.scene.remove_geometry(‘sphere’+str(self._pick_num))
self._pick_num -= 1
self._scene.remove_3d_label(self._label3d_list.pop())
self._scene.force_redraw()
else:
print(“Undo no point!”)
return gui.Widget.EventCallbackResult.HANDLED
return gui.Widget.EventCallbackResult.IGNORED
def _cacl_prefer_indicate(self, point):
pcd = copy.deepcopy(self.pcd)
pcd.points.append(np.asarray(point))
pcd_tree = o3d.geometry.KDTreeFlann(pcd)
[k, idx, _]=pcd_tree.search_knn_vector_3d(pcd.points[-1], 2)
return idx[-1]
# 打开并显示一个obj模型
def _menu_open(self):
# 文件拾取对话框
file_picker = gui.FileDialog(gui.FileDialog.OPEN,“Select file…”,self.window.theme)
# 文件类型过滤
file_picker.add_filter(‘.obj’, ‘obj model files’)
file_picker.add_filter(‘’, ‘All files’)
# 初始文件路径
file_picker.set_path(‘./model’)
# 设置对话框按钮回调
file_picker.set_on_cancel(self._on_cancel)
file_picker.set_on_done(self._on_done)
# 显示对话框
self.window.show_dialog(file_picker)
def _on_cancel(self):
# 关闭当前对话框
self.window.close_dialog()
def _on_done(self, filename):
self.window.close_dialog()
self.load(filename)
def load(self, file):
# 读取模型文件
mesh = o3d.io.read_triangle_mesh(file)
mesh.compute_vertex_normals()
# 定义材质
material = rendering.MaterialRecord()
material.shader = ‘defaultLit’
# 向场景中添加模型
self._scene.scene.add_geometry(‘bunny’,mesh,material)
bounds = mesh.get_axis_aligned_bounding_box()
self._scene.setup_camera(60,bounds,bounds.get_center())
# self._scene.scene.show_geometry(‘bunny’,False)
self.mesh = mesh
self.pcd = o3d.geometry.PointCloud()
self.pcd.points = o3d.utility.Vector3dVector(np.asarray(mesh.vertices))
self.pcd.normals = o3d.utility.Vector3dVector(np.asarray(mesh.vertex_normals))
normals = normals_lineset(self.pcd)
normal_mat = rendering.MaterialRecord()
normal_mat.shader = ‘defaultUnlit’
self._scene.scene.add_geometry(‘normal’,normals,normal_mat)
# 重绘
self._scene.force_redraw()
# 退出应用
def _menu_quit(self):
self.window.close()
# 切换显示模型
def _menu_show(self):
self.show = not self.show
gui.Application.instance.menubar.set_checked(App.MENU_SHOW,self.show)
self._scene.scene.show_geometry(‘bunny’,self.show)
def _on_layout(self, layout_context):
r = self.window.content_rect
self._scene.frame = r
pref = self._info.calc_preferred_size(layout_context, gui.Widget.Constraints())
self._info.frame = gui.Rect(
r.x, r.get_bottom()-pref.height, pref.width, pref.height)
def run(self):
gui.Application.instance.run()
if name == “main”:
app = App()
app.run()