Open3D-GUI系列教程(七)打包应用程序

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

版本:0.14.1

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._MEIPASS
resource_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):
# ...

# 初始路径 /model
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’)

# …

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 = None

a = 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’)

  • 打包

    pyinstaller .\myApp.spec
    
      
      
  • 至此,打包的程序应该和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 os
    

    def 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_set

    SCRIPT_DIR = os.path.dirname(os.path.realpath(file))

    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, resource_path):
    gui.Application.instance.initialize(resource_path)

    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’)
    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 = 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))

    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.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:

    if getattr(sys, ‘frozen’, False) and hasattr(sys,‘_MEIPASS’):
    print(“Run in Pyinstaller!”)
    print(sys._MEIPASS)
    base_path = sys._MEIPASS
    resource_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()

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值