Open3D-GUI 应用程序打包

前面几篇讲解了从创建一个窗口,回调函数,再到选取顶点,显示法线。这都是在python环境中运行的,为了在其他环境下也能正常使用,需要将其打包成exe文件。

版本:0.14.1

文章目录

  • Open3D-GUI 应用程序打包
    • 1. pyinstaller直接打包(浅尝一下)
      • 错误1【INTEL MKL ERROR】
      • 错误2【Open3D Error】
        • 1.1 pyinstaller 运行时信息
        • 1.2 设置资源路径
        • 1.3 其他更改
    • 2. 正确的打包姿势
    • 3. 其他错误
    • 完整代码

1. pyinstaller直接打包(浅尝一下)

项目结构:

app
├── model
│ └── bunny.obj
└── myApp.py

pyinstaller -w ./myApp.py

直接对myApp.py打包,尝试运行可能会出现以下问题:

错误1【INTEL MKL ERROR】

解决方法:把mkl_intel_thread.1.dll直接复制到exe的目录中,不知道位置的话建议直接Everything搜索

错误2【Open3D Error】

可以看到错误是由于找不到resource目录引起的。

这个问题发生在初始化过程中,即gui.Application.instance.initialize(),这个函数是重载的

initialize(*args, **kwargs)

  1. initialize(self) -> None

    初始化应用,使用默认的资源文件(包含在轮子里)

  2. initialize(self, str) -> None

    使用str给出的资源文件路径来初始化应用。

在之前的代码中,使用的一直是默认资源文件,这在python的运行环境中是没问题的,但是打包之后,程序不在python环境中运行,所以默认的资源文件路径失效,导致无法找到资源文件,即上述错误。

1.1 pyinstaller 运行时信息

打包的代码运行在捆绑包(bundle)中,我们可以使用下面的方法来检测代码是否运行在bundle(“frozen”)中:

import sys
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):print('running in a PyInstaller bundle')
else:print('running in a normal Python process')

当bundle应用程序启动时,引导加载器(bootloader)设置sys.frozen属性,并将bundle文件夹的绝对路径存储在**sys._MEIPASS**中.

  • 对于单文件夹(one-folder),存储的是文件夹的路径
  • 对于单文件(one-file),存储的时bootloader创建的临时文件夹路径。

1.2 设置资源路径

对于要打包的文件,我们需要使用显示的给出资源文件路径,使用上面提到的方法可以兼顾bundle的运行和python环境的运行。

if __name__ == "__main__":if getattr(sys, 'forzen',False) and hasattr(sys,'_MEIPASS'):print("Run in Pyinstaller!")print(sys._MEIPASS)base_path = sys._MEIPASSresource_path = os.path.abspath(os.path.join(base_path,'resources'))   else:print("Run in python process!")resource_path = 'resources'resource_path = os.path.abspath(resource_path)print("Resources:",resource_path)app = App(resource_path)app.run()

相应的对App的__init__进行修改,

class App:# ...def __init__(self, resource_path):gui.Application.instance.initialize(resource_path)  #用指定资源进行初始化#...

默认的资源文件可以在轮子里找到,例如

C:\Users\xxx\anaconda3\envs\open3d_014\Lib\site-packages\open3d

将该目录下的resources目录复制到项目中。

1.3 其他更改

在文件拾取器中,我们设置了拾取器的初始路径,但这个路径在bundle里一般是错误的,所以要更改对应的代码,即:

def _menu_open(self):# ...# 初始路径 /modelif getattr(sys, 'frozen', False) and hasattr(sys,'_MEIPASS'):file_picker.set_path(os.path.join(sys._MEIPASS,'model'))else:file_picker.set_path('./model')# ...

2. 正确的打包姿势

因为要添加资源文件和数据,我们使用spec进行打包。

