鼠標事件(拾取頂點)
這里實現(xiàn)一下鼠標拾取頂點的操作。open3d本身提供了交互選點的操作gui.SceneWidget.Controls.PICK_POINTS
,但是出于某些超出我認知范圍的因素,這玩意兒根本不起作用。所以只能另辟蹊徑。
最新的open3d 0.15.1好像修復了這個bug,我試了一下好像還不行,或許是我真的不會用。
open3d版本:0.14.1
1. 注冊鼠標事件
通過gui.SceneWidget.set_on_mouse(Callable)
注冊一個鼠標回調(diào)回調(diào)函數(shù)
- 這個函數(shù)傳入一個
MouseEvent
對象- 必須返回以下三個之一
- EventCallbackResult.IGNORED
- EventCallbackResult.HANDLED
- EventCallbackResult.CONSUMED
2. 定義鼠標事件
2.1 空間變換
通常一個模型要呈現(xiàn)在屏幕上需要經(jīng)過一系列的變化,即MVP矩陣。所以要得到模型上的坐標,只需要進行一次逆變換即可。即 ( M V P ) ? 1 (MVP)^{-1} (MVP)?1
Camera
中提供的函數(shù)unproject()
可以完成屏幕空間到世界空間的變換。如果加載的模型沒有進行個平移,那么模型的原點坐標和世界的原點是重合的,所以此時世界坐標等價于模型坐標。
unproject(arg0, arg1, arg2, arg3, arg4)
- arg0: 視圖中的x
- arg1: 視圖中的y
- arg2: 視圖中的z(深度值)
- arg3: 寬度view_width
- arg3: 高度view_height
2.2 實現(xiàn)
需要實現(xiàn)的功能是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 )
,該函數(shù)只能用于GUI程序,并將深度圖傳入回調(diào)函數(shù)。
首先定義該回調(diào)函數(shù):
def depth_callback(depth_image):
x = event.x - self._scene.frame.x
y = event.y - self._scene.frame.y
# np.asarray()翻轉(zhuǎn)軸
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在模型曲面上,但不一定是頂點數(shù)據(jù)
# 使用最近點算法在頂點中搜索一個最近的作為選擇點
idx = self._calc_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]})")
得到頂點后,為了安全的改變UI(這里更新一個Label的文本和可見性),需要讓這個函數(shù)在主線程中執(zhí)行,即提供一個函數(shù),調(diào)用gui.Application.instance.post_to_main_thread(window, function)
定義這個繪制函數(shù)用來畫出選擇點:
def draw_point():
self._info.text = text
self._info.visible = (text != "")
# 改變layout
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)
# 標記球,半徑看著調(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.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 -= 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
2.2.3 總結(jié)
上面這段代碼的大體框架是,其中存儲只是選擇點只是一些列表,從用法應該也看得出來,就懶得寫了。具體可以看后面的源碼。
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.HANDLED
elif 右鍵:
# ...
return gui.Widget.EventCallbackResult.HANDLED
# 其他事件忽略
return gui.Widget.EventCallbackResult.IGNORED
2.3 運行結(jié)果
2.4 完整源代碼
import open3d as o3d
import open3d.visualization.gui as gui
import open3d.visualization.rendering as rendering
import numpy as np
import copy
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
# 布局回調(diào)函數(shù)
w.set_on_layout(self._on_layout)
w.add_child(self._scene)
w.add_child(self._info)
# ---------------Menu----------------
# 菜單欄是全局的(因為macOS上是全局的)
# 無論創(chuàng)建多少窗口,菜單欄只創(chuàng)建一次。
# ----以下只針對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
#-----注冊菜單欄事件------
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')
# 設置對話框按鈕回調(diào)
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()
# 定義材質(zhì)
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 = 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))
# 退出應用
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()
附:關于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()`的實現(xiàn)發(fā)生改動,所以要得到正確結(jié)果只需將y直接作為參數(shù),不需要height-y這一步,即文章來源:http://www.zghlxwxcb.cn/news/detail-784867.html
world = self._scene.scene.camera.unproject(x, y, depth, self._scene.frame.width, self._scene.frame.height)
實現(xiàn)的具體改動如下:參考0.15.1和0.14.1版本的FilamentCamera.cpp文章來源地址http://www.zghlxwxcb.cn/news/detail-784867.html
- 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()
實現(xiàn)中有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-GUI系列教程(五)鼠標事件(拾取頂點)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!