admin管理员组文章数量:1794759
Open3D
鼠标事件(拾取顶点)
这里实现一下鼠标拾取顶点的操作。open3d本身提供了交互选点的操作gui.SceneWidget.Controls.PICK_POINTS
,但是出于某些超出我认知范围的因素,这玩意儿根本不起作用。所以只能另辟蹊径。
最新的open3d 0.15.1好像修复了这个bug,我试了一下好像还不行,或许是我真的不会用。
open3d版本:0.14.1
文章目录
- 鼠标事件(拾取顶点)
- 1. 注册鼠标事件
- 2. 定义鼠标事件
- 2.1 空间变换
- 2.2 实现
- 2.2.1 左键
- 2.2.2 右键
- 2.2.3 总结
- 2.3 运行结果
- 2.4 完整源代码
- 附:关于0.15.1版本解投影部分说明
1. 注册鼠标事件
通过gui.SceneWidget.set_on_mouse(Callable)
注册一个鼠标回调回调函数
- 这个函数传入一个
MouseEvent
对象- 必须返回以下三个之一
- EventCallbackResult.IGNORED
- EventCallbackResult.HANDLED
- EventCallbackResult.CONSUMED
2. 定义鼠标事件
2.1 空间变换
通常一个模型要呈现在屏幕上需要经过一系列的变化,即MVP矩阵。所以要得到模型上的坐标,只需要进行一次逆变换即可。即 ( M V P ) − 1 (MVP)^{-1} (MVP)−1
Camera
中提供的函数unproject()
可以完成屏幕空间到世界空间的变换。如果加载的模型没有进行个平移,那么模型的原点坐标和世界的原点是重合的,所以此时世界坐标等价于模型坐标。
unproject(arg0, arg1, arg2, arg3, arg4)
- arg0: 视图中的x
- arg1: 视图中的y
- arg2: 视图中的z(深度值)
- arg3: 宽度view_width
- arg3: 高度view_height
2.2 实现
需要实现的功能是ctrl+鼠标左键选择一个顶点,ctrl+鼠标右键删除最后选择的顶点。
def _on_mouse_widget3d(self, event):
2.2.1 左键
if event.type == gui.MouseEvent.Type.BUTTON_DOWN and event.is_button_down(gui.MouseButton.LEFT) and event.is_modifier_down(gui.KeyModifier.CTRL):
为了获取屏幕中某一点的深度值,需要使用rendering.Scene.render_to_depth_image( Callable )
,该函数只能用于GUI程序,并将深度图传入回调函数。
首先定义该回调函数:
def depth_callback(depth_image):x = event.x - self._scene.frame.xy = event.y - self._scene.frame.y# np.asarray()翻转轴depth = np.asarray(depth_image)[y, x]if depth == 1.0:# 远平面(没有选中任何物体)text = ""else:# 这里解投影注意y轴,因为模型空间向上为y轴正方向,屏幕空间向下为y轴正方向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])# world在模型曲面上,但不一定是顶点数据# 使用最近点算法在顶点中搜索一个最近的作为选择点idx = self._calc_prefer_indicate(world)true_point = np.asarray(self.pcd.points)[idx]# 存储选择的顶点self._pick_num += 1self._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]})")
得到顶点后,为了安全的改变UI(这里更新一个Label的文本和可见性),需要让这个函数在主线程中执行,即提供一个函数,调用gui.Application.instance.post_to_main_thread(window, function)
定义这个绘制函数用来画出选择点:
def draw_point():self._info.text = textself._info.visible = (text != "")# 改变layoutself.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
depth_callback中的最近点搜索
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]
2.2.2 右键
简单的删除工作
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 -= 1self._scene.remove_3d_label(self._label3d_list.pop())self._scene.force_redraw()else:print("Undo no point!")return gui.Widget.EventCallbackResult.HANDLEDreturn gui.Widget.EventCallbackResult.IGNORED
2.2.3 总结
上面这段代码的大体框架是,其中存储只是选择点只是一些列表,从用法应该也看得出来,就懒得写了。具体可以看后面的源码。
def _on_mouse_widget3d(self, event):if 左键:def depth_callback(depth_image):# ...def draw_point():#...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.HANDLEDelif 右键:# ...return gui.Widget.EventCallbackResult.HANDLED# 其他事件忽略return gui.Widget.EventCallbackResult.IGNORED
2.3 运行结果
2.4 完整源代码
import open3d as o3d
import open3d.visualization.gui as gui
import open3d.visualization.rendering as rendering
import numpy as np
import copyclass App:MENU_OPEN = 1MENU_SHOW = 5MENU_QUIT = 20MENU_ABOUT = 21show = 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.windowem = 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.xy = event.y - self._scene.frame.ydepth = 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 += 1self._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 = textself._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.HANDLEDelif 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 -= 1self._scene.remove_3d_label(self._label3d_list.pop())self._scene.force_redraw()else:print("Undo no point!")return gui.Widget.EventCallbackResult.HANDLEDreturn gui.Widget.EventCallbackResult.IGNOREDdef _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.force_redraw()self.mesh = meshself.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))# 退出应用def _menu_quit(self):self.window.close()# 切换显示模型def _menu_show(self):self.show = not self.showgui.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_rectself._scene.frame = rpref = 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()
附:关于0.15.1版本解投影部分说明
在解投影部分:
world = self._scene.scene.camera.unproject(x, self._scene.frame.height - y, depth, self._scene.frame.width, self._scene.frame.height)
由于新版本中关于``unproject()`的实现发生改动,所以要得到正确结果只需将y直接作为参数,不需要height-y这一步,即
world = self._scene.scene.camera.unproject(x, y, depth, self._scene.frame.width, self._scene.frame.height)
实现的具体改动如下:参考0.15.1和0.14.1版本的FilamentCamera.cpp
- 0.14.1中的
unproject()
:
Eigen::Vector3f FilamentCamera::Unproject(float x, float y, float z, float view_width, float view_height) const
{Eigen::Vector4f gl_pt(2.0f * x / view_width - 1.0f,2.0f * y / view_height - 1.0f, 2.0f * z - 1.0f, 1.0f);auto proj = GetProjectionMatrix();Eigen::Vector4f obj_pt = (proj * GetViewMatrix()).inverse() * gl_pt;return {obj_pt.x() / obj_pt.w(), obj_pt.y() / obj_pt.w(),obj_pt.z() / obj_pt.w()};
}
- 0.15.1中的
unproject()
实现中有height-y
这一步:
Eigen::Vector3f FilamentCamera::Unproject(float x, float y, float z, float view_width, float view_height) const
{Eigen::Vector4f gl_pt(2.0f * x / view_width - 1.0f,2.0f * (view_height - y) / view_height - 1.0f,2.0f * z - 1.0f, 1.0f);auto proj = GetProjectionMatrix();Eigen::Vector4f obj_pt = (proj * GetViewMatrix()).inverse() * gl_pt;return {obj_pt.x() / obj_pt.w(), obj_pt.y() / obj_pt.w(),obj_pt.z() / obj_pt.w()};
}
本文标签: Open3d
版权声明:本文标题:Open3D 内容由林淑君副主任自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.xiehuijuan.com/baike/1692769720a182326.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论