项目结构
app
├── Geometry.ico
├── mkl_intel_thread.1.dll
├── model
│ └── bunny.obj
├── myApp.py
└── resources
│ ├── brightday_ibl.ktx
│ ├── brightday_skybox.ktx
│ ├── colorMap.filamat
│ ├── crossroads_ibl.ktx
│ ├── crossroads_skybox.ktx
│ ├── defaultGradient.png
│ ├── defaultLit.filamat
│ ├── defaultLitSSR.filamat
│ ├── defaultLitTransparency.filamat
│ ├── defaultTexture.png
│ ├── defaultUnlit.filamat
│ ├── defaultUnlitTransparency.filamat
│ ├── default_ibl.ktx
│ ├── default_skybox.ktx
│ ├── depth.filamat
│ ├── depth_value.filamat
│ ├── hall_ibl.ktx
│ ├── hall_skybox.ktx
│ ├── html
│ ├── img_blit.filamat
│ ├── infiniteGroundPlane.filamat
│ ├── konzerthaus_ibl.ktx
│ ├── konzerthaus_skybox.ktx
│ ├── nightlights_ibl.ktx
│ ├── nightlights_skybox.ktx
│ ├── normals.filamat
│ ├── park2_ibl.ktx
│ ├── park2_skybox.ktx
│ ├── park_ibl.ktx
│ ├── park_skybox.ktx
│ ├── pillars_ibl.ktx
│ ├── pillars_skybox.ktx
│ ├── pointcloud.filamat
│ ├── Roboto-Bold.ttf
│ ├── Roboto-BoldItalic.ttf
│ ├── Roboto-License.txt
│ ├── Roboto-Medium.ttf
│ ├── Roboto-MediumItalic.ttf
│ ├── RobotoMono-Medium.ttf
│ ├── streetlamp_ibl.ktx
│ ├── streetlamp_skybox.ktx
│ ├── ui_blit.filamat
│ ├── unlitBackground.filamat
│ ├── unlitGradient.filamat
│ ├── unlitLine.filamat
│ ├── unlitPolygonOffset.filamat
│ └── unlitSolidColor.filamat


  1. 创建spec文件

    pyi-makespec ./myApp.py
    
  2. 在myApp.spec中添加资源文件,图标等,为了防止错误1,这里也把mkl_intel_thread.1.dll添加进去。

    • 在datas中添加数据文件
    • 在EXE中添加图标(icon = “Geometry.ico”)
    # -*- mode: python ; coding: utf-8 -*-block_cipher = Nonea = Analysis(['myApp.py'],pathex=[],binaries=[],datas=[('resources','resources'),('model','model'),('mkl_intel_thread.1.dll','.')],hiddenimports=[],hookspath=[],hooksconfig={},runtime_hooks=[],excludes=[],win_no_prefer_redirects=False,win_private_assemblies=False,cipher=block_cipher,noarchive=False)
    pyz = PYZ(a.pure, a.zipped_data,cipher=block_cipher)exe = EXE(pyz,a.scripts, [],exclude_binaries=True,name='myApp',debug=False,bootloader_ignore_signals=False,strip=False,upx=True,console=True,disable_windowed_traceback=False,target_arch=None,codesign_identity=None,entitlements_file=None,icon='Geometry.ico',)
    coll = COLLECT(exe,a.binaries,a.zipfiles,a.datas, strip=False,upx=True,upx_exclude=[],name='myApp')
    
  3. 打包

    pyinstaller .\myApp.spec
    
  4. 至此,打包的程序应该和python环境中运行结果一致。

3. 其他错误

还可能出现一些其它问题,可以打开cmd,将exe拖入cmd运行防止闪退,然后根据错误信息改bug即可。

完整代码

import open3d as o3d
import open3d.visualization.gui as gui
import open3d.visualization.rendering as rendering
import numpy as np
import copy
import sys
import osdef 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_setSCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))class App:MENU_OPEN = 1MENU_SHOW = 5MENU_QUIT = 20MENU_ABOUT = 21show = True_picked_indicates = []_picked_points = []_pick_num = 0_label3d_list = []def __init__(self, resource_path):gui.Application.instance.initialize(resource_path)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')file_picker.add_filter('', 'All files')if getattr(sys, 'frozen', False) and hasattr(sys,'_MEIPASS'):file_picker.set_path(os.path.join(sys._MEIPASS,'model'))else:file_picker.set_path('./model')print("Current workspace",os.getcwd())        # 设置对话框按钮回调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):# 读取模型文件print("file: ",file)mesh = o3d.io.read_triangle_mesh(os.path.realpath(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 = 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))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.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__":if getattr(sys, 'frozen', False) and hasattr(sys,'_MEIPASS'):print("Run in Pyinstaller!")print(sys._MEIPASS)base_path = sys._MEIPASSresource_path = os.path.abspath(os.path.join(base_path,'resources'))     else:print("Run in python process!")resource_path = 'resources'resource_path = os.path.abspath(resource_path)print("Resources:",resource_path)app = App(resource_path)app.run()

