Open3D-GUI系列教程(五)鼠标事件(拾取顶点)

这篇具有很好参考价值的文章主要介绍了Open3D-GUI系列教程(五)鼠标事件(拾取顶点)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

鼠标事件(拾取顶点)

这里实现一下鼠标拾取顶点的操作。open3d本身提供了交互选点的操作gui.SceneWidget.Controls.PICK_POINTS,但是出于某些超出我认知范围的因素,这玩意儿根本不起作用。所以只能另辟蹊径。

最新的open3d 0.15.1好像修复了这个bug,我试了一下好像还不行,或许是我真的不会用。
open3d 响应鼠标事件,Open3D-GUI,python,3d,gui,mesh


open3d版本:0.14.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.x
            y = 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 += 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的文本和可见性),需要让这个函数在主线程中执行,即提供一个函数,调用gui.Application.instance.post_to_main_thread(window, function)

定义这个绘制函数用来画出选择点:

			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)
                    
                    # 标记球,半径看着调
                    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 总结

上面这段代码的大体框架是,其中存储只是选择点只是一些列表,从用法应该也看得出来,就懒得写了。具体可以看后面的源码。

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 运行结果

open3d 响应鼠标事件,Open3D-GUI,python,3d,gui,mesh

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

        # 布局回调函数
        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.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()`的实现发生改动,所以要得到正确结果只需将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文章来源地址https://www.toymoban.com/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()实现中有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系列教程(五)鼠标事件(拾取顶点)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Open3D点云数据处理(一):VSCode配置python,并安装open3d教程

    专栏地址:https://blog.csdn.net/weixin_46098577/category_11392993.html 在很久很久以前,我写过这么一篇博客,讲的是open3d点云处理的基本方法。👇 当时是 PyCharm + Anaconda + python3.8 + open3d 0.13 已经是2023年了,现在有了全新版本。目前python由当年的3.8更新到了3.11版本,open3d也从0.13来到了

    2024年02月07日
    浏览(59)
  • open3d学习教程1--点云对象PointCloud

    点云简单来说就是3d坐标下一个个点组成的数据,每个点可以包含x,y,z,颜色、分类值、强度值、时间等等信息。点云是3d数据的表示形式之一。 open3d中用来表示点云的数据结构。pointcloud对象包含了很多处理点云的成员方法,如点云体素下采样,点云上色等等。 pointcloud的静态

    2023年04月08日
    浏览(36)
  • 基于Python的Open3D库进行点云聚类(详细教程)

    基于Python的Open3D库进行点云聚类(详细教程) 随着3D技术的不断发展,点云处理已成为越来越重要的研究领域之一。而点云聚类作为其中的一个热门问题,已经成为广大3D技术工作者必须掌握的技能之一。本文将介绍如何使用Python中的Open3D库实现点云聚类,并给出详细的代码

    2024年02月09日
    浏览(35)
  • 【点云处理教程】00计算机视觉的Open3D简介

            Open3D 是一个开源库,使开发人员能够处理 3D 数据。它提供了一组用于 3D 数据处理、可视化和机器学习任务的工具。该库支持各种数据格式,例如 .ply、.obj、.stl 和 .xyz,并允许用户创建自定义数据结构并在程序中访问它们。 Open3D 广泛应用于机器人、增强现实和自

    2024年02月14日
    浏览(45)
  • open3d教程(二):可视化三维模型,并转换成点云(Python版本)

    可以自己用建模软件建立一个模型 从free3d免费下载 open3d.visualization. draw_geometries 参数: geometry_list ( List[open3d.geometry.Geometry]) : 要可视化的几何体列表. window_name( str ,  optional ,  default=\\\'Open3D\\\'): 展示模型的可视化窗口名称,默认是Open3d. width: 

    2024年02月11日
    浏览(46)
  • python如何实现点云可视化交互——Open3D实例教程(获取所选点的信息)保姆级教学

    Open3D是目前python中可用的用于 3D 数据处理的现代库,可以对点云、网格等三维数据进行读取、采样、配准、可视化等操作。其中对点云等三维模型进行可视化的功能在Python中显得非常方便。 在通过对官方文档的研究之后作者发现在Open3D的多种可视化函数中出现了返回所选点

    2024年02月02日
    浏览(61)
  • 【Open3D】如何在CMake/C++中调用Open3D

    qquad Open3D是点云的开源处理库,支持Python或C++。其Python已有较全的教程,也可以直接使用 pip install open3d 直接进行安装,而若想在C++中调用Open3D则麻烦一些,需要满足以下条件: Open3D git源代码(本教程针对0.16.1的版本) CMake = 3.20 clang = 7 分为以下几步进行: 下载Open3D源代码

    2023年04月18日
    浏览(46)
  • 基于Open3D的点云处理17-Open3d的C++版本

    http://www.open3d.org/docs/latest/cpp_api.html http://www.open3d.org/docs/latest/getting_started.html#c http://www.open3d.org/docs/release/cpp_project.html#cplusplus-example-project https://github.com/isl-org/open3d-cmake-find-package https://github.com/isl-org/open3d-cmake-external-project https://github.com/isl-org/Open3D/releases Note: -DBUILD_SHARED_LIBS

    2024年02月09日
    浏览(58)
  • open3d 0.17.0的open3d.visualization.ViewControl类有bug

    在使用过程中发现 open3d.visualization.ViewControl 的如下方法,在 open3d 0.17.0 环境下不起作用,点云的显示视场还是默认配置;而在 open3d 0.16.0 环境下却正常工作。 rotate set_front set_lookat set_up set_zoom 上述测试代码在如下虚拟环境中进行过测试,均失败。 在如下虚拟环境中正常工作

    2024年02月21日
    浏览(55)
  • 【Open3D可视化——添加标签】:如何在Open3D的可视化窗口中添加文字标签?

    【Open3D可视化——添加标签】:如何在Open3D的可视化窗口中添加文字标签? Open3D是一个基于Python语言开发的跨平台开源工具包,主要用于三维数据处理和可视化。在进行三维数据可视化过程中,往往需要在场景中添加标签来标识物体、点云等信息。本文将介绍如何在Open3D的可

    2024年02月11日
    浏览(72)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包