Open3D-GUI系列教程(五)鼠标事件(拾取顶点)
鼠標(biāo)事件(拾取頂點(diǎn))
這里實(shí)現(xiàn)一下鼠標(biāo)拾取頂點(diǎn)的操作。open3d本身提供了交互選點(diǎn)的操作gui.SceneWidget.Controls.PICK_POINTS,但是出于某些超出我認(rèn)知范圍的因素,這玩意兒根本不起作用。所以只能另辟蹊徑。
最新的open3d 0.15.1好像修復(fù)了這個(gè)bug,我試了一下好像還不行,或許是我真的不會(huì)用。
open3d版本:0.14.1
文章目錄
- 鼠標(biāo)事件(拾取頂點(diǎn))
- 1. 注冊(cè)鼠標(biāo)事件
- 2. 定義鼠標(biāo)事件
- 2.1 空間變換
- 2.2 實(shí)現(xiàn)
- 2.2.1 左鍵
- 2.2.2 右鍵
- 2.2.3 總結(jié)
- 2.3 運(yùn)行結(jié)果
- 2.4 完整源代碼
- 附:關(guān)于0.15.1版本解投影部分說(shuō)明
1. 注冊(cè)鼠標(biāo)事件
通過(guò)gui.SceneWidget.set_on_mouse(Callable)注冊(cè)一個(gè)鼠標(biāo)回調(diào)回調(diào)函數(shù)
- 這個(gè)函數(shù)傳入一個(gè)MouseEvent對(duì)象
- 必須返回以下三個(gè)之一
- EventCallbackResult.IGNORED
- EventCallbackResult.HANDLED
- EventCallbackResult.CONSUMED
2. 定義鼠標(biāo)事件
2.1 空間變換
通常一個(gè)模型要呈現(xiàn)在屏幕上需要經(jīng)過(guò)一系列的變化,即MVP矩陣。所以要得到模型上的坐標(biāo),只需要進(jìn)行一次逆變換即可。即 ( M V P ) ? 1 (MVP)^{-1} (MVP)?1
Camera中提供的函數(shù)unproject()可以完成屏幕空間到世界空間的變換。如果加載的模型沒(méi)有進(jìn)行個(gè)平移,那么模型的原點(diǎn)坐標(biāo)和世界的原點(diǎn)是重合的,所以此時(shí)世界坐標(biāo)等價(jià)于模型坐標(biāo)。
unproject(arg0, arg1, arg2, arg3, arg4)
- arg0: 視圖中的x
- arg1: 視圖中的y
- arg2: 視圖中的z(深度值)
- arg3: 寬度view_width
- arg3: 高度view_height
2.2 實(shí)現(xiàn)
需要實(shí)現(xiàn)的功能是ctrl+鼠標(biāo)左鍵選擇一個(gè)頂點(diǎn),ctrl+鼠標(biāo)右鍵刪除最后選擇的頂點(diǎn)。
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):為了獲取屏幕中某一點(diǎn)的深度值,需要使用rendering.Scene.render_to_depth_image( Callable ),該函數(shù)只能用于GUI程序,并將深度圖傳入回調(diào)函數(shù)。
首先定義該回調(diào)函數(shù):
def depth_callback(depth_image):x = event.x - self._scene.frame.xy = event.y - self._scene.frame.y# np.asarray()翻轉(zhuǎn)軸depth = np.asarray(depth_image)[y, x]if depth == 1.0:# 遠(yuǎn)平面(沒(méi)有選中任何物體)text = ""else:# 這里解投影注意y軸,因?yàn)槟P涂臻g向上為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在模型曲面上,但不一定是頂點(diǎn)數(shù)據(jù)# 使用最近點(diǎn)算法在頂點(diǎn)中搜索一個(gè)最近的作為選擇點(diǎn)idx = self._calc_prefer_indicate(world)true_point = np.asarray(self.pcd.points)[idx]# 存儲(chǔ)選擇的頂點(diǎn)self._pick_num += 1self._picked_indicates.append(idx)self._picked_points.append(true_point)# 輸出坐標(biāo)print(f"Pick point #{idx} at ({true_point[0]}, {true_point[1]}, {true_point[2]})")得到頂點(diǎn)后,為了安全的改變UI(這里更新一個(gè)Label的文本和可見(jiàn)性),需要讓這個(gè)函數(shù)在主線程中執(zhí)行,即提供一個(gè)函數(shù),調(diào)用gui.Application.instance.post_to_main_thread(window, function)
定義這個(gè)繪制函數(shù)用來(lái)畫(huà)出選擇點(diǎn):
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)# 標(biāo)記球,半徑看著調(diào)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.HANDLEDdepth_callback中的最近點(diǎn)搜索
def _cacl_prefer_indicate(self, point):pcd = copy.deepcopy(self.pcd)pcd.points.append(np.asarray(point))pcd_tree = o3d.geometry.KDTreeFlann(pcd)# 搜索兩個(gè)最近點(diǎn),第一個(gè)是自身[k, idx, _]=pcd_tree.search_knn_vector_3d(pcd.points[-1], 2)return idx[-1]2.2.2 右鍵
簡(jiǎn)單的刪除工作
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.IGNORED2.2.3 總結(jié)
上面這段代碼的大體框架是,其中存儲(chǔ)只是選擇點(diǎn)只是一些列表,從用法應(yīng)該也看得出來(lái),就懶得寫(xiě)了。具體可以看后面的源碼。
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.IGNORED2.3 運(yùn)行結(jié)果
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# 布局回調(diào)函數(shù)w.set_on_layout(self._on_layout)w.add_child(self._scene)w.add_child(self._info)# ---------------Menu----------------# 菜單欄是全局的(因?yàn)閙acOS上是全局的)# 無(wú)論創(chuàng)建多少窗口,菜單欄只創(chuàng)建一次。# ----以下只針對(duì)Windows的菜單欄創(chuàng)建----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#-----注冊(cè)菜單欄事件------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)# 鼠標(biāo)事件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:# 遠(yuǎn)平面(沒(méi)有幾何體)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)# 標(biāo)記球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]# 打開(kāi)并顯示一個(gè)obj模型def _menu_open(self):# 文件拾取對(duì)話框file_picker = gui.FileDialog(gui.FileDialog.OPEN,"Select file...",self.window.theme)# 文件類(lèi)型過(guò)濾file_picker.add_filter('.obj', 'obj model files')file_picker.add_filter('', 'All files')# 初始文件路徑file_picker.set_path('./model')# 設(shè)置對(duì)話框按鈕回調(diào)file_picker.set_on_cancel(self._on_cancel)file_picker.set_on_done(self._on_done)# 顯示對(duì)話框self.window.show_dialog(file_picker)def _on_cancel(self):# 關(guān)閉當(dāng)前對(duì)話框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()# 定義材質(zhì)material = rendering.MaterialRecord()material.shader = 'defaultLit'# 向場(chǎng)景中添加模型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))# 退出應(yīng)用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()附:關(guān)于0.15.1版本解投影部分說(shuō)明
在解投影部分:
world = self._scene.scene.camera.unproject(x, self._scene.frame.height - y, depth, self._scene.frame.width, self._scene.frame.height)由于新版本中關(guān)于``unproject()`的實(shí)現(xiàn)發(fā)生改動(dòng),所以要得到正確結(jié)果只需將y直接作為參數(shù),不需要height-y這一步,即
world = self._scene.scene.camera.unproject(x, y, depth, self._scene.frame.width, self._scene.frame.height)實(shí)現(xiàn)的具體改動(dòng)如下:參考0.15.1和0.14.1版本的FilamentCamera.cpp
- 0.14.1中的unproject():
- 0.15.1中的unproject()實(shí)現(xiàn)中有height-y這一步:
總結(jié)
以上是生活随笔為你收集整理的Open3D-GUI系列教程(五)鼠标事件(拾取顶点)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 腾讯可信区块链方案白皮书 QA
- 下一篇: 数码相机中的ISO