Open3D-GUI系列教程(七)打包应用程序相关推荐

  1. 黄聪:Microsoft Enterprise Library 5.0 系列教程(七) Exception Handling Application Block

    黄聪:Microsoft Enterprise Library 5.0 系列教程(七) Exception Handling Application Block 原文:黄聪:Microsoft Ent ...

  2. 汇川小型PLC梯形图编程系列教程(七):数值存储与二进制数据知识详解

    原文链接:汇川小型PLC梯形图编程系列教程(七):数值存储与二进制数据知识详解 PLC数据存储原理简介 H123U小型PLC内部采用的是32位的处理器,PLC中的数据处理和电脑中的数据处理基本是一致的 ...

  3. ASP.NET 5系列教程(七)完结篇-解读代码

     在本文中,我们将一起查看TodoController 类代码. [Route] 属性定义了Controller的URL 模板: [Route("api/[controller]&quo ...

  4. 米思齐(Mixly)图形化系列教程(七)-while与do……while

    目录 while循环的执行过程 while循环流程 do--while循环流程 举例 break与continue 教程导航 联系我们 while循环只要循环条件为真就一直执行循环体 while循环的 ...

  5. ASP.NET Core Web Razor Pages系列教程七: 添加新的字段

    系列文章目录:系列教程:使用ASP.NET Core创建Razor Pages Web应用程序 - zhangpeterx的博客 系列教程代码的GitHub地址:ASP .Net Core Razor ...

  6. Unity3D脚本中文系列教程(七)

    http://dong2008hong.blog.163.com/blog/static/4696882720140311445677/?suggestedreading&wumii Unit ...

  7. ClickHouse系列教程七:centos下源码编译安装及报错解决

    ClickHouse系列教程: ClickHouse系列教程 参考上一篇博客: ClickHouse系列教程六:源码分析之Debug编译运行 先安装 gcc 8, g++ 8, cmake 3, ni ...

  8. Redis系列教程(七):Redis并发竞争key的解决方案详解

    Redis高并发的问题 Redis缓存的高性能有目共睹,应用的场景也是非常广泛,但是在高并发的场景下,也会出现问题: 高并发架构系列:Redis缓存和MySQL数据一致性方案详解 如何解决Redis缓 ...

  9. Go 语言系列教程(七) : Map深入解析

    前言 Map 哈希表是一种巧妙并且实用的数据结构.它是一个无序的key/value对的集合,其中所有的key都是不同的,在Go语言中,map类型可以写为map[K]V ,key和value之间可以是不 ...

  10. springboot 2.3_Spring Boot 2.X系列教程:七天从无到有掌握Spring Boot-持续更新

    简介 自从Spring横空出世之后,Spring就成了事实上的J2EE标准.Spring作为一个轻量级的J2EE应用框架,就是针对EJB的复杂特性而设计的,最后毫无疑问,Spring凭借它的简洁,可理 ...

最新文章

  1. 双水泵轮换工作原理图_「物业管理工作」水泵维护保养规程
  2. SmartOS之以太网精简协议栈TinyIP
  3. Java简单多线程断点下载
  4. jvm性能调优 - 08什么情况下对象会被GC
  5. 20211004 矩阵的子空间
  6. SecureCRT 遇到一个致命的错误且必须关闭
  7. Oracle数据库文件恢复与备份思路
  8. android ota不打包_android 6.0系统 make otapackage 错误
  9. open cv+C++错误及经验总结(五)
  10. Go 程序是如何编译成目标机器码的
  11. python的scipy库无法使用_scipy库内存错误
  12. Go语言安装配置运行
  13. Buck_Boost电路分析 亲测
  14. 抓包基础概述,以及为什么抓包
  15. 视频直播技术详解之直播云SDK性能测试模型
  16. 查找算法--Java实例/原理
  17. 计算机二级有没有年龄,九龄童通过全国计算机二级 创年龄最小纪录(图)_新闻中心_新浪福建_新浪网...
  18. Excel和Python实现梯度下降法
  19. 解决img撑大父盒子
  20. python requests瓜子二手车城市列表

热门文章

  1. 英语音标 什么是音素
  2. 【WebDriver】WebDriverWait 用法代码
  3. FICO-固定资产报废处置流程ABAVN
  4. Typora初步学习
  5. 【机器学习】强化学习算法的优化
  6. 信用卡的使用之二——哪些情况下银行降额
  7. 【组合数学】组合恒等式 ( 组合恒等式 积之和 1 | 积之和 1 证明 | 组合恒等式 积之和 2 | 积之和 2 证明 )
  8. 快乐想象识字认字注册码发放!
  9. 程序员必备《新手手册》
  10. Balsamiq新的感觉