Visualization

add_geometry.py

 27import open3d as o3d
 28import open3d.visualization.gui as gui
 29import open3d.visualization.rendering as rendering
 30import platform
 31import random
 32import threading
 33import time
 34
 35isMacOS = (platform.system() == "Darwin")
 36
 37
 38# This example shows two methods of adding geometry to an existing scene.
 39# 1) add via a UI callback (in this case a menu, but a button would be similar,
 40#    you would call `button.set_on_clicked(self.on_menu_sphere_)` when
 41#    configuring the button. See `on_menu_sphere()`.
 42# 2) add asynchronously by polling from another thread. GUI functions must be
 43#    called from the UI thread, so use Application.post_to_main_thread().
 44#    See `on_menu_random()`.
 45# Running the example will show a simple window with a Debug menu item with the
 46# two different options. The second method will add random spheres for
 47# 20 seconds, during which time you can be interacting with the scene, rotating,
 48# etc.
 49class SpheresApp:
 50    MENU_SPHERE = 1
 51    MENU_RANDOM = 2
 52    MENU_QUIT = 3
 53
 54    def __init__(self):
 55        self._id = 0
 56        self.window = gui.Application.instance.create_window(
 57            "Add Spheres Example", 1024, 768)
 58        self.scene = gui.SceneWidget()
 59        self.scene.scene = rendering.Open3DScene(self.window.renderer)
 60        self.scene.scene.set_background([1, 1, 1, 1])
 61        self.scene.scene.scene.set_sun_light(
 62            [-1, -1, -1],  # direction
 63            [1, 1, 1],  # color
 64            100000)  # intensity
 65        self.scene.scene.scene.enable_sun_light(True)
 66        bbox = o3d.geometry.AxisAlignedBoundingBox([-10, -10, -10],
 67                                                   [10, 10, 10])
 68        self.scene.setup_camera(60, bbox, [0, 0, 0])
 69
 70        self.window.add_child(self.scene)
 71
 72        # The menu is global (because the macOS menu is global), so only create
 73        # it once, no matter how many windows are created
 74        if gui.Application.instance.menubar is None:
 75            if isMacOS:
 76                app_menu = gui.Menu()
 77                app_menu.add_item("Quit", SpheresApp.MENU_QUIT)
 78            debug_menu = gui.Menu()
 79            debug_menu.add_item("Add Sphere", SpheresApp.MENU_SPHERE)
 80            debug_menu.add_item("Add Random Spheres", SpheresApp.MENU_RANDOM)
 81            if not isMacOS:
 82                debug_menu.add_separator()
 83                debug_menu.add_item("Quit", SpheresApp.MENU_QUIT)
 84
 85            menu = gui.Menu()
 86            if isMacOS:
 87                # macOS will name the first menu item for the running application
 88                # (in our case, probably "Python"), regardless of what we call
 89                # it. This is the application menu, and it is where the
 90                # About..., Preferences..., and Quit menu items typically go.
 91                menu.add_menu("Example", app_menu)
 92                menu.add_menu("Debug", debug_menu)
 93            else:
 94                menu.add_menu("Debug", debug_menu)
 95            gui.Application.instance.menubar = menu
 96
 97        # The menubar is global, but we need to connect the menu items to the
 98        # window, so that the window can call the appropriate function when the
 99        # menu item is activated.
100        self.window.set_on_menu_item_activated(SpheresApp.MENU_SPHERE,
101                                               self._on_menu_sphere)
102        self.window.set_on_menu_item_activated(SpheresApp.MENU_RANDOM,
103                                               self._on_menu_random)
104        self.window.set_on_menu_item_activated(SpheresApp.MENU_QUIT,
105                                               self._on_menu_quit)
106
107    def add_sphere(self):
108        self._id += 1
109        mat = rendering.MaterialRecord()
110        mat.base_color = [
111            random.random(),
112            random.random(),
113            random.random(), 1.0
114        ]
115        mat.shader = "defaultLit"
116        sphere = o3d.geometry.TriangleMesh.create_sphere(0.5)
117        sphere.compute_vertex_normals()
118        sphere.translate([
119            10.0 * random.uniform(-1.0, 1.0), 10.0 * random.uniform(-1.0, 1.0),
120            10.0 * random.uniform(-1.0, 1.0)
121        ])
122        self.scene.scene.add_geometry("sphere" + str(self._id), sphere, mat)
123
124    def _on_menu_sphere(self):
125        # GUI callbacks happen on the main thread, so we can do everything
126        # normally here.
127        self.add_sphere()
128
129    def _on_menu_random(self):
130        # This adds spheres asynchronously. This pattern is useful if you have
131        # data coming in from another source than user interaction.
132        def thread_main():
133            for _ in range(0, 20):
134                # We can only modify GUI objects on the main thread, so we
135                # need to post the function to call to the main thread.
136                gui.Application.instance.post_to_main_thread(
137                    self.window, self.add_sphere)
138                time.sleep(1)
139
140        threading.Thread(target=thread_main).start()
141
142    def _on_menu_quit(self):
143        gui.Application.instance.quit()
144
145
146def main():
147    gui.Application.instance.initialize()
148    SpheresApp()
149    gui.Application.instance.run()
150
151
152if __name__ == "__main__":
153    main()

all_widgets.py

 27import open3d.visualization.gui as gui
 28import os.path
 29
 30basedir = os.path.dirname(os.path.realpath(__file__))
 31
 32
 33class ExampleWindow:
 34    MENU_CHECKABLE = 1
 35    MENU_DISABLED = 2
 36    MENU_QUIT = 3
 37
 38    def __init__(self):
 39        self.window = gui.Application.instance.create_window("Test", 400, 768)
 40        # self.window = gui.Application.instance.create_window("Test", 400, 768,
 41        #                                                        x=50, y=100)
 42        w = self.window  # for more concise code
 43
 44        # Rather than specifying sizes in pixels, which may vary in size based
 45        # on the monitor, especially on macOS which has 220 dpi monitors, use
 46        # the em-size. This way sizings will be proportional to the font size,
 47        # which will create a more visually consistent size across platforms.
 48        em = w.theme.font_size
 49
 50        # Widgets are laid out in layouts: gui.Horiz, gui.Vert,
 51        # gui.CollapsableVert, and gui.VGrid. By nesting the layouts we can
 52        # achieve complex designs. Usually we use a vertical layout as the
 53        # topmost widget, since widgets tend to be organized from top to bottom.
 54        # Within that, we usually have a series of horizontal layouts for each
 55        # row.
 56        layout = gui.Vert(0, gui.Margins(0.5 * em, 0.5 * em, 0.5 * em,
 57                                         0.5 * em))
 58
 59        # Create the menu. The menu is global (because the macOS menu is global),
 60        # so only create it once.
 61        if gui.Application.instance.menubar is None:
 62            menubar = gui.Menu()
 63            test_menu = gui.Menu()
 64            test_menu.add_item("An option", ExampleWindow.MENU_CHECKABLE)
 65            test_menu.set_checked(ExampleWindow.MENU_CHECKABLE, True)
 66            test_menu.add_item("Unavailable feature",
 67                               ExampleWindow.MENU_DISABLED)
 68            test_menu.set_enabled(ExampleWindow.MENU_DISABLED, False)
 69            test_menu.add_separator()
 70            test_menu.add_item("Quit", ExampleWindow.MENU_QUIT)
 71            # On macOS the first menu item is the application menu item and will
 72            # always be the name of the application (probably "Python"),
 73            # regardless of what you pass in here. The application menu is
 74            # typically where About..., Preferences..., and Quit go.
 75            menubar.add_menu("Test", test_menu)
 76            gui.Application.instance.menubar = menubar
 77
 78        # Each window needs to know what to do with the menu items, so we need
 79        # to tell the window how to handle menu items.
 80        w.set_on_menu_item_activated(ExampleWindow.MENU_CHECKABLE,
 81                                     self._on_menu_checkable)
 82        w.set_on_menu_item_activated(ExampleWindow.MENU_QUIT,
 83                                     self._on_menu_quit)
 84
 85        # Create a file-chooser widget. One part will be a text edit widget for
 86        # the filename and clicking on the button will let the user choose using
 87        # the file dialog.
 88        self._fileedit = gui.TextEdit()
 89        filedlgbutton = gui.Button("...")
 90        filedlgbutton.horizontal_padding_em = 0.5
 91        filedlgbutton.vertical_padding_em = 0
 92        filedlgbutton.set_on_clicked(self._on_filedlg_button)
 93
 94        # (Create the horizontal widget for the row. This will make sure the
 95        # text editor takes up as much space as it can.)
 96        fileedit_layout = gui.Horiz()
 97        fileedit_layout.add_child(gui.Label("Model file"))
 98        fileedit_layout.add_child(self._fileedit)
 99        fileedit_layout.add_fixed(0.25 * em)
100        fileedit_layout.add_child(filedlgbutton)
101        # add to the top-level (vertical) layout
102        layout.add_child(fileedit_layout)
103
104        # Create a collapsible vertical widget, which takes up enough vertical
105        # space for all its children when open, but only enough for text when
106        # closed. This is useful for property pages, so the user can hide sets
107        # of properties they rarely use. All layouts take a spacing parameter,
108        # which is the spacinging between items in the widget, and a margins
109        # parameter, which specifies the spacing of the left, top, right,
110        # bottom margins. (This acts like the 'padding' property in CSS.)
111        collapse = gui.CollapsableVert("Widgets", 0.33 * em,
112                                       gui.Margins(em, 0, 0, 0))
113        self._label = gui.Label("Lorem ipsum dolor")
114        self._label.text_color = gui.Color(1.0, 0.5, 0.0)
115        collapse.add_child(self._label)
116
117        # Create a checkbox. Checking or unchecking would usually be used to set
118        # a binary property, but in this case it will show a simple message box,
119        # which illustrates how to create simple dialogs.
120        cb = gui.Checkbox("Enable some really cool effect")
121        cb.set_on_checked(self._on_cb)  # set the callback function
122        collapse.add_child(cb)
123
124        # Create a color editor. We will change the color of the orange label
125        # above when the color changes.
126        color = gui.ColorEdit()
127        color.color_value = self._label.text_color
128        color.set_on_value_changed(self._on_color)
129        collapse.add_child(color)
130
131        # This is a combobox, nothing fancy here, just set a simple function to
132        # handle the user selecting an item.
133        combo = gui.Combobox()
134        combo.add_item("Show point labels")
135        combo.add_item("Show point velocity")
136        combo.add_item("Show bounding boxes")
137        combo.set_on_selection_changed(self._on_combo)
138        collapse.add_child(combo)
139
140        # This is a toggle switch, which is similar to a checkbox. To my way of
141        # thinking the difference is subtle: a checkbox toggles properties
142        # (for example, purely visual changes like enabling lighting) while a
143        # toggle switch is better for changing the behavior of the app (for
144        # example, turning on processing from the camera).
145        switch = gui.ToggleSwitch("Continuously update from camera")
146        switch.set_on_clicked(self._on_switch)
147        collapse.add_child(switch)
148
149        self.logo_idx = 0
150        proxy = gui.WidgetProxy()
151
152        def switch_proxy():
153            self.logo_idx += 1
154            if self.logo_idx % 3 == 0:
155                proxy.set_widget(None)
156            elif self.logo_idx % 3 == 1:
157                # Add a simple image
158                logo = gui.ImageWidget(basedir + "/icon-32.png")
159                proxy.set_widget(logo)
160            else:
161                label = gui.Label(
162                    'Open3D: A Modern Library for 3D Data Processing')
163                proxy.set_widget(label)
164            w.set_needs_layout()
165
166        logo_btn = gui.Button('Switch Logo By WidgetProxy')
167        logo_btn.vertical_padding_em = 0
168        logo_btn.background_color = gui.Color(r=0, b=0.5, g=0)
169        logo_btn.set_on_clicked(switch_proxy)
170        collapse.add_child(logo_btn)
171        collapse.add_child(proxy)
172
173        # Widget stack demo
174        self._widget_idx = 0
175        hz = gui.Horiz(spacing=5)
176        push_widget_btn = gui.Button('Push widget')
177        push_widget_btn.vertical_padding_em = 0
178        pop_widget_btn = gui.Button('Pop widget')
179        pop_widget_btn.vertical_padding_em = 0
180        stack = gui.WidgetStack()
181        stack.set_on_top(lambda w: print(f'New widget is: {w.text}'))
182        hz.add_child(gui.Label('WidgetStack '))
183        hz.add_child(push_widget_btn)
184        hz.add_child(pop_widget_btn)
185        hz.add_child(stack)
186        collapse.add_child(hz)
187
188        def push_widget():
189            self._widget_idx += 1
190            stack.push_widget(gui.Label(f'Widget {self._widget_idx}'))
191
192        push_widget_btn.set_on_clicked(push_widget)
193        pop_widget_btn.set_on_clicked(stack.pop_widget)
194
195        # Add a list of items
196        lv = gui.ListView()
197        lv.set_items(["Ground", "Trees", "Buildings", "Cars", "People", "Cats"])
198        lv.selected_index = lv.selected_index + 2  # initially is -1, so now 1
199        lv.set_max_visible_items(4)
200        lv.set_on_selection_changed(self._on_list)
201        collapse.add_child(lv)
202
203        # Add a tree view
204        tree = gui.TreeView()
205        tree.add_text_item(tree.get_root_item(), "Camera")
206        geo_id = tree.add_text_item(tree.get_root_item(), "Geometries")
207        mesh_id = tree.add_text_item(geo_id, "Mesh")
208        tree.add_text_item(mesh_id, "Triangles")
209        tree.add_text_item(mesh_id, "Albedo texture")
210        tree.add_text_item(mesh_id, "Normal map")
211        points_id = tree.add_text_item(geo_id, "Points")
212        tree.can_select_items_with_children = True
213        tree.set_on_selection_changed(self._on_tree)
214        # does not call on_selection_changed: user did not change selection
215        tree.selected_item = points_id
216        collapse.add_child(tree)
217
218        # Add two number editors, one for integers and one for floating point
219        # Number editor can clamp numbers to a range, although this is more
220        # useful for integers than for floating point.
221        intedit = gui.NumberEdit(gui.NumberEdit.INT)
222        intedit.int_value = 0
223        intedit.set_limits(1, 19)  # value coerced to 1
224        intedit.int_value = intedit.int_value + 2  # value should be 3
225        doubleedit = gui.NumberEdit(gui.NumberEdit.DOUBLE)
226        numlayout = gui.Horiz()
227        numlayout.add_child(gui.Label("int"))
228        numlayout.add_child(intedit)
229        numlayout.add_fixed(em)  # manual spacing (could set it in Horiz() ctor)
230        numlayout.add_child(gui.Label("double"))
231        numlayout.add_child(doubleedit)
232        collapse.add_child(numlayout)
233
234        # Create a progress bar. It ranges from 0.0 to 1.0.
235        self._progress = gui.ProgressBar()
236        self._progress.value = 0.25  # 25% complete
237        self._progress.value = self._progress.value + 0.08  # 0.25 + 0.08 = 33%
238        prog_layout = gui.Horiz(em)
239        prog_layout.add_child(gui.Label("Progress..."))
240        prog_layout.add_child(self._progress)
241        collapse.add_child(prog_layout)
242
243        # Create a slider. It acts very similar to NumberEdit except that the
244        # user moves a slider and cannot type the number.
245        slider = gui.Slider(gui.Slider.INT)
246        slider.set_limits(5, 13)
247        slider.set_on_value_changed(self._on_slider)
248        collapse.add_child(slider)
249
250        # Create a text editor. The placeholder text (if not empty) will be
251        # displayed when there is no text, as concise help, or visible tooltip.
252        tedit = gui.TextEdit()
253        tedit.placeholder_text = "Edit me some text here"
254
255        # on_text_changed fires whenever the user changes the text (but not if
256        # the text_value property is assigned to).
257        tedit.set_on_text_changed(self._on_text_changed)
258
259        # on_value_changed fires whenever the user signals that they are finished
260        # editing the text, either by pressing return or by clicking outside of
261        # the text editor, thus losing text focus.
262        tedit.set_on_value_changed(self._on_value_changed)
263        collapse.add_child(tedit)
264
265        # Create a widget for showing/editing a 3D vector
266        vedit = gui.VectorEdit()
267        vedit.vector_value = [1, 2, 3]
268        vedit.set_on_value_changed(self._on_vedit)
269        collapse.add_child(vedit)
270
271        # Create a VGrid layout. This layout specifies the number of columns
272        # (two, in this case), and will place the first child in the first
273        # column, the second in the second, the third in the first, the fourth
274        # in the second, etc.
275        # So:
276        #      2 cols             3 cols                  4 cols
277        #   |  1  |  2  |   |  1  |  2  |  3  |   |  1  |  2  |  3  |  4  |
278        #   |  3  |  4  |   |  4  |  5  |  6  |   |  5  |  6  |  7  |  8  |
279        #   |  5  |  6  |   |  7  |  8  |  9  |   |  9  | 10  | 11  | 12  |
280        #   |    ...    |   |       ...       |   |         ...           |
281        vgrid = gui.VGrid(2)
282        vgrid.add_child(gui.Label("Trees"))
283        vgrid.add_child(gui.Label("12 items"))
284        vgrid.add_child(gui.Label("People"))
285        vgrid.add_child(gui.Label("2 (93% certainty)"))
286        vgrid.add_child(gui.Label("Cars"))
287        vgrid.add_child(gui.Label("5 (87% certainty)"))
288        collapse.add_child(vgrid)
289
290        # Create a tab control. This is really a set of N layouts on top of each
291        # other, but with only one selected.
292        tabs = gui.TabControl()
293        tab1 = gui.Vert()
294        tab1.add_child(gui.Checkbox("Enable option 1"))
295        tab1.add_child(gui.Checkbox("Enable option 2"))
296        tab1.add_child(gui.Checkbox("Enable option 3"))
297        tabs.add_tab("Options", tab1)
298        tab2 = gui.Vert()
299        tab2.add_child(gui.Label("No plugins detected"))
300        tab2.add_stretch()
301        tabs.add_tab("Plugins", tab2)
302        tab3 = gui.RadioButton(gui.RadioButton.VERT)
303        tab3.set_items(["Apple", "Orange"])
304
305        def vt_changed(idx):
306            print(f"current cargo: {tab3.selected_value}")
307
308        tab3.set_on_selection_changed(vt_changed)
309        tabs.add_tab("Cargo", tab3)
310        tab4 = gui.RadioButton(gui.RadioButton.HORIZ)
311        tab4.set_items(["Air plane", "Train", "Bus"])
312
313        def hz_changed(idx):
314            print(f"current traffic plan: {tab4.selected_value}")
315
316        tab4.set_on_selection_changed(hz_changed)
317        tabs.add_tab("Traffic", tab4)
318        collapse.add_child(tabs)
319
320        # Quit button. (Typically this is a menu item)
321        button_layout = gui.Horiz()
322        ok_button = gui.Button("Ok")
323        ok_button.set_on_clicked(self._on_ok)
324        button_layout.add_stretch()
325        button_layout.add_child(ok_button)
326
327        layout.add_child(collapse)
328        layout.add_child(button_layout)
329
330        # We're done, set the window's layout
331        w.add_child(layout)
332
333    def _on_filedlg_button(self):
334        filedlg = gui.FileDialog(gui.FileDialog.OPEN, "Select file",
335                                 self.window.theme)
336        filedlg.add_filter(".obj .ply .stl", "Triangle mesh (.obj, .ply, .stl)")
337        filedlg.add_filter("", "All files")
338        filedlg.set_on_cancel(self._on_filedlg_cancel)
339        filedlg.set_on_done(self._on_filedlg_done)
340        self.window.show_dialog(filedlg)
341
342    def _on_filedlg_cancel(self):
343        self.window.close_dialog()
344
345    def _on_filedlg_done(self, path):
346        self._fileedit.text_value = path
347        self.window.close_dialog()
348
349    def _on_cb(self, is_checked):
350        if is_checked:
351            text = "Sorry, effects are unimplemented"
352        else:
353            text = "Good choice"
354
355        self.show_message_dialog("There might be a problem...", text)
356
357    def _on_switch(self, is_on):
358        if is_on:
359            print("Camera would now be running")
360        else:
361            print("Camera would now be off")
362
363    # This function is essentially the same as window.show_message_box(),
364    # so for something this simple just use that, but it illustrates making a
365    # dialog.
366    def show_message_dialog(self, title, message):
367        # A Dialog is just a widget, so you make its child a layout just like
368        # a Window.
369        dlg = gui.Dialog(title)
370
371        # Add the message text
372        em = self.window.theme.font_size
373        dlg_layout = gui.Vert(em, gui.Margins(em, em, em, em))
374        dlg_layout.add_child(gui.Label(message))
375
376        # Add the Ok button. We need to define a callback function to handle
377        # the click.
378        ok_button = gui.Button("Ok")
379        ok_button.set_on_clicked(self._on_dialog_ok)
380
381        # We want the Ok button to be an the right side, so we need to add
382        # a stretch item to the layout, otherwise the button will be the size
383        # of the entire row. A stretch item takes up as much space as it can,
384        # which forces the button to be its minimum size.
385        button_layout = gui.Horiz()
386        button_layout.add_stretch()
387        button_layout.add_child(ok_button)
388
389        # Add the button layout,
390        dlg_layout.add_child(button_layout)
391        # ... then add the layout as the child of the Dialog
392        dlg.add_child(dlg_layout)
393        # ... and now we can show the dialog
394        self.window.show_dialog(dlg)
395
396    def _on_dialog_ok(self):
397        self.window.close_dialog()
398
399    def _on_color(self, new_color):
400        self._label.text_color = new_color
401
402    def _on_combo(self, new_val, new_idx):
403        print(new_idx, new_val)
404
405    def _on_list(self, new_val, is_dbl_click):
406        print(new_val)
407
408    def _on_tree(self, new_item_id):
409        print(new_item_id)
410
411    def _on_slider(self, new_val):
412        self._progress.value = new_val / 20.0
413
414    def _on_text_changed(self, new_text):
415        print("edit:", new_text)
416
417    def _on_value_changed(self, new_text):
418        print("value:", new_text)
419
420    def _on_vedit(self, new_val):
421        print(new_val)
422
423    def _on_ok(self):
424        gui.Application.instance.quit()
425
426    def _on_menu_checkable(self):
427        gui.Application.instance.menubar.set_checked(
428            ExampleWindow.MENU_CHECKABLE,
429            not gui.Application.instance.menubar.is_checked(
430                ExampleWindow.MENU_CHECKABLE))
431
432    def _on_menu_quit(self):
433        gui.Application.instance.quit()
434
435
436# This class is essentially the same as window.show_message_box(),
437# so for something this simple just use that, but it illustrates making a
438# dialog.
439class MessageBox:
440
441    def __init__(self, title, message):
442        self._window = None
443
444        # A Dialog is just a widget, so you make its child a layout just like
445        # a Window.
446        dlg = gui.Dialog(title)
447
448        # Add the message text
449        em = self.window.theme.font_size
450        dlg_layout = gui.Vert(em, gui.Margins(em, em, em, em))
451        dlg_layout.add_child(gui.Label(message))
452
453        # Add the Ok button. We need to define a callback function to handle
454        # the click.
455        ok_button = gui.Button("Ok")
456        ok_button.set_on_clicked(self._on_ok)
457
458        # We want the Ok button to be an the right side, so we need to add
459        # a stretch item to the layout, otherwise the button will be the size
460        # of the entire row. A stretch item takes up as much space as it can,
461        # which forces the button to be its minimum size.
462        button_layout = gui.Horiz()
463        button_layout.add_stretch()
464        button_layout.add_child(ok_button)
465
466        # Add the button layout,
467        dlg_layout.add_child(button_layout)
468        # ... then add the layout as the child of the Dialog
469        dlg.add_child(dlg_layout)
470
471    def show(self, window):
472        self._window = window
473
474    def _on_ok(self):
475        self._window.close_dialog()
476
477
478def main():
479    # We need to initialize the application, which finds the necessary shaders for
480    # rendering and prepares the cross-platform window abstraction.
481    gui.Application.instance.initialize()
482
483    w = ExampleWindow()
484
485    # Run the event loop. This will not return until the last window is closed.
486    gui.Application.instance.run()
487
488
489if __name__ == "__main__":
490    main()

customized_visualization.py

 27import os
 28import open3d as o3d
 29import numpy as np
 30import matplotlib.pyplot as plt
 31
 32pyexample_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 33test_data_path = os.path.join(os.path.dirname(pyexample_path), 'test_data')
 34
 35
 36def custom_draw_geometry(pcd):
 37    # The following code achieves the same effect as:
 38    # o3d.visualization.draw_geometries([pcd])
 39    vis = o3d.visualization.Visualizer()
 40    vis.create_window()
 41    vis.add_geometry(pcd)
 42    vis.run()
 43    vis.destroy_window()
 44
 45
 46def custom_draw_geometry_with_custom_fov(pcd, fov_step):
 47    vis = o3d.visualization.Visualizer()
 48    vis.create_window()
 49    vis.add_geometry(pcd)
 50    ctr = vis.get_view_control()
 51    print("Field of view (before changing) %.2f" % ctr.get_field_of_view())
 52    ctr.change_field_of_view(step=fov_step)
 53    print("Field of view (after changing) %.2f" % ctr.get_field_of_view())
 54    vis.run()
 55    vis.destroy_window()
 56
 57
 58def custom_draw_geometry_with_rotation(pcd):
 59
 60    def rotate_view(vis):
 61        ctr = vis.get_view_control()
 62        ctr.rotate(10.0, 0.0)
 63        return False
 64
 65    o3d.visualization.draw_geometries_with_animation_callback([pcd],
 66                                                              rotate_view)
 67
 68
 69def custom_draw_geometry_load_option(pcd, render_option_path):
 70    vis = o3d.visualization.Visualizer()
 71    vis.create_window()
 72    vis.add_geometry(pcd)
 73    vis.get_render_option().load_from_json(render_option_path)
 74    vis.run()
 75    vis.destroy_window()
 76
 77
 78def custom_draw_geometry_with_key_callback(pcd, render_option_path):
 79
 80    def change_background_to_black(vis):
 81        opt = vis.get_render_option()
 82        opt.background_color = np.asarray([0, 0, 0])
 83        return False
 84
 85    def load_render_option(vis):
 86        vis.get_render_option().load_from_json(render_option_path)
 87        return False
 88
 89    def capture_depth(vis):
 90        depth = vis.capture_depth_float_buffer()
 91        plt.imshow(np.asarray(depth))
 92        plt.show()
 93        return False
 94
 95    def capture_image(vis):
 96        image = vis.capture_screen_float_buffer()
 97        plt.imshow(np.asarray(image))
 98        plt.show()
 99        return False
100
101    key_to_callback = {}
102    key_to_callback[ord("K")] = change_background_to_black
103    key_to_callback[ord("R")] = load_render_option
104    key_to_callback[ord(",")] = capture_depth
105    key_to_callback[ord(".")] = capture_image
106    o3d.visualization.draw_geometries_with_key_callbacks([pcd], key_to_callback)
107
108
109def custom_draw_geometry_with_camera_trajectory(pcd, render_option_path,
110                                                camera_trajectory_path):
111    custom_draw_geometry_with_camera_trajectory.index = -1
112    custom_draw_geometry_with_camera_trajectory.trajectory =\
113        o3d.io.read_pinhole_camera_trajectory(camera_trajectory_path)
114    custom_draw_geometry_with_camera_trajectory.vis = o3d.visualization.Visualizer(
115    )
116    image_path = os.path.join(test_data_path, 'image')
117    if not os.path.exists(image_path):
118        os.makedirs(image_path)
119    depth_path = os.path.join(test_data_path, 'depth')
120    if not os.path.exists(depth_path):
121        os.makedirs(depth_path)
122
123    def move_forward(vis):
124        # This function is called within the o3d.visualization.Visualizer::run() loop
125        # The run loop calls the function, then re-render
126        # So the sequence in this function is to:
127        # 1. Capture frame
128        # 2. index++, check ending criteria
129        # 3. Set camera
130        # 4. (Re-render)
131        ctr = vis.get_view_control()
132        glb = custom_draw_geometry_with_camera_trajectory
133        if glb.index >= 0:
134            print("Capture image {:05d}".format(glb.index))
135            depth = vis.capture_depth_float_buffer(False)
136            image = vis.capture_screen_float_buffer(False)
137            plt.imsave(os.path.join(depth_path, '{:05d}.png'.format(glb.index)),
138                       np.asarray(depth),
139                       dpi=1)
140            plt.imsave(os.path.join(image_path, '{:05d}.png'.format(glb.index)),
141                       np.asarray(image),
142                       dpi=1)
143            # vis.capture_depth_image("depth/{:05d}.png".format(glb.index), False)
144            # vis.capture_screen_image("image/{:05d}.png".format(glb.index), False)
145        glb.index = glb.index + 1
146        if glb.index < len(glb.trajectory.parameters):
147            ctr.convert_from_pinhole_camera_parameters(
148                glb.trajectory.parameters[glb.index], allow_arbitrary=True)
149        else:
150            custom_draw_geometry_with_camera_trajectory.vis.\
151                register_animation_callback(None)
152        return False
153
154    vis = custom_draw_geometry_with_camera_trajectory.vis
155    vis.create_window()
156    vis.add_geometry(pcd)
157    vis.get_render_option().load_from_json(render_option_path)
158    vis.register_animation_callback(move_forward)
159    vis.run()
160    vis.destroy_window()
161
162
163if __name__ == "__main__":
164    sample_data = o3d.data.DemoCustomVisualization()
165    pcd_flipped = o3d.io.read_point_cloud(sample_data.point_cloud_path)
166    # Flip it, otherwise the pointcloud will be upside down
167    pcd_flipped.transform([[1, 0, 0, 0], [0, -1, 0, 0], [0, 0, -1, 0],
168                           [0, 0, 0, 1]])
169
170    print("1. Customized visualization to mimic DrawGeometry")
171    custom_draw_geometry(pcd_flipped)
172
173    print("2. Changing field of view")
174    custom_draw_geometry_with_custom_fov(pcd_flipped, 90.0)
175    custom_draw_geometry_with_custom_fov(pcd_flipped, -90.0)
176
177    print("3. Customized visualization with a rotating view")
178    custom_draw_geometry_with_rotation(pcd_flipped)
179
180    print("4. Customized visualization showing normal rendering")
181    custom_draw_geometry_load_option(pcd_flipped,
182                                     sample_data.render_option_path)
183
184    print("5. Customized visualization with key press callbacks")
185    print("   Press 'K' to change background color to black")
186    print("   Press 'R' to load a customized render option, showing normals")
187    print("   Press ',' to capture the depth buffer and show it")
188    print("   Press '.' to capture the screen and show it")
189    custom_draw_geometry_with_key_callback(pcd_flipped,
190                                           sample_data.render_option_path)
191
192    pcd = o3d.io.read_point_cloud(sample_data.point_cloud_path)
193    print("6. Customized visualization playing a camera trajectory")
194    custom_draw_geometry_with_camera_trajectory(
195        pcd, sample_data.render_option_path, sample_data.camera_trajectory_path)

customized_visualization_key_action.py

27import open3d as o3d
28
29
30def custom_key_action_without_kb_repeat_delay(pcd):
31    rotating = False
32
33    vis = o3d.visualization.VisualizerWithKeyCallback()
34
35    def key_action_callback(vis, action, mods):
36        nonlocal rotating
37        print(action)
38        if action == 1:  # key down
39            rotating = True
40        elif action == 0:  # key up
41            rotating = False
42        elif action == 2:  # key repeat
43            pass
44        return True
45
46    def animation_callback(vis):
47        nonlocal rotating
48        if rotating:
49            ctr = vis.get_view_control()
50            ctr.rotate(10.0, 0.0)
51
52    # key_action_callback will be triggered when there's a keyboard press, release or repeat event
53    vis.register_key_action_callback(32, key_action_callback)  # space
54
55    # animation_callback is always repeatedly called by the visualizer
56    vis.register_animation_callback(animation_callback)
57
58    vis.create_window()
59    vis.add_geometry(pcd)
60    vis.run()
61
62
63if __name__ == "__main__":
64    ply_data = o3d.data.PLYPointCloud()
65    pcd = o3d.io.read_point_cloud(ply_data.path)
66
67    print(
68        "Customized visualization with smooth key action (without keyboard repeat delay)"
69    )
70    custom_key_action_without_kb_repeat_delay(pcd)

demo_scene.py

 27
 28import math
 29import numpy as np
 30import os
 31import open3d as o3d
 32import open3d.visualization as vis
 33
 34
 35def convert_material_record(mat_record):
 36    mat = vis.Material('defaultLit')
 37    # Convert scalar parameters
 38    mat.vector_properties['base_color'] = mat_record.base_color
 39    mat.scalar_properties['metallic'] = mat_record.base_metallic
 40    mat.scalar_properties['roughness'] = mat_record.base_roughness
 41    mat.scalar_properties['reflectance'] = mat_record.base_reflectance
 42    mat.texture_maps['albedo'] = o3d.t.geometry.Image.from_legacy(
 43        mat_record.albedo_img)
 44    mat.texture_maps['normal'] = o3d.t.geometry.Image.from_legacy(
 45        mat_record.normal_img)
 46    mat.texture_maps['ao_rough_metal'] = o3d.t.geometry.Image.from_legacy(
 47        mat_record.ao_rough_metal_img)
 48    return mat
 49
 50
 51def create_scene():
 52    '''
 53    Creates the geometry and materials for the demo scene and returns a dictionary suitable for draw call
 54    '''
 55    # Create some shapes for our scene
 56    a_cube = o3d.geometry.TriangleMesh.create_box(2,
 57                                                  4,
 58                                                  4,
 59                                                  create_uv_map=True,
 60                                                  map_texture_to_each_face=True)
 61    a_cube.compute_triangle_normals()
 62    a_cube.translate((-5, 0, -2))
 63    a_cube = o3d.t.geometry.TriangleMesh.from_legacy(a_cube)
 64
 65    a_sphere = o3d.geometry.TriangleMesh.create_sphere(2.5,
 66                                                       resolution=40,
 67                                                       create_uv_map=True)
 68    a_sphere.compute_vertex_normals()
 69    rotate_90 = o3d.geometry.get_rotation_matrix_from_xyz((-math.pi / 2, 0, 0))
 70    a_sphere.rotate(rotate_90)
 71    a_sphere.translate((5, 2.4, 0))
 72    a_sphere = o3d.t.geometry.TriangleMesh.from_legacy(a_sphere)
 73
 74    a_cylinder = o3d.geometry.TriangleMesh.create_cylinder(
 75        1.0, 4.0, 30, 4, True)
 76    a_cylinder.compute_triangle_normals()
 77    a_cylinder.rotate(rotate_90)
 78    a_cylinder.translate((10, 2, 0))
 79    a_cylinder = o3d.t.geometry.TriangleMesh.from_legacy(a_cylinder)
 80
 81    a_ico = o3d.geometry.TriangleMesh.create_icosahedron(1.25,
 82                                                         create_uv_map=True)
 83    a_ico.compute_triangle_normals()
 84    a_ico.translate((-10, 2, 0))
 85    a_ico = o3d.t.geometry.TriangleMesh.from_legacy(a_ico)
 86
 87    # Load an OBJ model for our scene
 88    helmet_data = o3d.data.FlightHelmetModel()
 89    helmet = o3d.io.read_triangle_model(helmet_data.path)
 90    helmet_parts = []
 91    for m in helmet.meshes:
 92        # m.mesh.paint_uniform_color((1.0, 0.75, 0.3))
 93        m.mesh.scale(10.0, (0.0, 0.0, 0.0))
 94        helmet_parts.append(m)
 95
 96    # Create a ground plane
 97    ground_plane = o3d.geometry.TriangleMesh.create_box(
 98        50.0, 0.1, 50.0, create_uv_map=True, map_texture_to_each_face=True)
 99    ground_plane.compute_triangle_normals()
100    rotate_180 = o3d.geometry.get_rotation_matrix_from_xyz((-math.pi, 0, 0))
101    ground_plane.rotate(rotate_180)
102    ground_plane.translate((-25.0, -0.1, -25.0))
103    ground_plane.paint_uniform_color((1, 1, 1))
104    ground_plane = o3d.t.geometry.TriangleMesh.from_legacy(ground_plane)
105
106    # Material to make ground plane more interesting - a rough piece of glass
107    ground_plane.material = vis.Material("defaultLitSSR")
108    ground_plane.material.scalar_properties['roughness'] = 0.15
109    ground_plane.material.scalar_properties['reflectance'] = 0.72
110    ground_plane.material.scalar_properties['transmission'] = 0.6
111    ground_plane.material.scalar_properties['thickness'] = 0.3
112    ground_plane.material.scalar_properties['absorption_distance'] = 0.1
113    ground_plane.material.vector_properties['absorption_color'] = np.array(
114        [0.82, 0.98, 0.972, 1.0])
115    painted_plaster_texture_data = o3d.data.PaintedPlasterTexture()
116    ground_plane.material.texture_maps['albedo'] = o3d.t.io.read_image(
117        painted_plaster_texture_data.albedo_texture_path)
118    ground_plane.material.texture_maps['normal'] = o3d.t.io.read_image(
119        painted_plaster_texture_data.normal_texture_path)
120    ground_plane.material.texture_maps['roughness'] = o3d.t.io.read_image(
121        painted_plaster_texture_data.roughness_texture_path)
122
123    # Load textures and create materials for each of our demo items
124    wood_floor_texture_data = o3d.data.WoodFloorTexture()
125    a_cube.material = vis.Material('defaultLit')
126    a_cube.material.texture_maps['albedo'] = o3d.t.io.read_image(
127        wood_floor_texture_data.albedo_texture_path)
128    a_cube.material.texture_maps['normal'] = o3d.t.io.read_image(
129        wood_floor_texture_data.normal_texture_path)
130    a_cube.material.texture_maps['roughness'] = o3d.t.io.read_image(
131        wood_floor_texture_data.roughness_texture_path)
132
133    tiles_texture_data = o3d.data.TilesTexture()
134    a_sphere.material = vis.Material('defaultLit')
135    a_sphere.material.texture_maps['albedo'] = o3d.t.io.read_image(
136        tiles_texture_data.albedo_texture_path)
137    a_sphere.material.texture_maps['normal'] = o3d.t.io.read_image(
138        tiles_texture_data.normal_texture_path)
139    a_sphere.material.texture_maps['roughness'] = o3d.t.io.read_image(
140        tiles_texture_data.roughness_texture_path)
141
142    terrazzo_texture_data = o3d.data.TerrazzoTexture()
143    a_ico.material = vis.Material('defaultLit')
144    a_ico.material.texture_maps['albedo'] = o3d.t.io.read_image(
145        terrazzo_texture_data.albedo_texture_path)
146    a_ico.material.texture_maps['normal'] = o3d.t.io.read_image(
147        terrazzo_texture_data.normal_texture_path)
148    a_ico.material.texture_maps['roughness'] = o3d.t.io.read_image(
149        terrazzo_texture_data.roughness_texture_path)
150
151    metal_texture_data = o3d.data.MetalTexture()
152    a_cylinder.material = vis.Material('defaultLit')
153    a_cylinder.material.texture_maps['albedo'] = o3d.t.io.read_image(
154        metal_texture_data.albedo_texture_path)
155    a_cylinder.material.texture_maps['normal'] = o3d.t.io.read_image(
156        metal_texture_data.normal_texture_path)
157    a_cylinder.material.texture_maps['roughness'] = o3d.t.io.read_image(
158        metal_texture_data.roughness_texture_path)
159    a_cylinder.material.texture_maps['metallic'] = o3d.t.io.read_image(
160        metal_texture_data.metallic_texture_path)
161
162    geoms = [{
163        "name": "plane",
164        "geometry": ground_plane
165    }, {
166        "name": "cube",
167        "geometry": a_cube
168    }, {
169        "name": "cylinder",
170        "geometry": a_cylinder
171    }, {
172        "name": "ico",
173        "geometry": a_ico
174    }, {
175        "name": "sphere",
176        "geometry": a_sphere
177    }]
178    # Load the helmet
179    for part in helmet_parts:
180        name = part.mesh_name
181        tgeom = o3d.t.geometry.TriangleMesh.from_legacy(part.mesh)
182        tgeom.material = convert_material_record(
183            helmet.materials[part.material_idx])
184        geoms.append({"name": name, "geometry": tgeom})
185    return geoms
186
187
188if __name__ == "__main__":
189    geoms = create_scene()
190    vis.draw(geoms,
191             bg_color=(0.8, 0.9, 0.9, 1.0),
192             show_ui=True,
193             width=1920,
194             height=1080)

draw.py

 27import math
 28import numpy as np
 29import open3d as o3d
 30import open3d.visualization as vis
 31import os
 32import random
 33
 34pyexample_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 35test_data_path = os.path.join(os.path.dirname(pyexample_path), 'test_data')
 36
 37
 38def normalize(v):
 39    a = 1.0 / math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2])
 40    return (a * v[0], a * v[1], a * v[2])
 41
 42
 43def make_point_cloud(npts, center, radius, colorize):
 44    pts = np.random.uniform(-radius, radius, size=[npts, 3]) + center
 45    cloud = o3d.geometry.PointCloud()
 46    cloud.points = o3d.utility.Vector3dVector(pts)
 47    if colorize:
 48        colors = np.random.uniform(0.0, 1.0, size=[npts, 3])
 49        cloud.colors = o3d.utility.Vector3dVector(colors)
 50    return cloud
 51
 52
 53def single_object():
 54    # No colors, no normals, should appear unlit black
 55    cube = o3d.geometry.TriangleMesh.create_box(1, 2, 4)
 56    vis.draw(cube)
 57
 58
 59def multi_objects():
 60    pc_rad = 1.0
 61    pc_nocolor = make_point_cloud(100, (0, -2, 0), pc_rad, False)
 62    pc_color = make_point_cloud(100, (3, -2, 0), pc_rad, True)
 63    r = 0.4
 64    sphere_unlit = o3d.geometry.TriangleMesh.create_sphere(r)
 65    sphere_unlit.translate((0, 1, 0))
 66    sphere_colored_unlit = o3d.geometry.TriangleMesh.create_sphere(r)
 67    sphere_colored_unlit.paint_uniform_color((1.0, 0.0, 0.0))
 68    sphere_colored_unlit.translate((2, 1, 0))
 69    sphere_lit = o3d.geometry.TriangleMesh.create_sphere(r)
 70    sphere_lit.compute_vertex_normals()
 71    sphere_lit.translate((4, 1, 0))
 72    sphere_colored_lit = o3d.geometry.TriangleMesh.create_sphere(r)
 73    sphere_colored_lit.compute_vertex_normals()
 74    sphere_colored_lit.paint_uniform_color((0.0, 1.0, 0.0))
 75    sphere_colored_lit.translate((6, 1, 0))
 76    big_bbox = o3d.geometry.AxisAlignedBoundingBox((-pc_rad, -3, -pc_rad),
 77                                                   (6.0 + r, 1.0 + r, pc_rad))
 78    big_bbox.color = (0.0, 0.0, 0.0)
 79    sphere_bbox = sphere_unlit.get_axis_aligned_bounding_box()
 80    sphere_bbox.color = (1.0, 0.5, 0.0)
 81    lines = o3d.geometry.LineSet.create_from_axis_aligned_bounding_box(
 82        sphere_lit.get_axis_aligned_bounding_box())
 83    lines.paint_uniform_color((0.0, 1.0, 0.0))
 84    lines_colored = o3d.geometry.LineSet.create_from_axis_aligned_bounding_box(
 85        sphere_colored_lit.get_axis_aligned_bounding_box())
 86    lines_colored.paint_uniform_color((0.0, 0.0, 1.0))
 87
 88    vis.draw([
 89        pc_nocolor, pc_color, sphere_unlit, sphere_colored_unlit, sphere_lit,
 90        sphere_colored_lit, big_bbox, sphere_bbox, lines, lines_colored
 91    ])
 92
 93
 94def actions():
 95    SOURCE_NAME = "Source"
 96    RESULT_NAME = "Result (Poisson reconstruction)"
 97    TRUTH_NAME = "Ground truth"
 98
 99    bunny = o3d.data.BunnyMesh()
100    bunny_mesh = o3d.io.read_triangle_mesh(bunny.path)
101    bunny_mesh.compute_vertex_normals()
102
103    bunny_mesh.paint_uniform_color((1, 0.75, 0))
104    bunny_mesh.compute_vertex_normals()
105    cloud = o3d.geometry.PointCloud()
106    cloud.points = bunny_mesh.vertices
107    cloud.normals = bunny_mesh.vertex_normals
108
109    def make_mesh(o3dvis):
110        # TODO: call o3dvis.get_geometry instead of using bunny_mesh
111        mesh, _ = o3d.geometry.TriangleMesh.create_from_point_cloud_poisson(
112            cloud)
113        mesh.paint_uniform_color((1, 1, 1))
114        mesh.compute_vertex_normals()
115        o3dvis.add_geometry({"name": RESULT_NAME, "geometry": mesh})
116        o3dvis.show_geometry(SOURCE_NAME, False)
117
118    def toggle_result(o3dvis):
119        truth_vis = o3dvis.get_geometry(TRUTH_NAME).is_visible
120        o3dvis.show_geometry(TRUTH_NAME, not truth_vis)
121        o3dvis.show_geometry(RESULT_NAME, truth_vis)
122
123    vis.draw([{
124        "name": SOURCE_NAME,
125        "geometry": cloud
126    }, {
127        "name": TRUTH_NAME,
128        "geometry": bunny_mesh,
129        "is_visible": False
130    }],
131             actions=[("Create Mesh", make_mesh),
132                      ("Toggle truth/result", toggle_result)])
133
134
135def get_icp_transform(source, target, source_indices, target_indices):
136    corr = np.zeros((len(source_indices), 2))
137    corr[:, 0] = source_indices
138    corr[:, 1] = target_indices
139
140    # Estimate rough transformation using correspondences
141    p2p = o3d.pipelines.registration.TransformationEstimationPointToPoint()
142    trans_init = p2p.compute_transformation(source, target,
143                                            o3d.utility.Vector2iVector(corr))
144
145    # Point-to-point ICP for refinement
146    threshold = 0.03  # 3cm distance threshold
147    reg_p2p = o3d.pipelines.registration.registration_icp(
148        source, target, threshold, trans_init,
149        o3d.pipelines.registration.TransformationEstimationPointToPoint())
150
151    return reg_p2p.transformation
152
153
154def selections():
155    pcd_fragments_data = o3d.data.DemoICPPointClouds()
156    source = o3d.io.read_point_cloud(pcd_fragments_data.paths[0])
157    target = o3d.io.read_point_cloud(pcd_fragments_data.paths[1])
158    source.paint_uniform_color([1, 0.706, 0])
159    target.paint_uniform_color([0, 0.651, 0.929])
160
161    source_name = "Source (yellow)"
162    target_name = "Target (blue)"
163
164    def do_icp_one_set(o3dvis):
165        # sets: [name: [{ "index": int, "order": int, "point": (x, y, z)}, ...],
166        #        ...]
167        sets = o3dvis.get_selection_sets()
168        source_picked = sorted(list(sets[0][source_name]),
169                               key=lambda x: x.order)
170        target_picked = sorted(list(sets[0][target_name]),
171                               key=lambda x: x.order)
172        source_indices = [idx.index for idx in source_picked]
173        target_indices = [idx.index for idx in target_picked]
174
175        t = get_icp_transform(source, target, source_indices, target_indices)
176        source.transform(t)
177
178        # Update the source geometry
179        o3dvis.remove_geometry(source_name)
180        o3dvis.add_geometry({"name": source_name, "geometry": source})
181
182    def do_icp_two_sets(o3dvis):
183        sets = o3dvis.get_selection_sets()
184        source_set = sets[0][source_name]
185        target_set = sets[1][target_name]
186        source_picked = sorted(list(source_set), key=lambda x: x.order)
187        target_picked = sorted(list(target_set), key=lambda x: x.order)
188        source_indices = [idx.index for idx in source_picked]
189        target_indices = [idx.index for idx in target_picked]
190
191        t = get_icp_transform(source, target, source_indices, target_indices)
192        source.transform(t)
193
194        # Update the source geometry
195        o3dvis.remove_geometry(source_name)
196        o3dvis.add_geometry({"name": source_name, "geometry": source})
197
198    vis.draw([{
199        "name": source_name,
200        "geometry": source
201    }, {
202        "name": target_name,
203        "geometry": target
204    }],
205             actions=[("ICP Registration (one set)", do_icp_one_set),
206                      ("ICP Registration (two sets)", do_icp_two_sets)],
207             show_ui=True)
208
209
210def time_animation():
211    orig = make_point_cloud(200, (0, 0, 0), 1.0, True)
212    clouds = [{"name": "t=0", "geometry": orig, "time": 0}]
213    drift_dir = (1.0, 0.0, 0.0)
214    expand = 1.0
215    n = 20
216    for i in range(1, n):
217        amount = float(i) / float(n - 1)
218        cloud = o3d.geometry.PointCloud()
219        pts = np.asarray(orig.points)
220        pts = pts * (1.0 + amount * expand) + [amount * v for v in drift_dir]
221        cloud.points = o3d.utility.Vector3dVector(pts)
222        cloud.colors = orig.colors
223        clouds.append({
224            "name": "points at t=" + str(i),
225            "geometry": cloud,
226            "time": i
227        })
228
229    vis.draw(clouds)
230
231
232def groups():
233    building_mat = vis.rendering.MaterialRecord()
234    building_mat.shader = "defaultLit"
235    building_mat.base_color = (1.0, .90, .75, 1.0)
236    building_mat.base_reflectance = 0.1
237    midrise_mat = vis.rendering.MaterialRecord()
238    midrise_mat.shader = "defaultLit"
239    midrise_mat.base_color = (.475, .450, .425, 1.0)
240    midrise_mat.base_reflectance = 0.1
241    skyscraper_mat = vis.rendering.MaterialRecord()
242    skyscraper_mat.shader = "defaultLit"
243    skyscraper_mat.base_color = (.05, .20, .55, 1.0)
244    skyscraper_mat.base_reflectance = 0.9
245    skyscraper_mat.base_roughness = 0.01
246
247    buildings = []
248    size = 10.0
249    half = size / 2.0
250    min_height = 1.0
251    max_height = 20.0
252    for z in range(0, 10):
253        for x in range(0, 10):
254            max_h = max_height * (1.0 - abs(half - x) / half) * (
255                1.0 - abs(half - z) / half)
256            h = random.uniform(min_height, max(max_h, min_height + 1.0))
257            box = o3d.geometry.TriangleMesh.create_box(0.9, h, 0.9)
258            box.compute_triangle_normals()
259            box.translate((x + 0.05, 0.0, z + 0.05))
260            if h > 0.333 * max_height:
261                mat = skyscraper_mat
262            elif h > 0.1 * max_height:
263                mat = midrise_mat
264            else:
265                mat = building_mat
266            buildings.append({
267                "name": "building_" + str(x) + "_" + str(z),
268                "geometry": box,
269                "material": mat,
270                "group": "buildings"
271            })
272
273    haze = make_point_cloud(5000, (half, 0.333 * max_height, half),
274                            1.414 * half, False)
275    haze.paint_uniform_color((0.8, 0.8, 0.8))
276
277    smog = make_point_cloud(10000, (half, 0.25 * max_height, half), 1.2 * half,
278                            False)
279    smog.paint_uniform_color((0.95, 0.85, 0.75))
280
281    vis.draw(buildings + [{
282        "name": "haze",
283        "geometry": haze,
284        "group": "haze"
285    }, {
286        "name": "smog",
287        "geometry": smog,
288        "group": "smog"
289    }])
290
291
292def remove():
293
294    def make_sphere(name, center, color, group, time):
295        sphere = o3d.geometry.TriangleMesh.create_sphere(0.5)
296        sphere.compute_vertex_normals()
297        sphere.translate(center)
298
299        mat = vis.rendering.Material()
300        mat.shader = "defaultLit"
301        mat.base_color = color
302
303        return {
304            "name": name,
305            "geometry": sphere,
306            "material": mat,
307            "group": group,
308            "time": time
309        }
310
311    red = make_sphere("red", (0, 0, 0), (1.0, 0.0, 0.0, 1.0), "spheres", 0)
312    green = make_sphere("green", (2, 0, 0), (0.0, 1.0, 0.0, 1.0), "spheres", 0)
313    blue = make_sphere("blue", (4, 0, 0), (0.0, 0.0, 1.0, 1.0), "spheres", 0)
314    yellow = make_sphere("yellow", (0, 0, 0), (1.0, 1.0, 0.0, 1.0), "spheres",
315                         1)
316    bbox = {
317        "name": "bbox",
318        "geometry": red["geometry"].get_axis_aligned_bounding_box()
319    }
320
321    def remove_green(visdraw):
322        visdraw.remove_geometry("green")
323
324    def remove_yellow(visdraw):
325        visdraw.remove_geometry("yellow")
326
327    def remove_bbox(visdraw):
328        visdraw.remove_geometry("bbox")
329
330    vis.draw([red, green, blue, yellow, bbox],
331             actions=[("Remove Green", remove_green),
332                      ("Remove Yellow", remove_yellow),
333                      ("Remove Bounds", remove_bbox)])
334
335
336def main():
337    single_object()
338    multi_objects()
339    actions()
340    selections()
341
342
343if __name__ == "__main__":
344    main()

draw_webrtc.py

27import open3d as o3d
28
29if __name__ == "__main__":
30    o3d.visualization.webrtc_server.enable_webrtc()
31    cube_red = o3d.geometry.TriangleMesh.create_box(1, 2, 4)
32    cube_red.compute_vertex_normals()
33    cube_red.paint_uniform_color((1.0, 0.0, 0.0))
34    o3d.visualization.draw(cube_red)

headless_rendering.py

 27import os
 28import open3d as o3d
 29import numpy as np
 30import matplotlib.pyplot as plt
 31
 32
 33def custom_draw_geometry_with_camera_trajectory(pcd, camera_trajectory_path,
 34                                                render_option_path,
 35                                                output_path):
 36    custom_draw_geometry_with_camera_trajectory.index = -1
 37    custom_draw_geometry_with_camera_trajectory.trajectory =\
 38        o3d.io.read_pinhole_camera_trajectory(camera_trajectory_path)
 39    custom_draw_geometry_with_camera_trajectory.vis = o3d.visualization.Visualizer(
 40    )
 41    image_path = os.path.join(output_path, 'image')
 42    if not os.path.exists(image_path):
 43        os.makedirs(image_path)
 44    depth_path = os.path.join(output_path, 'depth')
 45    if not os.path.exists(depth_path):
 46        os.makedirs(depth_path)
 47
 48    print("Saving color images in " + image_path)
 49    print("Saving depth images in " + depth_path)
 50
 51    def move_forward(vis):
 52        # This function is called within the o3d.visualization.Visualizer::run() loop
 53        # The run loop calls the function, then re-render
 54        # So the sequence in this function is to:
 55        # 1. Capture frame
 56        # 2. index++, check ending criteria
 57        # 3. Set camera
 58        # 4. (Re-render)
 59        ctr = vis.get_view_control()
 60        glb = custom_draw_geometry_with_camera_trajectory
 61        if glb.index >= 0:
 62            print("Capture image {:05d}".format(glb.index))
 63            # Capture and save image using Open3D.
 64            vis.capture_depth_image(
 65                os.path.join(depth_path, "{:05d}.png".format(glb.index)), False)
 66            vis.capture_screen_image(
 67                os.path.join(image_path, "{:05d}.png".format(glb.index)), False)
 68
 69            # Example to save image using matplotlib.
 70            '''
 71            depth = vis.capture_depth_float_buffer()
 72            image = vis.capture_screen_float_buffer()
 73            plt.imsave(os.path.join(depth_path, "{:05d}.png".format(glb.index)),
 74                       np.asarray(depth),
 75                       dpi=1)
 76            plt.imsave(os.path.join(image_path, "{:05d}.png".format(glb.index)),
 77                       np.asarray(image),
 78                       dpi=1)
 79            '''
 80
 81        glb.index = glb.index + 1
 82        if glb.index < len(glb.trajectory.parameters):
 83            ctr.convert_from_pinhole_camera_parameters(
 84                glb.trajectory.parameters[glb.index])
 85        else:
 86            custom_draw_geometry_with_camera_trajectory.vis.destroy_window()
 87
 88        # Return false as we don't need to call UpdateGeometry()
 89        return False
 90
 91    vis = custom_draw_geometry_with_camera_trajectory.vis
 92    vis.create_window()
 93    vis.add_geometry(pcd)
 94    vis.get_render_option().load_from_json(render_option_path)
 95    vis.register_animation_callback(move_forward)
 96    vis.run()
 97
 98
 99if __name__ == "__main__":
100    if not o3d._build_config['ENABLE_HEADLESS_RENDERING']:
101        print("Headless rendering is not enabled. "
102              "Please rebuild Open3D with ENABLE_HEADLESS_RENDERING=ON")
103        exit(1)
104
105    sample_data = o3d.data.DemoCustomVisualization()
106    pcd = o3d.io.read_point_cloud(sample_data.point_cloud_path)
107    print("Customized visualization playing a camera trajectory. "
108          "Press ctrl+z to terminate.")
109    custom_draw_geometry_with_camera_trajectory(
110        pcd, sample_data.camera_trajectory_path, sample_data.render_option_path,
111        'HeadlessRenderingOutput')

interactive_visualization.py

 27# examples/python/visualization/interactive_visualization.py
 28
 29import numpy as np
 30import copy
 31import open3d as o3d
 32
 33
 34def demo_crop_geometry():
 35    print("Demo for manual geometry cropping")
 36    print(
 37        "1) Press 'Y' twice to align geometry with negative direction of y-axis"
 38    )
 39    print("2) Press 'K' to lock screen and to switch to selection mode")
 40    print("3) Drag for rectangle selection,")
 41    print("   or use ctrl + left click for polygon selection")
 42    print("4) Press 'C' to get a selected geometry")
 43    print("5) Press 'S' to save the selected geometry")
 44    print("6) Press 'F' to switch to freeview mode")
 45    pcd_data = o3d.data.DemoICPPointClouds()
 46    pcd = o3d.io.read_point_cloud(pcd_data.paths[0])
 47    o3d.visualization.draw_geometries_with_editing([pcd])
 48
 49
 50def draw_registration_result(source, target, transformation):
 51    source_temp = copy.deepcopy(source)
 52    target_temp = copy.deepcopy(target)
 53    source_temp.paint_uniform_color([1, 0.706, 0])
 54    target_temp.paint_uniform_color([0, 0.651, 0.929])
 55    source_temp.transform(transformation)
 56    o3d.visualization.draw_geometries([source_temp, target_temp])
 57
 58
 59def pick_points(pcd):
 60    print("")
 61    print(
 62        "1) Please pick at least three correspondences using [shift + left click]"
 63    )
 64    print("   Press [shift + right click] to undo point picking")
 65    print("2) After picking points, press 'Q' to close the window")
 66    vis = o3d.visualization.VisualizerWithEditing()
 67    vis.create_window()
 68    vis.add_geometry(pcd)
 69    vis.run()  # user picks points
 70    vis.destroy_window()
 71    print("")
 72    return vis.get_picked_points()
 73
 74
 75def demo_manual_registration():
 76    print("Demo for manual ICP")
 77    pcd_data = o3d.data.DemoICPPointClouds()
 78    source = o3d.io.read_point_cloud(pcd_data.paths[0])
 79    target = o3d.io.read_point_cloud(pcd_data.paths[2])
 80    print("Visualization of two point clouds before manual alignment")
 81    draw_registration_result(source, target, np.identity(4))
 82
 83    # pick points from two point clouds and builds correspondences
 84    picked_id_source = pick_points(source)
 85    picked_id_target = pick_points(target)
 86    assert (len(picked_id_source) >= 3 and len(picked_id_target) >= 3)
 87    assert (len(picked_id_source) == len(picked_id_target))
 88    corr = np.zeros((len(picked_id_source), 2))
 89    corr[:, 0] = picked_id_source
 90    corr[:, 1] = picked_id_target
 91
 92    # estimate rough transformation using correspondences
 93    print("Compute a rough transform using the correspondences given by user")
 94    p2p = o3d.pipelines.registration.TransformationEstimationPointToPoint()
 95    trans_init = p2p.compute_transformation(source, target,
 96                                            o3d.utility.Vector2iVector(corr))
 97
 98    # point-to-point ICP for refinement
 99    print("Perform point-to-point ICP refinement")
100    threshold = 0.03  # 3cm distance threshold
101    reg_p2p = o3d.pipelines.registration.registration_icp(
102        source, target, threshold, trans_init,
103        o3d.pipelines.registration.TransformationEstimationPointToPoint())
104    draw_registration_result(source, target, reg_p2p.transformation)
105    print("")
106
107
108if __name__ == "__main__":
109    demo_crop_geometry()
110    demo_manual_registration()

line_width.py

27import open3d as o3d
28import random
29
30NUM_LINES = 10
31
32
33def random_point():
34    return [5 * random.random(), 5 * random.random(), 5 * random.random()]
35
36
37def main():
38    pts = [random_point() for _ in range(0, 2 * NUM_LINES)]
39    line_indices = [[2 * i, 2 * i + 1] for i in range(0, NUM_LINES)]
40    colors = [[0.0, 0.0, 0.0] for _ in range(0, NUM_LINES)]
41
42    lines = o3d.geometry.LineSet()
43    lines.points = o3d.utility.Vector3dVector(pts)
44    lines.lines = o3d.utility.Vector2iVector(line_indices)
45    # The default color of the lines is white, which will be invisible on the
46    # default white background. So we either need to set the color of the lines
47    # or the base_color of the material.
48    lines.colors = o3d.utility.Vector3dVector(colors)
49
50    # Some platforms do not require OpenGL implementations to support wide lines,
51    # so the renderer requires a custom shader to implement this: "unlitLine".
52    # The line_width field is only used by this shader; all other shaders ignore
53    # it.
54    mat = o3d.visualization.rendering.MaterialRecord()
55    mat.shader = "unlitLine"
56    mat.line_width = 10  # note that this is scaled with respect to pixels,
57    # so will give different results depending on the
58    # scaling values of your system
59    o3d.visualization.draw({
60        "name": "lines",
61        "geometry": lines,
62        "material": mat
63    })
64
65
66if __name__ == "__main__":
67    main()

load_save_viewpoint.py

27import open3d as o3d
28
29
30def save_view_point(pcd, filename):
31    vis = o3d.visualization.Visualizer()
32    vis.create_window()
33    vis.add_geometry(pcd)
34    vis.run()  # user changes the view and press "q" to terminate
35    param = vis.get_view_control().convert_to_pinhole_camera_parameters()
36    o3d.io.write_pinhole_camera_parameters(filename, param)
37    vis.destroy_window()
38
39
40def load_view_point(pcd, filename):
41    vis = o3d.visualization.Visualizer()
42    vis.create_window()
43    ctr = vis.get_view_control()
44    param = o3d.io.read_pinhole_camera_parameters(filename)
45    vis.add_geometry(pcd)
46    ctr.convert_from_pinhole_camera_parameters(param)
47    vis.run()
48    vis.destroy_window()
49
50
51if __name__ == "__main__":
52    pcd_data = o3d.data.PCDPointCloud()
53    pcd = o3d.io.read_point_cloud(pcd_data.path)
54    save_view_point(pcd, "viewpoint.json")
55    load_view_point(pcd, "viewpoint.json")

mouse_and_point_coord.py

 27import numpy as np
 28import open3d as o3d
 29import open3d.visualization.gui as gui
 30import open3d.visualization.rendering as rendering
 31
 32
 33# This example displays a point cloud and if you Ctrl-click on a point
 34# (Cmd-click on macOS) it will show the coordinates of the point.
 35# This example illustrates:
 36# - custom mouse handling on SceneWidget
 37# - getting a the depth value of a point (OpenGL depth)
 38# - converting from a window point + OpenGL depth to world coordinate
 39class ExampleApp:
 40
 41    def __init__(self, cloud):
 42        # We will create a SceneWidget that fills the entire window, and then
 43        # a label in the lower left on top of the SceneWidget to display the
 44        # coordinate.
 45        app = gui.Application.instance
 46        self.window = app.create_window("Open3D - GetCoord Example", 1024, 768)
 47        # Since we want the label on top of the scene, we cannot use a layout,
 48        # so we need to manually layout the window's children.
 49        self.window.set_on_layout(self._on_layout)
 50        self.widget3d = gui.SceneWidget()
 51        self.window.add_child(self.widget3d)
 52        self.info = gui.Label("")
 53        self.info.visible = False
 54        self.window.add_child(self.info)
 55
 56        self.widget3d.scene = rendering.Open3DScene(self.window.renderer)
 57
 58        mat = rendering.MaterialRecord()
 59        mat.shader = "defaultUnlit"
 60        # Point size is in native pixels, but "pixel" means different things to
 61        # different platforms (macOS, in particular), so multiply by Window scale
 62        # factor.
 63        mat.point_size = 3 * self.window.scaling
 64        self.widget3d.scene.add_geometry("Point Cloud", cloud, mat)
 65
 66        bounds = self.widget3d.scene.bounding_box
 67        center = bounds.get_center()
 68        self.widget3d.setup_camera(60, bounds, center)
 69        self.widget3d.look_at(center, center - [0, 0, 3], [0, -1, 0])
 70
 71        self.widget3d.set_on_mouse(self._on_mouse_widget3d)
 72
 73    def _on_layout(self, layout_context):
 74        r = self.window.content_rect
 75        self.widget3d.frame = r
 76        pref = self.info.calc_preferred_size(layout_context,
 77                                             gui.Widget.Constraints())
 78        self.info.frame = gui.Rect(r.x,
 79                                   r.get_bottom() - pref.height, pref.width,
 80                                   pref.height)
 81
 82    def _on_mouse_widget3d(self, event):
 83        # We could override BUTTON_DOWN without a modifier, but that would
 84        # interfere with manipulating the scene.
 85        if event.type == gui.MouseEvent.Type.BUTTON_DOWN and event.is_modifier_down(
 86                gui.KeyModifier.CTRL):
 87
 88            def depth_callback(depth_image):
 89                # Coordinates are expressed in absolute coordinates of the
 90                # window, but to dereference the image correctly we need them
 91                # relative to the origin of the widget. Note that even if the
 92                # scene widget is the only thing in the window, if a menubar
 93                # exists it also takes up space in the window (except on macOS).
 94                x = event.x - self.widget3d.frame.x
 95                y = event.y - self.widget3d.frame.y
 96                # Note that np.asarray() reverses the axes.
 97                depth = np.asarray(depth_image)[y, x]
 98
 99                if depth == 1.0:  # clicked on nothing (i.e. the far plane)
100                    text = ""
101                else:
102                    world = self.widget3d.scene.camera.unproject(
103                        event.x, event.y, depth, self.widget3d.frame.width,
104                        self.widget3d.frame.height)
105                    text = "({:.3f}, {:.3f}, {:.3f})".format(
106                        world[0], world[1], world[2])
107
108                # This is not called on the main thread, so we need to
109                # post to the main thread to safely access UI items.
110                def update_label():
111                    self.info.text = text
112                    self.info.visible = (text != "")
113                    # We are sizing the info label to be exactly the right size,
114                    # so since the text likely changed width, we need to
115                    # re-layout to set the new frame.
116                    self.window.set_needs_layout()
117
118                gui.Application.instance.post_to_main_thread(
119                    self.window, update_label)
120
121            self.widget3d.scene.scene.render_to_depth_image(depth_callback)
122            return gui.Widget.EventCallbackResult.HANDLED
123        return gui.Widget.EventCallbackResult.IGNORED
124
125
126def main():
127    app = gui.Application.instance
128    app.initialize()
129
130    # This example will also work with a triangle mesh, or any 3D object.
131    # If you use a triangle mesh you will probably want to set the material
132    # shader to "defaultLit" instead of "defaultUnlit".
133    pcd_data = o3d.data.DemoICPPointClouds()
134    cloud = o3d.io.read_point_cloud(pcd_data.paths[0])
135    ex = ExampleApp(cloud)
136
137    app.run()
138
139
140if __name__ == "__main__":
141    main()

multiple_windows.py

 27import numpy as np
 28import open3d as o3d
 29import threading
 30import time
 31
 32CLOUD_NAME = "points"
 33
 34
 35def main():
 36    MultiWinApp().run()
 37
 38
 39class MultiWinApp:
 40
 41    def __init__(self):
 42        self.is_done = False
 43        self.n_snapshots = 0
 44        self.cloud = None
 45        self.main_vis = None
 46        self.snapshot_pos = None
 47
 48    def run(self):
 49        app = o3d.visualization.gui.Application.instance
 50        app.initialize()
 51
 52        self.main_vis = o3d.visualization.O3DVisualizer(
 53            "Open3D - Multi-Window Demo")
 54        self.main_vis.add_action("Take snapshot in new window",
 55                                 self.on_snapshot)
 56        self.main_vis.set_on_close(self.on_main_window_closing)
 57
 58        app.add_window(self.main_vis)
 59        self.snapshot_pos = (self.main_vis.os_frame.x, self.main_vis.os_frame.y)
 60
 61        threading.Thread(target=self.update_thread).start()
 62
 63        app.run()
 64
 65    def on_snapshot(self, vis):
 66        self.n_snapshots += 1
 67        self.snapshot_pos = (self.snapshot_pos[0] + 50,
 68                             self.snapshot_pos[1] + 50)
 69        title = "Open3D - Multi-Window Demo (Snapshot #" + str(
 70            self.n_snapshots) + ")"
 71        new_vis = o3d.visualization.O3DVisualizer(title)
 72        mat = o3d.visualization.rendering.MaterialRecord()
 73        mat.shader = "defaultUnlit"
 74        new_vis.add_geometry(CLOUD_NAME + " #" + str(self.n_snapshots),
 75                             self.cloud, mat)
 76        new_vis.reset_camera_to_default()
 77        bounds = self.cloud.get_axis_aligned_bounding_box()
 78        extent = bounds.get_extent()
 79        new_vis.setup_camera(60, bounds.get_center(),
 80                             bounds.get_center() + [0, 0, -3], [0, -1, 0])
 81        o3d.visualization.gui.Application.instance.add_window(new_vis)
 82        new_vis.os_frame = o3d.visualization.gui.Rect(self.snapshot_pos[0],
 83                                                      self.snapshot_pos[1],
 84                                                      new_vis.os_frame.width,
 85                                                      new_vis.os_frame.height)
 86
 87    def on_main_window_closing(self):
 88        self.is_done = True
 89        return True  # False would cancel the close
 90
 91    def update_thread(self):
 92        # This is NOT the UI thread, need to call post_to_main_thread() to update
 93        # the scene or any part of the UI.
 94        pcd_data = o3d.data.DemoICPPointClouds()
 95        self.cloud = o3d.io.read_point_cloud(pcd_data.paths[0])
 96        bounds = self.cloud.get_axis_aligned_bounding_box()
 97        extent = bounds.get_extent()
 98
 99        def add_first_cloud():
100            mat = o3d.visualization.rendering.MaterialRecord()
101            mat.shader = "defaultUnlit"
102            self.main_vis.add_geometry(CLOUD_NAME, self.cloud, mat)
103            self.main_vis.reset_camera_to_default()
104            self.main_vis.setup_camera(60, bounds.get_center(),
105                                       bounds.get_center() + [0, 0, -3],
106                                       [0, -1, 0])
107
108        o3d.visualization.gui.Application.instance.post_to_main_thread(
109            self.main_vis, add_first_cloud)
110
111        while not self.is_done:
112            time.sleep(0.1)
113
114            # Perturb the cloud with a random walk to simulate an actual read
115            pts = np.asarray(self.cloud.points)
116            magnitude = 0.005 * extent
117            displacement = magnitude * (np.random.random_sample(pts.shape) -
118                                        0.5)
119            new_pts = pts + displacement
120            self.cloud.points = o3d.utility.Vector3dVector(new_pts)
121
122            def update_cloud():
123                # Note: if the number of points is less than or equal to the
124                #       number of points in the original object that was added,
125                #       using self.scene.update_geometry() will be faster.
126                #       Requires that the point cloud be a t.PointCloud.
127                self.main_vis.remove_geometry(CLOUD_NAME)
128                mat = o3d.visualization.rendering.MaterialRecord()
129                mat.shader = "defaultUnlit"
130                self.main_vis.add_geometry(CLOUD_NAME, self.cloud, mat)
131
132            if self.is_done:  # might have changed while sleeping
133                break
134            o3d.visualization.gui.Application.instance.post_to_main_thread(
135                self.main_vis, update_cloud)
136
137
138if __name__ == "__main__":
139    main()

non_blocking_visualization.py

27# examples/python/visualization/non_blocking_visualization.py
28
29import open3d as o3d
30import numpy as np
31
32if __name__ == "__main__":
33    o3d.utility.set_verbosity_level(o3d.utility.VerbosityLevel.Debug)
34    pcd_data = o3d.data.DemoICPPointClouds()
35    source_raw = o3d.io.read_point_cloud(pcd_data.paths[0])
36    target_raw = o3d.io.read_point_cloud(pcd_data.paths[1])
37
38    source = source_raw.voxel_down_sample(voxel_size=0.02)
39    target = target_raw.voxel_down_sample(voxel_size=0.02)
40    trans = [[0.862, 0.011, -0.507, 0.0], [-0.139, 0.967, -0.215, 0.7],
41             [0.487, 0.255, 0.835, -1.4], [0.0, 0.0, 0.0, 1.0]]
42    source.transform(trans)
43
44    flip_transform = [[1, 0, 0, 0], [0, -1, 0, 0], [0, 0, -1, 0], [0, 0, 0, 1]]
45    source.transform(flip_transform)
46    target.transform(flip_transform)
47
48    vis = o3d.visualization.Visualizer()
49    vis.create_window()
50    vis.add_geometry(source)
51    vis.add_geometry(target)
52    threshold = 0.05
53    icp_iteration = 100
54    save_image = False
55
56    for i in range(icp_iteration):
57        reg_p2l = o3d.pipelines.registration.registration_icp(
58            source, target, threshold, np.identity(4),
59            o3d.pipelines.registration.TransformationEstimationPointToPlane(),
60            o3d.pipelines.registration.ICPConvergenceCriteria(max_iteration=1))
61        source.transform(reg_p2l.transformation)
62        vis.update_geometry(source)
63        vis.poll_events()
64        vis.update_renderer()
65        if save_image:
66            vis.capture_screen_image("temp_%04d.jpg" % i)
67    vis.destroy_window()
68    o3d.utility.set_verbosity_level(o3d.utility.VerbosityLevel.Info)

non_english.py

 27import open3d.visualization.gui as gui
 28import os.path
 29import platform
 30
 31basedir = os.path.dirname(os.path.realpath(__file__))
 32
 33# This is all-widgets.py with some modifications for non-English languages.
 34# Please see all-widgets.py for usage of the GUI widgets
 35
 36MODE_SERIF = "serif"
 37MODE_COMMON_HANYU = "common"
 38MODE_SERIF_AND_COMMON_HANYU = "serif+common"
 39MODE_COMMON_HANYU_EN = "hanyu_en+common"
 40MODE_ALL_HANYU = "all"
 41MODE_CUSTOM_CHARS = "custom"
 42
 43#mode = MODE_SERIF
 44#mode = MODE_COMMON_HANYU
 45mode = MODE_SERIF_AND_COMMON_HANYU
 46#mode = MODE_ALL_HANYU
 47#mode = MODE_CUSTOM_CHARS
 48
 49# Fonts can be names or paths
 50if platform.system() == "Darwin":
 51    serif = "Times New Roman"
 52    hanzi = "STHeiti Light"
 53    chess = "/System/Library/Fonts/Apple Symbols.ttf"
 54elif platform.system() == "Windows":
 55    # it is necessary to specify paths on Windows since it stores its fonts
 56    # with a cryptic name, so font name searches do not work on Windows
 57    serif = "c:/windows/fonts/times.ttf"  # Times New Roman
 58    hanzi = "c:/windows/fonts/msyh.ttc"  # YaHei UI
 59    chess = "c:/windows/fonts/seguisym.ttf"  # Segoe UI Symbol
 60else:
 61    # Assumes Ubuntu 18.04
 62    serif = "DejaVuSerif"
 63    hanzi = "NotoSansCJK"
 64    chess = "/usr/share/fonts/truetype/freefont/FreeSerif.ttf"
 65
 66
 67def main():
 68    gui.Application.instance.initialize()
 69
 70    # Font changes must be done after initialization but before creating
 71    # a window.
 72
 73    # MODE_SERIF changes the English font; Chinese will not be displayed
 74    font = None
 75    if mode == MODE_SERIF:
 76        font = gui.FontDescription(serif)
 77    # MODE_COMMON_HANYU uses the default English font and adds common Chinese
 78    elif mode == MODE_COMMON_HANYU:
 79        font = gui.FontDescription()
 80        font.add_typeface_for_language(hanzi, "zh")
 81    # MODE_SERIF_AND_COMMON_HANYU uses a serif English font and adds common
 82    # Chinese characters
 83    elif mode == MODE_SERIF_AND_COMMON_HANYU:
 84        font = gui.FontDescription(serif)
 85        font.add_typeface_for_language(hanzi, "zh")
 86    # MODE_COMMON_HANYU_EN the Chinese font for both English and the common
 87    # characters
 88    elif mode == MODE_COMMON_HANYU_EN:
 89        font = gui.FontDescription(hanzi)
 90        font.add_typeface_for_language(hanzi, "zh")
 91    # MODE_ALL_HANYU uses the default English font but includes all the Chinese
 92    # characters (which uses a substantial amount of memory)
 93    elif mode == MODE_ALL_HANYU:
 94        font = gui.FontDescription()
 95        font.add_typeface_for_language(hanzi, "zh_all")
 96    elif mode == MODE_CUSTOM_CHARS:
 97        range = [0x2654, 0x2655, 0x2656, 0x2657, 0x2658, 0x2659]
 98        font = gui.FontDescription()
 99        font.add_typeface_for_code_points(chess, range)
100
101    if font is not None:
102        gui.Application.instance.set_font(gui.Application.DEFAULT_FONT_ID, font)
103
104    w = ExampleWindow()
105    gui.Application.instance.run()
106
107
108class ExampleWindow:
109    MENU_CHECKABLE = 1
110    MENU_DISABLED = 2
111    MENU_QUIT = 3
112
113    def __init__(self):
114        self.window = gui.Application.instance.create_window("Test", 400, 768)
115        # self.window = gui.Application.instance.create_window("Test", 400, 768,
116        #                                                        x=50, y=100)
117        w = self.window  # for more concise code
118
119        # Rather than specifying sizes in pixels, which may vary in size based
120        # on the monitor, especially on macOS which has 220 dpi monitors, use
121        # the em-size. This way sizings will be proportional to the font size,
122        # which will create a more visually consistent size across platforms.
123        em = w.theme.font_size
124
125        # Widgets are laid out in layouts: gui.Horiz, gui.Vert,
126        # gui.CollapsableVert, and gui.VGrid. By nesting the layouts we can
127        # achieve complex designs. Usually we use a vertical layout as the
128        # topmost widget, since widgets tend to be organized from top to bottom.
129        # Within that, we usually have a series of horizontal layouts for each
130        # row.
131        layout = gui.Vert(0, gui.Margins(0.5 * em, 0.5 * em, 0.5 * em,
132                                         0.5 * em))
133
134        # Create the menu. The menu is global (because the macOS menu is global),
135        # so only create it once.
136        if gui.Application.instance.menubar is None:
137            menubar = gui.Menu()
138            test_menu = gui.Menu()
139            test_menu.add_item("An option", ExampleWindow.MENU_CHECKABLE)
140            test_menu.set_checked(ExampleWindow.MENU_CHECKABLE, True)
141            test_menu.add_item("Unavailable feature",
142                               ExampleWindow.MENU_DISABLED)
143            test_menu.set_enabled(ExampleWindow.MENU_DISABLED, False)
144            test_menu.add_separator()
145            test_menu.add_item("Quit", ExampleWindow.MENU_QUIT)
146            # On macOS the first menu item is the application menu item and will
147            # always be the name of the application (probably "Python"),
148            # regardless of what you pass in here. The application menu is
149            # typically where About..., Preferences..., and Quit go.
150            menubar.add_menu("Test", test_menu)
151            gui.Application.instance.menubar = menubar
152
153        # Each window needs to know what to do with the menu items, so we need
154        # to tell the window how to handle menu items.
155        w.set_on_menu_item_activated(ExampleWindow.MENU_CHECKABLE,
156                                     self._on_menu_checkable)
157        w.set_on_menu_item_activated(ExampleWindow.MENU_QUIT,
158                                     self._on_menu_quit)
159
160        # Create a file-chooser widget. One part will be a text edit widget for
161        # the filename and clicking on the button will let the user choose using
162        # the file dialog.
163        self._fileedit = gui.TextEdit()
164        filedlgbutton = gui.Button("...")
165        filedlgbutton.horizontal_padding_em = 0.5
166        filedlgbutton.vertical_padding_em = 0
167        filedlgbutton.set_on_clicked(self._on_filedlg_button)
168
169        # (Create the horizontal widget for the row. This will make sure the
170        # text editor takes up as much space as it can.)
171        fileedit_layout = gui.Horiz()
172        fileedit_layout.add_child(gui.Label("Model file"))
173        fileedit_layout.add_child(self._fileedit)
174        fileedit_layout.add_fixed(0.25 * em)
175        fileedit_layout.add_child(filedlgbutton)
176        # add to the top-level (vertical) layout
177        layout.add_child(fileedit_layout)
178
179        # Create a collapsible vertical widget, which takes up enough vertical
180        # space for all its children when open, but only enough for text when
181        # closed. This is useful for property pages, so the user can hide sets
182        # of properties they rarely use. All layouts take a spacing parameter,
183        # which is the spacinging between items in the widget, and a margins
184        # parameter, which specifies the spacing of the left, top, right,
185        # bottom margins. (This acts like the 'padding' property in CSS.)
186        collapse = gui.CollapsableVert("Widgets", 0.33 * em,
187                                       gui.Margins(em, 0, 0, 0))
188        if mode == MODE_CUSTOM_CHARS:
189            self._label = gui.Label("♔♕♖♗♘♙")
190        elif mode == MODE_ALL_HANYU:
191            self._label = gui.Label("天地玄黃,宇宙洪荒。日月盈昃,辰宿列張。")
192        else:
193            self._label = gui.Label("锄禾日当午,汗滴禾下土。谁知盘中餐,粒粒皆辛苦。")
194        self._label.text_color = gui.Color(1.0, 0.5, 0.0)
195        collapse.add_child(self._label)
196
197        # Create a checkbox. Checking or unchecking would usually be used to set
198        # a binary property, but in this case it will show a simple message box,
199        # which illustrates how to create simple dialogs.
200        cb = gui.Checkbox("Enable some really cool effect")
201        cb.set_on_checked(self._on_cb)  # set the callback function
202        collapse.add_child(cb)
203
204        # Create a color editor. We will change the color of the orange label
205        # above when the color changes.
206        color = gui.ColorEdit()
207        color.color_value = self._label.text_color
208        color.set_on_value_changed(self._on_color)
209        collapse.add_child(color)
210
211        # This is a combobox, nothing fancy here, just set a simple function to
212        # handle the user selecting an item.
213        combo = gui.Combobox()
214        combo.add_item("Show point labels")
215        combo.add_item("Show point velocity")
216        combo.add_item("Show bounding boxes")
217        combo.set_on_selection_changed(self._on_combo)
218        collapse.add_child(combo)
219
220        # Add a simple image
221        logo = gui.ImageWidget(basedir + "/icon-32.png")
222        collapse.add_child(logo)
223
224        # Add a list of items
225        lv = gui.ListView()
226        lv.set_items(["Ground", "Trees", "Buildings" "Cars", "People"])
227        lv.selected_index = lv.selected_index + 2  # initially is -1, so now 1
228        lv.set_on_selection_changed(self._on_list)
229        collapse.add_child(lv)
230
231        # Add a tree view
232        tree = gui.TreeView()
233        tree.add_text_item(tree.get_root_item(), "Camera")
234        geo_id = tree.add_text_item(tree.get_root_item(), "Geometries")
235        mesh_id = tree.add_text_item(geo_id, "Mesh")
236        tree.add_text_item(mesh_id, "Triangles")
237        tree.add_text_item(mesh_id, "Albedo texture")
238        tree.add_text_item(mesh_id, "Normal map")
239        points_id = tree.add_text_item(geo_id, "Points")
240        tree.can_select_items_with_children = True
241        tree.set_on_selection_changed(self._on_tree)
242        # does not call on_selection_changed: user did not change selection
243        tree.selected_item = points_id
244        collapse.add_child(tree)
245
246        # Add two number editors, one for integers and one for floating point
247        # Number editor can clamp numbers to a range, although this is more
248        # useful for integers than for floating point.
249        intedit = gui.NumberEdit(gui.NumberEdit.INT)
250        intedit.int_value = 0
251        intedit.set_limits(1, 19)  # value coerced to 1
252        intedit.int_value = intedit.int_value + 2  # value should be 3
253        doubleedit = gui.NumberEdit(gui.NumberEdit.DOUBLE)
254        numlayout = gui.Horiz()
255        numlayout.add_child(gui.Label("int"))
256        numlayout.add_child(intedit)
257        numlayout.add_fixed(em)  # manual spacing (could set it in Horiz() ctor)
258        numlayout.add_child(gui.Label("double"))
259        numlayout.add_child(doubleedit)
260        collapse.add_child(numlayout)
261
262        # Create a progress bar. It ranges from 0.0 to 1.0.
263        self._progress = gui.ProgressBar()
264        self._progress.value = 0.25  # 25% complete
265        self._progress.value = self._progress.value + 0.08  # 0.25 + 0.08 = 33%
266        prog_layout = gui.Horiz(em)
267        prog_layout.add_child(gui.Label("Progress..."))
268        prog_layout.add_child(self._progress)
269        collapse.add_child(prog_layout)
270
271        # Create a slider. It acts very similar to NumberEdit except that the
272        # user moves a slider and cannot type the number.
273        slider = gui.Slider(gui.Slider.INT)
274        slider.set_limits(5, 13)
275        slider.set_on_value_changed(self._on_slider)
276        collapse.add_child(slider)
277
278        # Create a text editor. The placeholder text (if not empty) will be
279        # displayed when there is no text, as concise help, or visible tooltip.
280        tedit = gui.TextEdit()
281        tedit.placeholder_text = "Edit me some text here"
282
283        # on_text_changed fires whenever the user changes the text (but not if
284        # the text_value property is assigned to).
285        tedit.set_on_text_changed(self._on_text_changed)
286
287        # on_value_changed fires whenever the user signals that they are finished
288        # editing the text, either by pressing return or by clicking outside of
289        # the text editor, thus losing text focus.
290        tedit.set_on_value_changed(self._on_value_changed)
291        collapse.add_child(tedit)
292
293        # Create a widget for showing/editing a 3D vector
294        vedit = gui.VectorEdit()
295        vedit.vector_value = [1, 2, 3]
296        vedit.set_on_value_changed(self._on_vedit)
297        collapse.add_child(vedit)
298
299        # Create a VGrid layout. This layout specifies the number of columns
300        # (two, in this case), and will place the first child in the first
301        # column, the second in the second, the third in the first, the fourth
302        # in the second, etc.
303        # So:
304        #      2 cols             3 cols                  4 cols
305        #   |  1  |  2  |   |  1  |  2  |  3  |   |  1  |  2  |  3  |  4  |
306        #   |  3  |  4  |   |  4  |  5  |  6  |   |  5  |  6  |  7  |  8  |
307        #   |  5  |  6  |   |  7  |  8  |  9  |   |  9  | 10  | 11  | 12  |
308        #   |    ...    |   |       ...       |   |         ...           |
309        vgrid = gui.VGrid(2)
310        vgrid.add_child(gui.Label("Trees"))
311        vgrid.add_child(gui.Label("12 items"))
312        vgrid.add_child(gui.Label("People"))
313        vgrid.add_child(gui.Label("2 (93% certainty)"))
314        vgrid.add_child(gui.Label("Cars"))
315        vgrid.add_child(gui.Label("5 (87% certainty)"))
316        collapse.add_child(vgrid)
317
318        # Create a tab control. This is really a set of N layouts on top of each
319        # other, but with only one selected.
320        tabs = gui.TabControl()
321        tab1 = gui.Vert()
322        tab1.add_child(gui.Checkbox("Enable option 1"))
323        tab1.add_child(gui.Checkbox("Enable option 2"))
324        tab1.add_child(gui.Checkbox("Enable option 3"))
325        tabs.add_tab("Options", tab1)
326        tab2 = gui.Vert()
327        tab2.add_child(gui.Label("No plugins detected"))
328        tab2.add_stretch()
329        tabs.add_tab("Plugins", tab2)
330        collapse.add_child(tabs)
331
332        # Quit button. (Typically this is a menu item)
333        button_layout = gui.Horiz()
334        ok_button = gui.Button("Ok")
335        ok_button.set_on_clicked(self._on_ok)
336        button_layout.add_stretch()
337        button_layout.add_child(ok_button)
338
339        layout.add_child(collapse)
340        layout.add_child(button_layout)
341
342        # We're done, set the window's layout
343        w.add_child(layout)
344
345    def _on_filedlg_button(self):
346        filedlg = gui.FileDialog(gui.FileDialog.OPEN, "Select file",
347                                 self.window.theme)
348        filedlg.add_filter(".obj .ply .stl", "Triangle mesh (.obj, .ply, .stl)")
349        filedlg.add_filter("", "All files")
350        filedlg.set_on_cancel(self._on_filedlg_cancel)
351        filedlg.set_on_done(self._on_filedlg_done)
352        self.window.show_dialog(filedlg)
353
354    def _on_filedlg_cancel(self):
355        self.window.close_dialog()
356
357    def _on_filedlg_done(self, path):
358        self._fileedit.text_value = path
359        self.window.close_dialog()
360
361    def _on_cb(self, is_checked):
362        if is_checked:
363            text = "Sorry, effects are unimplemented"
364        else:
365            text = "Good choice"
366
367        self.show_message_dialog("There might be a problem...", text)
368
369    # This function is essentially the same as window.show_message_box(),
370    # so for something this simple just use that, but it illustrates making a
371    # dialog.
372    def show_message_dialog(self, title, message):
373        # A Dialog is just a widget, so you make its child a layout just like
374        # a Window.
375        dlg = gui.Dialog(title)
376
377        # Add the message text
378        em = self.window.theme.font_size
379        dlg_layout = gui.Vert(em, gui.Margins(em, em, em, em))
380        dlg_layout.add_child(gui.Label(message))
381
382        # Add the Ok button. We need to define a callback function to handle
383        # the click.
384        ok_button = gui.Button("Ok")
385        ok_button.set_on_clicked(self._on_dialog_ok)
386
387        # We want the Ok button to be an the right side, so we need to add
388        # a stretch item to the layout, otherwise the button will be the size
389        # of the entire row. A stretch item takes up as much space as it can,
390        # which forces the button to be its minimum size.
391        button_layout = gui.Horiz()
392        button_layout.add_stretch()
393        button_layout.add_child(ok_button)
394
395        # Add the button layout,
396        dlg_layout.add_child(button_layout)
397        # ... then add the layout as the child of the Dialog
398        dlg.add_child(dlg_layout)
399        # ... and now we can show the dialog
400        self.window.show_dialog(dlg)
401
402    def _on_dialog_ok(self):
403        self.window.close_dialog()
404
405    def _on_color(self, new_color):
406        self._label.text_color = new_color
407
408    def _on_combo(self, new_val, new_idx):
409        print(new_idx, new_val)
410
411    def _on_list(self, new_val, is_dbl_click):
412        print(new_val)
413
414    def _on_tree(self, new_item_id):
415        print(new_item_id)
416
417    def _on_slider(self, new_val):
418        self._progress.value = new_val / 20.0
419
420    def _on_text_changed(self, new_text):
421        print("edit:", new_text)
422
423    def _on_value_changed(self, new_text):
424        print("value:", new_text)
425
426    def _on_vedit(self, new_val):
427        print(new_val)
428
429    def _on_ok(self):
430        gui.Application.instance.quit()
431
432    def _on_menu_checkable(self):
433        gui.Application.instance.menubar.set_checked(
434            ExampleWindow.MENU_CHECKABLE,
435            not gui.Application.instance.menubar.is_checked(
436                ExampleWindow.MENU_CHECKABLE))
437
438    def _on_menu_quit(self):
439        gui.Application.instance.quit()
440
441
442# This class is essentially the same as window.show_message_box(),
443# so for something this simple just use that, but it illustrates making a
444# dialog.
445class MessageBox:
446
447    def __init__(self, title, message):
448        self._window = None
449
450        # A Dialog is just a widget, so you make its child a layout just like
451        # a Window.
452        dlg = gui.Dialog(title)
453
454        # Add the message text
455        em = self.window.theme.font_size
456        dlg_layout = gui.Vert(em, gui.Margins(em, em, em, em))
457        dlg_layout.add_child(gui.Label(message))
458
459        # Add the Ok button. We need to define a callback function to handle
460        # the click.
461        ok_button = gui.Button("Ok")
462        ok_button.set_on_clicked(self._on_ok)
463
464        # We want the Ok button to be an the right side, so we need to add
465        # a stretch item to the layout, otherwise the button will be the size
466        # of the entire row. A stretch item takes up as much space as it can,
467        # which forces the button to be its minimum size.
468        button_layout = gui.Horiz()
469        button_layout.add_stretch()
470        button_layout.add_child(ok_button)
471
472        # Add the button layout,
473        dlg_layout.add_child(button_layout)
474        # ... then add the layout as the child of the Dialog
475        dlg.add_child(dlg_layout)
476
477    def show(self, window):
478        self._window = window
479
480    def _on_ok(self):
481        self._window.close_dialog()
482
483
484if __name__ == "__main__":
485    main()

online_processing.py

 27
 28- Connects to a RGBD camera or RGBD video file (currently
 29  RealSense camera and bag file format are supported).
 30- Captures / reads color and depth frames. Allow recording from camera.
 31- Convert frames to point cloud, optionally with normals.
 32- Visualize point cloud video and results.
 33- Save point clouds and RGBD images for selected frames.
 34
 35For this example, Open3D must be built with -DBUILD_LIBREALSENSE=ON
 36"""
 37
 38import os
 39import json
 40import time
 41import logging as log
 42import argparse
 43import threading
 44from datetime import datetime
 45from concurrent.futures import ThreadPoolExecutor
 46import numpy as np
 47import open3d as o3d
 48import open3d.visualization.gui as gui
 49import open3d.visualization.rendering as rendering
 50
 51
 52# Camera and processing
 53class PipelineModel:
 54    """Controls IO (camera, video file, recording, saving frames). Methods run
 55    in worker threads."""
 56
 57    def __init__(self,
 58                 update_view,
 59                 camera_config_file=None,
 60                 rgbd_video=None,
 61                 device=None):
 62        """Initialize.
 63
 64        Args:
 65            update_view (callback): Callback to update display elements for a
 66                frame.
 67            camera_config_file (str): Camera configuration json file.
 68            rgbd_video (str): RS bag file containing the RGBD video. If this is
 69                provided, connected cameras are ignored.
 70            device (str): Compute device (e.g.: 'cpu:0' or 'cuda:0').
 71        """
 72        self.update_view = update_view
 73        if device:
 74            self.device = device.lower()
 75        else:
 76            self.device = 'cuda:0' if o3d.core.cuda.is_available() else 'cpu:0'
 77        self.o3d_device = o3d.core.Device(self.device)
 78
 79        self.video = None
 80        self.camera = None
 81        self.flag_capture = False
 82        self.cv_capture = threading.Condition()  # condition variable
 83        self.recording = False  # Are we currently recording
 84        self.flag_record = False  # Request to start/stop recording
 85        if rgbd_video:  # Video file
 86            self.video = o3d.t.io.RGBDVideoReader.create(rgbd_video)
 87            self.rgbd_metadata = self.video.metadata
 88            self.status_message = f"Video {rgbd_video} opened."
 89
 90        else:  # RGBD camera
 91            now = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
 92            filename = f"{now}.bag"
 93            self.camera = o3d.t.io.RealSenseSensor()
 94            if camera_config_file:
 95                with open(camera_config_file) as ccf:
 96                    self.camera.init_sensor(o3d.t.io.RealSenseSensorConfig(
 97                        json.load(ccf)),
 98                                            filename=filename)
 99            else:
100                self.camera.init_sensor(filename=filename)
101            self.camera.start_capture(start_record=False)
102            self.rgbd_metadata = self.camera.get_metadata()
103            self.status_message = f"Camera {self.rgbd_metadata.serial_number} opened."
104
105        log.info(self.rgbd_metadata)
106
107        # RGBD -> PCD
108        self.extrinsics = o3d.core.Tensor.eye(4,
109                                              dtype=o3d.core.Dtype.Float32,
110                                              device=self.o3d_device)
111        self.intrinsic_matrix = o3d.core.Tensor(
112            self.rgbd_metadata.intrinsics.intrinsic_matrix,
113            dtype=o3d.core.Dtype.Float32,
114            device=self.o3d_device)
115        self.depth_max = 3.0  # m
116        self.pcd_stride = 2  # downsample point cloud, may increase frame rate
117        self.flag_normals = False
118        self.flag_save_rgbd = False
119        self.flag_save_pcd = False
120
121        self.pcd_frame = None
122        self.rgbd_frame = None
123        self.executor = ThreadPoolExecutor(max_workers=3,
124                                           thread_name_prefix='Capture-Save')
125        self.flag_exit = False
126
127    @property
128    def max_points(self):
129        """Max points in one frame for the camera or RGBD video resolution."""
130        return self.rgbd_metadata.width * self.rgbd_metadata.height
131
132    @property
133    def vfov(self):
134        """Camera or RGBD video vertical field of view."""
135        return np.rad2deg(2 * np.arctan(self.intrinsic_matrix[1, 2].item() /
136                                        self.intrinsic_matrix[1, 1].item()))
137
138    def run(self):
139        """Run pipeline."""
140        n_pts = 0
141        frame_id = 0
142        t1 = time.perf_counter()
143        if self.video:
144            self.rgbd_frame = self.video.next_frame()
145        else:
146            self.rgbd_frame = self.camera.capture_frame(
147                wait=True, align_depth_to_color=True)
148
149        pcd_errors = 0
150        while (not self.flag_exit and
151               (self.video is None or  # Camera
152                (self.video and not self.video.is_eof()))):  # Video
153            if self.video:
154                future_rgbd_frame = self.executor.submit(self.video.next_frame)
155            else:
156                future_rgbd_frame = self.executor.submit(
157                    self.camera.capture_frame,
158                    wait=True,
159                    align_depth_to_color=True)
160
161            if self.flag_save_pcd:
162                self.save_pcd()
163                self.flag_save_pcd = False
164            try:
165                self.rgbd_frame = self.rgbd_frame.to(self.o3d_device)
166                self.pcd_frame = o3d.t.geometry.PointCloud.create_from_rgbd_image(
167                    self.rgbd_frame, self.intrinsic_matrix, self.extrinsics,
168                    self.rgbd_metadata.depth_scale, self.depth_max,
169                    self.pcd_stride, self.flag_normals)
170                depth_in_color = self.rgbd_frame.depth.colorize_depth(
171                    self.rgbd_metadata.depth_scale, 0, self.depth_max)
172            except RuntimeError:
173                pcd_errors += 1
174
175            if self.pcd_frame.is_empty():
176                log.warning(f"No valid depth data in frame {frame_id})")
177                continue
178
179            n_pts += self.pcd_frame.point.positions.shape[0]
180            if frame_id % 60 == 0 and frame_id > 0:
181                t0, t1 = t1, time.perf_counter()
182                log.debug(f"\nframe_id = {frame_id}, \t {(t1-t0)*1000./60:0.2f}"
183                          f"ms/frame \t {(t1-t0)*1e9/n_pts} ms/Mp\t")
184                n_pts = 0
185            frame_elements = {
186                'color': self.rgbd_frame.color.cpu(),
187                'depth': depth_in_color.cpu(),
188                'pcd': self.pcd_frame.cpu(),
189                'status_message': self.status_message
190            }
191            self.update_view(frame_elements)
192
193            if self.flag_save_rgbd:
194                self.save_rgbd()
195                self.flag_save_rgbd = False
196            self.rgbd_frame = future_rgbd_frame.result()
197            with self.cv_capture:  # Wait for capture to be enabled
198                self.cv_capture.wait_for(
199                    predicate=lambda: self.flag_capture or self.flag_exit)
200            self.toggle_record()
201            frame_id += 1
202
203        if self.camera:
204            self.camera.stop_capture()
205        else:
206            self.video.close()
207        self.executor.shutdown()
208        log.debug(f"create_from_depth_image() errors = {pcd_errors}")
209
210    def toggle_record(self):
211        if self.camera is not None:
212            if self.flag_record and not self.recording:
213                self.camera.resume_record()
214                self.recording = True
215            elif not self.flag_record and self.recording:
216                self.camera.pause_record()
217                self.recording = False
218
219    def save_pcd(self):
220        """Save current point cloud."""
221        now = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
222        filename = f"{self.rgbd_metadata.serial_number}_pcd_{now}.ply"
223        # Convert colors to uint8 for compatibility
224        self.pcd_frame.point.colors = (self.pcd_frame.point.colors * 255).to(
225            o3d.core.Dtype.UInt8)
226        self.executor.submit(o3d.t.io.write_point_cloud,
227                             filename,
228                             self.pcd_frame,
229                             write_ascii=False,
230                             compressed=True,
231                             print_progress=False)
232        self.status_message = f"Saving point cloud to {filename}."
233
234    def save_rgbd(self):
235        """Save current RGBD image pair."""
236        now = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
237        filename = f"{self.rgbd_metadata.serial_number}_color_{now}.jpg"
238        self.executor.submit(o3d.t.io.write_image, filename,
239                             self.rgbd_frame.color)
240        filename = f"{self.rgbd_metadata.serial_number}_depth_{now}.png"
241        self.executor.submit(o3d.t.io.write_image, filename,
242                             self.rgbd_frame.depth)
243        self.status_message = (
244            f"Saving RGBD images to {filename[:-3]}.{{jpg,png}}.")
245
246
247class PipelineView:
248    """Controls display and user interface. All methods must run in the main thread."""
249
250    def __init__(self, vfov=60, max_pcd_vertices=1 << 20, **callbacks):
251        """Initialize.
252
253        Args:
254            vfov (float): Vertical field of view for the 3D scene.
255            max_pcd_vertices (int): Maximum point clud verties for which memory
256                is allocated.
257            callbacks (dict of kwargs): Callbacks provided by the controller
258                for various operations.
259        """
260
261        self.vfov = vfov
262        self.max_pcd_vertices = max_pcd_vertices
263
264        gui.Application.instance.initialize()
265        self.window = gui.Application.instance.create_window(
266            "Open3D || Online RGBD Video Processing", 1280, 960)
267        # Called on window layout (eg: resize)
268        self.window.set_on_layout(self.on_layout)
269        self.window.set_on_close(callbacks['on_window_close'])
270
271        self.pcd_material = o3d.visualization.rendering.MaterialRecord()
272        self.pcd_material.shader = "defaultLit"
273        # Set n_pixels displayed for each 3D point, accounting for HiDPI scaling
274        self.pcd_material.point_size = int(4 * self.window.scaling)
275
276        # 3D scene
277        self.pcdview = gui.SceneWidget()
278        self.window.add_child(self.pcdview)
279        self.pcdview.enable_scene_caching(
280            True)  # makes UI _much_ more responsive
281        self.pcdview.scene = rendering.Open3DScene(self.window.renderer)
282        self.pcdview.scene.set_background([1, 1, 1, 1])  # White background
283        self.pcdview.scene.set_lighting(
284            rendering.Open3DScene.LightingProfile.SOFT_SHADOWS, [0, -6, 0])
285        # Point cloud bounds, depends on the sensor range
286        self.pcd_bounds = o3d.geometry.AxisAlignedBoundingBox([-3, -3, 0],
287                                                              [3, 3, 6])
288        self.camera_view()  # Initially look from the camera
289        em = self.window.theme.font_size
290
291        # Options panel
292        self.panel = gui.Vert(em, gui.Margins(em, em, em, em))
293        self.panel.preferred_width = int(360 * self.window.scaling)
294        self.window.add_child(self.panel)
295        toggles = gui.Horiz(em)
296        self.panel.add_child(toggles)
297
298        toggle_capture = gui.ToggleSwitch("Capture / Play")
299        toggle_capture.is_on = False
300        toggle_capture.set_on_clicked(
301            callbacks['on_toggle_capture'])  # callback
302        toggles.add_child(toggle_capture)
303
304        self.flag_normals = False
305        self.toggle_normals = gui.ToggleSwitch("Colors / Normals")
306        self.toggle_normals.is_on = False
307        self.toggle_normals.set_on_clicked(
308            callbacks['on_toggle_normals'])  # callback
309        toggles.add_child(self.toggle_normals)
310
311        view_buttons = gui.Horiz(em)
312        self.panel.add_child(view_buttons)
313        view_buttons.add_stretch()  # for centering
314        camera_view = gui.Button("Camera view")
315        camera_view.set_on_clicked(self.camera_view)  # callback
316        view_buttons.add_child(camera_view)
317        birds_eye_view = gui.Button("Bird's eye view")
318        birds_eye_view.set_on_clicked(self.birds_eye_view)  # callback
319        view_buttons.add_child(birds_eye_view)
320        view_buttons.add_stretch()  # for centering
321
322        save_toggle = gui.Horiz(em)
323        self.panel.add_child(save_toggle)
324        save_toggle.add_child(gui.Label("Record / Save"))
325        self.toggle_record = None
326        if callbacks['on_toggle_record'] is not None:
327            save_toggle.add_fixed(1.5 * em)
328            self.toggle_record = gui.ToggleSwitch("Video")
329            self.toggle_record.is_on = False
330            self.toggle_record.set_on_clicked(callbacks['on_toggle_record'])
331            save_toggle.add_child(self.toggle_record)
332
333        save_buttons = gui.Horiz(em)
334        self.panel.add_child(save_buttons)
335        save_buttons.add_stretch()  # for centering
336        save_pcd = gui.Button("Save Point cloud")
337        save_pcd.set_on_clicked(callbacks['on_save_pcd'])
338        save_buttons.add_child(save_pcd)
339        save_rgbd = gui.Button("Save RGBD frame")
340        save_rgbd.set_on_clicked(callbacks['on_save_rgbd'])
341        save_buttons.add_child(save_rgbd)
342        save_buttons.add_stretch()  # for centering
343
344        self.video_size = (int(240 * self.window.scaling),
345                           int(320 * self.window.scaling), 3)
346        self.show_color = gui.CollapsableVert("Color image")
347        self.show_color.set_is_open(False)
348        self.panel.add_child(self.show_color)
349        self.color_video = gui.ImageWidget(
350            o3d.geometry.Image(np.zeros(self.video_size, dtype=np.uint8)))
351        self.show_color.add_child(self.color_video)
352        self.show_depth = gui.CollapsableVert("Depth image")
353        self.show_depth.set_is_open(False)
354        self.panel.add_child(self.show_depth)
355        self.depth_video = gui.ImageWidget(
356            o3d.geometry.Image(np.zeros(self.video_size, dtype=np.uint8)))
357        self.show_depth.add_child(self.depth_video)
358
359        self.status_message = gui.Label("")
360        self.panel.add_child(self.status_message)
361
362        self.flag_exit = False
363        self.flag_gui_init = False
364
365    def update(self, frame_elements):
366        """Update visualization with point cloud and images. Must run in main
367        thread since this makes GUI calls.
368
369        Args:
370            frame_elements: dict {element_type: geometry element}.
371                Dictionary of element types to geometry elements to be updated
372                in the GUI:
373                    'pcd': point cloud,
374                    'color': rgb image (3 channel, uint8),
375                    'depth': depth image (uint8),
376                    'status_message': message
377        """
378        if not self.flag_gui_init:
379            # Set dummy point cloud to allocate graphics memory
380            dummy_pcd = o3d.t.geometry.PointCloud({
381                'positions':
382                    o3d.core.Tensor.zeros((self.max_pcd_vertices, 3),
383                                          o3d.core.Dtype.Float32),
384                'colors':
385                    o3d.core.Tensor.zeros((self.max_pcd_vertices, 3),
386                                          o3d.core.Dtype.Float32),
387                'normals':
388                    o3d.core.Tensor.zeros((self.max_pcd_vertices, 3),
389                                          o3d.core.Dtype.Float32)
390            })
391            if self.pcdview.scene.has_geometry('pcd'):
392                self.pcdview.scene.remove_geometry('pcd')
393
394            self.pcd_material.shader = "normals" if self.flag_normals else "defaultLit"
395            self.pcdview.scene.add_geometry('pcd', dummy_pcd, self.pcd_material)
396            self.flag_gui_init = True
397
398        # TODO(ssheorey) Switch to update_geometry() after #3452 is fixed
399        if os.name == 'nt':
400            self.pcdview.scene.remove_geometry('pcd')
401            self.pcdview.scene.add_geometry('pcd', frame_elements['pcd'],
402                                            self.pcd_material)
403        else:
404            update_flags = (rendering.Scene.UPDATE_POINTS_FLAG |
405                            rendering.Scene.UPDATE_COLORS_FLAG |
406                            (rendering.Scene.UPDATE_NORMALS_FLAG
407                             if self.flag_normals else 0))
408            self.pcdview.scene.scene.update_geometry('pcd',
409                                                     frame_elements['pcd'],
410                                                     update_flags)
411
412        # Update color and depth images
413        # TODO(ssheorey) Remove CPU transfer after we have CUDA -> OpenGL bridge
414        if self.show_color.get_is_open() and 'color' in frame_elements:
415            sampling_ratio = self.video_size[1] / frame_elements['color'].columns
416            self.color_video.update_image(
417                frame_elements['color'].resize(sampling_ratio).cpu())
418        if self.show_depth.get_is_open() and 'depth' in frame_elements:
419            sampling_ratio = self.video_size[1] / frame_elements['depth'].columns
420            self.depth_video.update_image(
421                frame_elements['depth'].resize(sampling_ratio).cpu())
422
423        if 'status_message' in frame_elements:
424            self.status_message.text = frame_elements["status_message"]
425
426        self.pcdview.force_redraw()
427
428    def camera_view(self):
429        """Callback to reset point cloud view to the camera"""
430        self.pcdview.setup_camera(self.vfov, self.pcd_bounds, [0, 0, 0])
431        # Look at [0, 0, 1] from camera placed at [0, 0, 0] with Y axis
432        # pointing at [0, -1, 0]
433        self.pcdview.scene.camera.look_at([0, 0, 1], [0, 0, 0], [0, -1, 0])
434
435    def birds_eye_view(self):
436        """Callback to reset point cloud view to birds eye (overhead) view"""
437        self.pcdview.setup_camera(self.vfov, self.pcd_bounds, [0, 0, 0])
438        self.pcdview.scene.camera.look_at([0, 0, 1.5], [0, 3, 1.5], [0, -1, 0])
439
440    def on_layout(self, layout_context):
441        # The on_layout callback should set the frame (position + size) of every
442        # child correctly. After the callback is done the window will layout
443        # the grandchildren.
444        """Callback on window initialize / resize"""
445        frame = self.window.content_rect
446        self.pcdview.frame = frame
447        panel_size = self.panel.calc_preferred_size(layout_context,
448                                                    self.panel.Constraints())
449        self.panel.frame = gui.Rect(frame.get_right() - panel_size.width,
450                                    frame.y, panel_size.width,
451                                    panel_size.height)
452
453
454class PipelineController:
455    """Entry point for the app. Controls the PipelineModel object for IO and
456    processing  and the PipelineView object for display and UI. All methods
457    operate on the main thread.
458    """
459
460    def __init__(self, camera_config_file=None, rgbd_video=None, device=None):
461        """Initialize.
462
463        Args:
464            camera_config_file (str): Camera configuration json file.
465            rgbd_video (str): RS bag file containing the RGBD video. If this is
466                provided, connected cameras are ignored.
467            device (str): Compute device (e.g.: 'cpu:0' or 'cuda:0').
468        """
469        self.pipeline_model = PipelineModel(self.update_view,
470                                            camera_config_file, rgbd_video,
471                                            device)
472
473        self.pipeline_view = PipelineView(
474            1.25 * self.pipeline_model.vfov,
475            self.pipeline_model.max_points,
476            on_window_close=self.on_window_close,
477            on_toggle_capture=self.on_toggle_capture,
478            on_save_pcd=self.on_save_pcd,
479            on_save_rgbd=self.on_save_rgbd,
480            on_toggle_record=self.on_toggle_record
481            if rgbd_video is None else None,
482            on_toggle_normals=self.on_toggle_normals)
483
484        threading.Thread(name='PipelineModel',
485                         target=self.pipeline_model.run).start()
486        gui.Application.instance.run()
487
488    def update_view(self, frame_elements):
489        """Updates view with new data. May be called from any thread.
490
491        Args:
492            frame_elements (dict): Display elements (point cloud and images)
493                from the new frame to be shown.
494        """
495        gui.Application.instance.post_to_main_thread(
496            self.pipeline_view.window,
497            lambda: self.pipeline_view.update(frame_elements))
498
499    def on_toggle_capture(self, is_enabled):
500        """Callback to toggle capture."""
501        self.pipeline_model.flag_capture = is_enabled
502        if not is_enabled:
503            self.on_toggle_record(False)
504            if self.pipeline_view.toggle_record is not None:
505                self.pipeline_view.toggle_record.is_on = False
506        else:
507            with self.pipeline_model.cv_capture:
508                self.pipeline_model.cv_capture.notify()
509
510    def on_toggle_record(self, is_enabled):
511        """Callback to toggle recording RGBD video."""
512        self.pipeline_model.flag_record = is_enabled
513
514    def on_toggle_normals(self, is_enabled):
515        """Callback to toggle display of normals"""
516        self.pipeline_model.flag_normals = is_enabled
517        self.pipeline_view.flag_normals = is_enabled
518        self.pipeline_view.flag_gui_init = False
519
520    def on_window_close(self):
521        """Callback when the user closes the application window."""
522        self.pipeline_model.flag_exit = True
523        with self.pipeline_model.cv_capture:
524            self.pipeline_model.cv_capture.notify_all()
525        return True  # OK to close window
526
527    def on_save_pcd(self):
528        """Callback to save current point cloud."""
529        self.pipeline_model.flag_save_pcd = True
530
531    def on_save_rgbd(self):
532        """Callback to save current RGBD image pair."""
533        self.pipeline_model.flag_save_rgbd = True
534
535
536if __name__ == "__main__":
537
538    log.basicConfig(level=log.INFO)
539    parser = argparse.ArgumentParser(
540        description=__doc__,
541        formatter_class=argparse.RawDescriptionHelpFormatter)
542    parser.add_argument('--camera-config',
543                        help='RGBD camera configuration JSON file')
544    parser.add_argument('--rgbd-video', help='RGBD video file (RealSense bag)')
545    parser.add_argument('--device',
546                        help='Device to run computations. e.g. cpu:0 or cuda:0 '
547                        'Default is CUDA GPU if available, else CPU.')
548
549    args = parser.parse_args()
550    if args.camera_config and args.rgbd_video:
551        log.critical(
552            "Please provide only one of --camera-config and --rgbd-video arguments"
553        )
554    else:
555        PipelineController(args.camera_config, args.rgbd_video, args.device)

remove_geometry.py

27import open3d as o3d
28import numpy as np
29import time
30import copy
31
32
33def visualize_non_blocking(vis, pcds):
34    for pcd in pcds:
35        vis.update_geometry(pcd)
36    vis.poll_events()
37    vis.update_renderer()
38
39
40pcd_data = o3d.data.PCDPointCloud()
41pcd_orig = o3d.io.read_point_cloud(pcd_data.path)
42flip_transform = [[1, 0, 0, 0], [0, -1, 0, 0], [0, 0, -1, 0], [0, 0, 0, 1]]
43pcd_orig.transform(flip_transform)
44n_pcd = 5
45pcds = []
46for i in range(n_pcd):
47    pcds.append(copy.deepcopy(pcd_orig))
48    trans = np.identity(4)
49    trans[:3, 3] = [3 * i, 0, 0]
50    pcds[i].transform(trans)
51
52vis = o3d.visualization.Visualizer()
53vis.create_window()
54start_time = time.time()
55added = [False] * n_pcd
56
57curr_sec = int(time.time() - start_time)
58prev_sec = curr_sec - 1
59
60while True:
61    curr_sec = int(time.time() - start_time)
62    if curr_sec - prev_sec == 1:
63        prev_sec = curr_sec
64
65        for i in range(n_pcd):
66            if curr_sec % (n_pcd * 2) == i and not added[i]:
67                vis.add_geometry(pcds[i])
68                added[i] = True
69                print("Adding %d" % i)
70            if curr_sec % (n_pcd * 2) == (i + n_pcd) and added[i]:
71                vis.remove_geometry(pcds[i])
72                added[i] = False
73                print("Removing %d" % i)
74
75    visualize_non_blocking(vis, pcds)

render_to_image.py

27import open3d as o3d
28import open3d.visualization.rendering as rendering
29
30
31def main():
32    render = rendering.OffscreenRenderer(640, 480)
33
34    yellow = rendering.MaterialRecord()
35    yellow.base_color = [1.0, 0.75, 0.0, 1.0]
36    yellow.shader = "defaultLit"
37
38    green = rendering.MaterialRecord()
39    green.base_color = [0.0, 0.5, 0.0, 1.0]
40    green.shader = "defaultLit"
41
42    grey = rendering.MaterialRecord()
43    grey.base_color = [0.7, 0.7, 0.7, 1.0]
44    grey.shader = "defaultLit"
45
46    white = rendering.MaterialRecord()
47    white.base_color = [1.0, 1.0, 1.0, 1.0]
48    white.shader = "defaultLit"
49
50    cyl = o3d.geometry.TriangleMesh.create_cylinder(.05, 3)
51    cyl.compute_vertex_normals()
52    cyl.translate([-2, 0, 1.5])
53    sphere = o3d.geometry.TriangleMesh.create_sphere(.2)
54    sphere.compute_vertex_normals()
55    sphere.translate([-2, 0, 3])
56
57    box = o3d.geometry.TriangleMesh.create_box(2, 2, 1)
58    box.compute_vertex_normals()
59    box.translate([-1, -1, 0])
60    solid = o3d.geometry.TriangleMesh.create_icosahedron(0.5)
61    solid.compute_triangle_normals()
62    solid.compute_vertex_normals()
63    solid.translate([0, 0, 1.75])
64
65    render.scene.add_geometry("cyl", cyl, green)
66    render.scene.add_geometry("sphere", sphere, yellow)
67    render.scene.add_geometry("box", box, grey)
68    render.scene.add_geometry("solid", solid, white)
69    render.setup_camera(60.0, [0, 0, 0], [0, 10, 0], [0, 0, 1])
70    render.scene.scene.set_sun_light([0.707, 0.0, -.707], [1.0, 1.0, 1.0],
71                                     75000)
72    render.scene.scene.enable_sun_light(True)
73    render.scene.show_axes(True)
74
75    img = render.render_to_image()
76    print("Saving image at test.png")
77    o3d.io.write_image("test.png", img, 9)
78
79    render.setup_camera(60.0, [0, 0, 0], [-10, 0, 0], [0, 0, 1])
80    img = render.render_to_image()
81    print("Saving image at test2.png")
82    o3d.io.write_image("test2.png", img, 9)
83
84
85if __name__ == "__main__":
86    main()

tensorboard_pytorch.py

 27import os
 28import sys
 29import numpy as np
 30import open3d as o3d
 31# pylint: disable-next=unused-import
 32from open3d.visualization.tensorboard_plugin.util import to_dict_batch
 33from torch.utils.tensorboard import SummaryWriter
 34
 35BASE_LOGDIR = "demo_logs/pytorch/"
 36MODEL_DIR = os.path.join(
 37    os.path.dirname(os.path.dirname(os.path.dirname(
 38        os.path.realpath(__file__)))), "test_data", "monkey")
 39
 40
 41def small_scale(run_name="small_scale"):
 42    """Basic demo with cube and cylinder with normals and colors.
 43    """
 44    logdir = os.path.join(BASE_LOGDIR, run_name)
 45    writer = SummaryWriter(logdir)
 46
 47    cube = o3d.geometry.TriangleMesh.create_box(1, 2, 4, create_uv_map=True)
 48    cube.compute_vertex_normals()
 49    cylinder = o3d.geometry.TriangleMesh.create_cylinder(radius=1.0,
 50                                                         height=2.0,
 51                                                         resolution=20,
 52                                                         split=4,
 53                                                         create_uv_map=True)
 54    cylinder.compute_vertex_normals()
 55    colors = [(1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)]
 56    for step in range(3):
 57        cube.paint_uniform_color(colors[step])
 58        writer.add_3d('cube', to_dict_batch([cube]), step=step)
 59        cylinder.paint_uniform_color(colors[step])
 60        writer.add_3d('cylinder', to_dict_batch([cylinder]), step=step)
 61
 62
 63def property_reference(run_name="property_reference"):
 64    """Produces identical visualization to small_scale, but does not store
 65    repeated properties of ``vertex_positions`` and ``vertex_normals``.
 66    """
 67    logdir = os.path.join(BASE_LOGDIR, run_name)
 68    writer = SummaryWriter(logdir)
 69
 70    cube = o3d.geometry.TriangleMesh.create_box(1, 2, 4, create_uv_map=True)
 71    cube.compute_vertex_normals()
 72    cylinder = o3d.geometry.TriangleMesh.create_cylinder(radius=1.0,
 73                                                         height=2.0,
 74                                                         resolution=20,
 75                                                         split=4,
 76                                                         create_uv_map=True)
 77    cylinder.compute_vertex_normals()
 78    colors = [(1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)]
 79    for step in range(3):
 80        cube.paint_uniform_color(colors[step])
 81        cube_summary = to_dict_batch([cube])
 82        if step > 0:
 83            cube_summary['vertex_positions'] = 0
 84            cube_summary['vertex_normals'] = 0
 85        writer.add_3d('cube', cube_summary, step=step)
 86        cylinder.paint_uniform_color(colors[step])
 87        cylinder_summary = to_dict_batch([cylinder])
 88        if step > 0:
 89            cylinder_summary['vertex_positions'] = 0
 90            cylinder_summary['vertex_normals'] = 0
 91        writer.add_3d('cylinder', cylinder_summary, step=step)
 92
 93
 94def large_scale(n_steps=16,
 95                batch_size=1,
 96                base_resolution=200,
 97                run_name="large_scale"):
 98    """Generate a large scale summary. Geometry resolution increases linearly
 99    with step. Each element in a batch is painted a different color.
100    """
101    logdir = os.path.join(BASE_LOGDIR, run_name)
102    writer = SummaryWriter(logdir)
103    colors = []
104    for k in range(batch_size):
105        t = k * np.pi / batch_size
106        colors.append(((1 + np.sin(t)) / 2, (1 + np.cos(t)) / 2, t / np.pi))
107    for step in range(n_steps):
108        resolution = base_resolution * (step + 1)
109        cylinder_list = []
110        mobius_list = []
111        cylinder = o3d.geometry.TriangleMesh.create_cylinder(
112            radius=1.0, height=2.0, resolution=resolution, split=4)
113        cylinder.compute_vertex_normals()
114        mobius = o3d.geometry.TriangleMesh.create_mobius(
115            length_split=int(3.5 * resolution),
116            width_split=int(0.75 * resolution),
117            twists=1,
118            raidus=1,
119            flatness=1,
120            width=1,
121            scale=1)
122        mobius.compute_vertex_normals()
123        for b in range(batch_size):
124            cylinder_list.append(copy.deepcopy(cylinder))
125            cylinder_list[b].paint_uniform_color(colors[b])
126            mobius_list.append(copy.deepcopy(mobius))
127            mobius_list[b].paint_uniform_color(colors[b])
128        writer.add_3d('cylinder',
129                      to_dict_batch(cylinder_list),
130                      step=step,
131                      max_outputs=batch_size)
132        writer.add_3d('mobius',
133                      to_dict_batch(mobius_list),
134                      step=step,
135                      max_outputs=batch_size)
136
137
138def with_material(model_dir=MODEL_DIR):
139    """Read an obj model from a directory and write as a TensorBoard summary.
140    """
141    model_name = os.path.basename(model_dir)
142    logdir = os.path.join(BASE_LOGDIR, model_name)
143    model_path = os.path.join(model_dir, model_name + ".obj")
144    model = o3d.t.geometry.TriangleMesh.from_legacy(
145        o3d.io.read_triangle_mesh(model_path))
146    summary_3d = {
147        "vertex_positions": model.vertex.positions,
148        "vertex_normals": model.vertex.normals,
149        "triangle_texture_uvs": model.triangle["texture_uvs"],
150        "triangle_indices": model.triangle.indices,
151        "material_name": "defaultLit"
152    }
153    names_to_o3dprop = {"ao": "ambient_occlusion"}
154
155    for texture in ("albedo", "normal", "ao", "metallic", "roughness"):
156        texture_file = os.path.join(model_dir, texture + ".png")
157        if os.path.exists(texture_file):
158            texture = names_to_o3dprop.get(texture, texture)
159            summary_3d.update({
160                ("material_texture_map_" + texture):
161                    o3d.t.io.read_image(texture_file)
162            })
163            if texture == "metallic":
164                summary_3d.update(material_scalar_metallic=1.0)
165
166    writer = SummaryWriter(logdir)
167    writer.add_3d(model_name, summary_3d, step=0)
168
169
170def demo_scene():
171    """Write the demo_scene.py example showing rich PBR materials as a summary
172    """
173    import demo_scene
174    geoms = demo_scene.create_scene()
175    writer = SummaryWriter(os.path.join(BASE_LOGDIR, 'demo_scene'))
176    for geom_data in geoms:
177        geom = geom_data["geometry"]
178        summary_3d = {}
179        for key, tensor in geom.vertex.items():
180            summary_3d["vertex_" + key] = tensor
181        for key, tensor in geom.triangle.items():
182            summary_3d["triangle_" + key] = tensor
183        if geom.has_valid_material():
184            summary_3d["material_name"] = geom.material.material_name
185            for key, value in geom.material.scalar_properties.items():
186                summary_3d["material_scalar_" + key] = value
187            for key, value in geom.material.vector_properties.items():
188                summary_3d["material_vector_" + key] = value
189            for key, value in geom.material.texture_maps.items():
190                summary_3d["material_texture_map_" + key] = value
191        writer.add_3d(geom_data["name"], summary_3d, step=0)
192
193
194if __name__ == "__main__":
195
196    examples = ('small_scale', 'large_scale', 'property_reference',
197                'with_material', 'demo_scene')
198    selected = tuple(eg for eg in sys.argv[1:] if eg in examples)
199    if len(selected) == 0:
200        print(f'Usage: python {__file__} EXAMPLE...')
201        print(f'  where EXAMPLE are from {examples}')
202        selected = ('property_reference', 'with_material')
203
204    for eg in selected:
205        locals()[eg]()
206
207    print(f"Run 'tensorboard --logdir {BASE_LOGDIR}' to visualize the 3D "
208          "summary.")

tensorboard_tensorflow.py

 27import os
 28import sys
 29import numpy as np
 30import open3d as o3d
 31from open3d.visualization.tensorboard_plugin import summary
 32from open3d.visualization.tensorboard_plugin.util import to_dict_batch
 33import tensorflow as tf
 34
 35BASE_LOGDIR = "demo_logs/tf/"
 36MODEL_DIR = os.path.join(
 37    os.path.dirname(os.path.dirname(os.path.dirname(
 38        os.path.realpath(__file__)))), "test_data", "monkey")
 39
 40
 41def small_scale(run_name="small_scale"):
 42    """Basic demo with cube and cylinder with normals and colors.
 43    """
 44    logdir = os.path.join(BASE_LOGDIR, run_name)
 45    writer = tf.summary.create_file_writer(logdir)
 46
 47    cube = o3d.geometry.TriangleMesh.create_box(1, 2, 4, create_uv_map=True)
 48    cube.compute_vertex_normals()
 49    cylinder = o3d.geometry.TriangleMesh.create_cylinder(radius=1.0,
 50                                                         height=2.0,
 51                                                         resolution=20,
 52                                                         split=4,
 53                                                         create_uv_map=True)
 54    cylinder.compute_vertex_normals()
 55    colors = [(1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)]
 56    with writer.as_default():
 57        for step in range(3):
 58            cube.paint_uniform_color(colors[step])
 59            summary.add_3d('cube',
 60                           to_dict_batch([cube]),
 61                           step=step,
 62                           logdir=logdir)
 63            cylinder.paint_uniform_color(colors[step])
 64            summary.add_3d('cylinder',
 65                           to_dict_batch([cylinder]),
 66                           step=step,
 67                           logdir=logdir)
 68
 69
 70def property_reference(run_name="property_reference"):
 71    """Produces identical visualization to small_scale, but does not store
 72    repeated properties of ``vertex_positions`` and ``vertex_normals``.
 73    """
 74    logdir = os.path.join(BASE_LOGDIR, run_name)
 75    writer = tf.summary.create_file_writer(logdir)
 76
 77    cube = o3d.geometry.TriangleMesh.create_box(1, 2, 4, create_uv_map=True)
 78    cube.compute_vertex_normals()
 79    cylinder = o3d.geometry.TriangleMesh.create_cylinder(radius=1.0,
 80                                                         height=2.0,
 81                                                         resolution=20,
 82                                                         split=4,
 83                                                         create_uv_map=True)
 84    cylinder.compute_vertex_normals()
 85    colors = [(1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)]
 86    with writer.as_default():
 87        for step in range(3):
 88            cube.paint_uniform_color(colors[step])
 89            cube_summary = to_dict_batch([cube])
 90            if step > 0:
 91                cube_summary['vertex_positions'] = 0
 92                cube_summary['vertex_normals'] = 0
 93            summary.add_3d('cube', cube_summary, step=step, logdir=logdir)
 94            cylinder.paint_uniform_color(colors[step])
 95            cylinder_summary = to_dict_batch([cylinder])
 96            if step > 0:
 97                cylinder_summary['vertex_positions'] = 0
 98                cylinder_summary['vertex_normals'] = 0
 99            summary.add_3d('cylinder',
100                           cylinder_summary,
101                           step=step,
102                           logdir=logdir)
103
104
105def large_scale(n_steps=16,
106                batch_size=1,
107                base_resolution=200,
108                run_name="large_scale"):
109    """Generate a large scale summary. Geometry resolution increases linearly
110    with step. Each element in a batch is painted a different color.
111    """
112    logdir = os.path.join(BASE_LOGDIR, run_name)
113    writer = tf.summary.create_file_writer(logdir)
114    colors = []
115    for k in range(batch_size):
116        t = k * np.pi / batch_size
117        colors.append(((1 + np.sin(t)) / 2, (1 + np.cos(t)) / 2, t / np.pi))
118    with writer.as_default():
119        for step in range(n_steps):
120            resolution = base_resolution * (step + 1)
121            cylinder_list = []
122            mobius_list = []
123            cylinder = o3d.geometry.TriangleMesh.create_cylinder(
124                radius=1.0, height=2.0, resolution=resolution, split=4)
125            cylinder.compute_vertex_normals()
126            mobius = o3d.geometry.TriangleMesh.create_mobius(
127                length_split=int(3.5 * resolution),
128                width_split=int(0.75 * resolution),
129                twists=1,
130                raidus=1,
131                flatness=1,
132                width=1,
133                scale=1)
134            mobius.compute_vertex_normals()
135            for b in range(batch_size):
136                cylinder_list.append(copy.deepcopy(cylinder))
137                cylinder_list[b].paint_uniform_color(colors[b])
138                mobius_list.append(copy.deepcopy(mobius))
139                mobius_list[b].paint_uniform_color(colors[b])
140            summary.add_3d('cylinder',
141                           to_dict_batch(cylinder_list),
142                           step=step,
143                           logdir=logdir,
144                           max_outputs=batch_size)
145            summary.add_3d('mobius',
146                           to_dict_batch(mobius_list),
147                           step=step,
148                           logdir=logdir,
149                           max_outputs=batch_size)
150
151
152def with_material(model_dir=MODEL_DIR):
153    """Read an obj model from a directory and write as a TensorBoard summary.
154    """
155    model_name = os.path.basename(model_dir)
156    logdir = os.path.join(BASE_LOGDIR, model_name)
157    model_path = os.path.join(model_dir, model_name + ".obj")
158    model = o3d.t.geometry.TriangleMesh.from_legacy(
159        o3d.io.read_triangle_mesh(model_path))
160    summary_3d = {
161        "vertex_positions": model.vertex.positions,
162        "vertex_normals": model.vertex.normals,
163        "triangle_texture_uvs": model.triangle["texture_uvs"],
164        "triangle_indices": model.triangle.indices,
165        "material_name": "defaultLit"
166    }
167    names_to_o3dprop = {"ao": "ambient_occlusion"}
168
169    for texture in ("albedo", "normal", "ao", "metallic", "roughness"):
170        texture_file = os.path.join(model_dir, texture + ".png")
171        if os.path.exists(texture_file):
172            texture = names_to_o3dprop.get(texture, texture)
173            summary_3d.update({
174                ("material_texture_map_" + texture):
175                    o3d.t.io.read_image(texture_file)
176            })
177            if texture == "metallic":
178                summary_3d.update(material_scalar_metallic=1.0)
179
180    writer = tf.summary.create_file_writer(logdir)
181    with writer.as_default():
182        summary.add_3d(model_name, summary_3d, step=0, logdir=logdir)
183
184
185def demo_scene():
186    """Write the demo_scene.py example showing rich PBR materials as a summary.
187    """
188    import demo_scene
189    geoms = demo_scene.create_scene()
190    logdir = os.path.join(BASE_LOGDIR, 'demo_scene')
191    writer = tf.summary.create_file_writer(logdir)
192    for geom_data in geoms:
193        geom = geom_data["geometry"]
194        summary_3d = {}
195        for key, tensor in geom.vertex.items():
196            summary_3d["vertex_" + key] = tensor
197        for key, tensor in geom.triangle.items():
198            summary_3d["triangle_" + key] = tensor
199        if geom.has_valid_material():
200            summary_3d["material_name"] = geom.material.material_name
201            for key, value in geom.material.scalar_properties.items():
202                summary_3d["material_scalar_" + key] = value
203            for key, value in geom.material.vector_properties.items():
204                summary_3d["material_vector_" + key] = value
205            for key, value in geom.material.texture_maps.items():
206                summary_3d["material_texture_map_" + key] = value
207        with writer.as_default():
208            summary.add_3d(geom_data["name"], summary_3d, step=0, logdir=logdir)
209
210
211if __name__ == "__main__":
212
213    examples = ('small_scale', 'large_scale', 'property_reference',
214                'with_material', 'demo_scene')
215    selected = tuple(eg for eg in sys.argv[1:] if eg in examples)
216    if len(selected) == 0:
217        print(f'Usage: python {__file__} EXAMPLE...')
218        print(f'  where EXAMPLE are from {examples}')
219        selected = ('property_reference', 'with_material')
220
221    for eg in selected:
222        locals()[eg]()
223
224    print(f"Run 'tensorboard --logdir {BASE_LOGDIR}' to visualize the 3D "
225          "summary.")

text3d.py

27import numpy as np
28import open3d as o3d
29import open3d.visualization.gui as gui
30import open3d.visualization.rendering as rendering
31
32
33def make_point_cloud(npts, center, radius):
34    pts = np.random.uniform(-radius, radius, size=[npts, 3]) + center
35    cloud = o3d.geometry.PointCloud()
36    cloud.points = o3d.utility.Vector3dVector(pts)
37    colors = np.random.uniform(0.0, 1.0, size=[npts, 3])
38    cloud.colors = o3d.utility.Vector3dVector(colors)
39    return cloud
40
41
42def high_level():
43    app = gui.Application.instance
44    app.initialize()
45
46    points = make_point_cloud(100, (0, 0, 0), 1.0)
47
48    vis = o3d.visualization.O3DVisualizer("Open3D - 3D Text", 1024, 768)
49    vis.show_settings = True
50    vis.add_geometry("Points", points)
51    for idx in range(0, len(points.points)):
52        vis.add_3d_label(points.points[idx], "{}".format(idx))
53    vis.reset_camera_to_default()
54
55    app.add_window(vis)
56    app.run()
57
58
59def low_level():
60    app = gui.Application.instance
61    app.initialize()
62
63    points = make_point_cloud(100, (0, 0, 0), 1.0)
64
65    w = app.create_window("Open3D - 3D Text", 1024, 768)
66    widget3d = gui.SceneWidget()
67    widget3d.scene = rendering.Open3DScene(w.renderer)
68    mat = rendering.MaterialRecord()
69    mat.shader = "defaultUnlit"
70    mat.point_size = 5 * w.scaling
71    widget3d.scene.add_geometry("Points", points, mat)
72    for idx in range(0, len(points.points)):
73        l = widget3d.add_3d_label(points.points[idx], "{}".format(idx))
74        l.color = gui.Color(points.colors[idx][0], points.colors[idx][1],
75                            points.colors[idx][2])
76        l.scale = np.random.uniform(0.5, 3.0)
77    bbox = widget3d.scene.bounding_box
78    widget3d.setup_camera(60.0, bbox, bbox.get_center())
79    w.add_child(widget3d)
80
81    app.run()
82
83
84if __name__ == "__main__":
85    high_level()
86    low_level()

textured_mesh.py

27import sys
28import os
29import open3d as o3d
30
31
32def main():
33    if len(sys.argv) < 2:
34        print("""Usage: textured-mesh.py [model directory]
35    This example will load [model directory].obj plus any of albedo, normal,
36    ao, metallic and roughness textures present. The textures should be named
37    albedo.png, normal.png, ao.png, metallic.png and roughness.png
38    respectively.""")
39        sys.exit()
40
41    model_dir = os.path.normpath(os.path.realpath(sys.argv[1]))
42    model_name = os.path.join(model_dir, os.path.basename(model_dir) + ".obj")
43    mesh = o3d.t.geometry.TriangleMesh.from_legacy(
44        o3d.io.read_triangle_mesh(model_name))
45    material = mesh.material
46    material.material_name = "defaultLit"
47
48    names_to_o3dprop = {"ao": "ambient_occlusion"}
49    for texture in ("albedo", "normal", "ao", "metallic", "roughness"):
50        texture_file = os.path.join(model_dir, texture + ".png")
51        if os.path.exists(texture_file):
52            texture = names_to_o3dprop.get(texture, texture)
53            material.texture_maps[texture] = o3d.t.io.read_image(texture_file)
54    if "metallic" in material.texture_maps:
55        material.scalar_properties["metallic"] = 1.0
56
57    o3d.visualization.draw(mesh, title=model_name)
58
59
60if __name__ == "__main__":
61    main()

textured_model.py

27import sys
28import os
29import open3d as o3d
30
31
32def main():
33    if len(sys.argv) < 2:
34        print("""Usage: texture-model.py [model directory]
35    This example will load [model directory].obj plus any of albedo, normal,
36    ao, metallic and roughness textures present.""")
37        sys.exit()
38
39    model_dir = sys.argv[1]
40    model_name = os.path.join(model_dir, os.path.basename(model_dir) + ".obj")
41    model = o3d.io.read_triangle_mesh(model_name)
42    material = o3d.visualization.rendering.MaterialRecord()
43    material.shader = "defaultLit"
44
45    albedo_name = os.path.join(model_dir, "albedo.png")
46    normal_name = os.path.join(model_dir, "normal.png")
47    ao_name = os.path.join(model_dir, "ao.png")
48    metallic_name = os.path.join(model_dir, "metallic.png")
49    roughness_name = os.path.join(model_dir, "roughness.png")
50    if os.path.exists(albedo_name):
51        material.albedo_img = o3d.io.read_image(albedo_name)
52    if os.path.exists(normal_name):
53        material.normal_img = o3d.io.read_image(normal_name)
54    if os.path.exists(ao_name):
55        material.ao_img = o3d.io.read_image(ao_name)
56    if os.path.exists(metallic_name):
57        material.base_metallic = 1.0
58        material.metallic_img = o3d.io.read_image(metallic_name)
59    if os.path.exists(roughness_name):
60        material.roughness_img = o3d.io.read_image(roughness_name)
61
62    o3d.visualization.draw([{
63        "name": "cube",
64        "geometry": model,
65        "material": material
66    }])
67
68
69if __name__ == "__main__":
70    main()

video.py

 27import numpy as np
 28import open3d as o3d
 29import open3d.visualization.gui as gui
 30import open3d.visualization.rendering as rendering
 31import time
 32import threading
 33
 34
 35def rescale_greyscale(img):
 36    data = np.asarray(img)
 37    assert (len(data.shape) == 2)  # requires 1 channel image
 38    dataFloat = data.astype(np.float64)
 39    max_val = dataFloat.max()
 40    # We don't currently support 16-bit images, so convert to 8-bit
 41    dataFloat *= 255.0 / max_val
 42    data8 = dataFloat.astype(np.uint8)
 43    return o3d.geometry.Image(data8)
 44
 45
 46class VideoWindow:
 47
 48    def __init__(self):
 49        self.rgb_images = []
 50        rgbd_data = o3d.data.SampleRedwoodRGBDImages()
 51        for path in rgbd_data.color_paths:
 52            img = o3d.io.read_image(path)
 53            self.rgb_images.append(img)
 54        self.depth_images = []
 55        for path in rgbd_data.depth_paths:
 56            img = o3d.io.read_image(path)
 57            # The images are pretty dark, so rescale them so that it is
 58            # obvious that this is a depth image, for the sake of the example
 59            img = rescale_greyscale(img)
 60            self.depth_images.append(img)
 61        assert (len(self.rgb_images) == len(self.depth_images))
 62
 63        self.window = gui.Application.instance.create_window(
 64            "Open3D - Video Example", 1000, 500)
 65        self.window.set_on_layout(self._on_layout)
 66        self.window.set_on_close(self._on_close)
 67
 68        self.widget3d = gui.SceneWidget()
 69        self.widget3d.scene = rendering.Open3DScene(self.window.renderer)
 70        self.window.add_child(self.widget3d)
 71
 72        lit = rendering.MaterialRecord()
 73        lit.shader = "defaultLit"
 74        tet = o3d.geometry.TriangleMesh.create_tetrahedron()
 75        tet.compute_vertex_normals()
 76        tet.paint_uniform_color([0.5, 0.75, 1.0])
 77        self.widget3d.scene.add_geometry("tetrahedron", tet, lit)
 78        bounds = self.widget3d.scene.bounding_box
 79        self.widget3d.setup_camera(60.0, bounds, bounds.get_center())
 80        self.widget3d.scene.show_axes(True)
 81
 82        em = self.window.theme.font_size
 83        margin = 0.5 * em
 84        self.panel = gui.Vert(0.5 * em, gui.Margins(margin))
 85        self.panel.add_child(gui.Label("Color image"))
 86        self.rgb_widget = gui.ImageWidget(self.rgb_images[0])
 87        self.panel.add_child(self.rgb_widget)
 88        self.panel.add_child(gui.Label("Depth image (normalized)"))
 89        self.depth_widget = gui.ImageWidget(self.depth_images[0])
 90        self.panel.add_child(self.depth_widget)
 91        self.window.add_child(self.panel)
 92
 93        self.is_done = False
 94        threading.Thread(target=self._update_thread).start()
 95
 96    def _on_layout(self, layout_context):
 97        contentRect = self.window.content_rect
 98        panel_width = 15 * layout_context.theme.font_size  # 15 ems wide
 99        self.widget3d.frame = gui.Rect(contentRect.x, contentRect.y,
100                                       contentRect.width - panel_width,
101                                       contentRect.height)
102        self.panel.frame = gui.Rect(self.widget3d.frame.get_right(),
103                                    contentRect.y, panel_width,
104                                    contentRect.height)
105
106    def _on_close(self):
107        self.is_done = True
108        return True  # False would cancel the close
109
110    def _update_thread(self):
111        # This is NOT the UI thread, need to call post_to_main_thread() to update
112        # the scene or any part of the UI.
113        idx = 0
114        while not self.is_done:
115            time.sleep(0.100)
116
117            # Get the next frame, for instance, reading a frame from the camera.
118            rgb_frame = self.rgb_images[idx]
119            depth_frame = self.depth_images[idx]
120            idx += 1
121            if idx >= len(self.rgb_images):
122                idx = 0
123
124            # Update the images. This must be done on the UI thread.
125            def update():
126                self.rgb_widget.update_image(rgb_frame)
127                self.depth_widget.update_image(depth_frame)
128                self.widget3d.scene.set_background([1, 1, 1, 1], rgb_frame)
129
130            if not self.is_done:
131                gui.Application.instance.post_to_main_thread(
132                    self.window, update)
133
134
135def main():
136    app = o3d.visualization.gui.Application.instance
137    app.initialize()
138
139    win = VideoWindow()
140
141    app.run()
142
143
144if __name__ == "__main__":
145    main()

vis_gui.py

 27import glob
 28import numpy as np
 29import open3d as o3d
 30import open3d.visualization.gui as gui
 31import open3d.visualization.rendering as rendering
 32import os
 33import platform
 34import sys
 35
 36isMacOS = (platform.system() == "Darwin")
 37
 38
 39class Settings:
 40    UNLIT = "defaultUnlit"
 41    LIT = "defaultLit"
 42    NORMALS = "normals"
 43    DEPTH = "depth"
 44
 45    DEFAULT_PROFILE_NAME = "Bright day with sun at +Y [default]"
 46    POINT_CLOUD_PROFILE_NAME = "Cloudy day (no direct sun)"
 47    CUSTOM_PROFILE_NAME = "Custom"
 48    LIGHTING_PROFILES = {
 49        DEFAULT_PROFILE_NAME: {
 50            "ibl_intensity": 45000,
 51            "sun_intensity": 45000,
 52            "sun_dir": [0.577, -0.577, -0.577],
 53            # "ibl_rotation":
 54            "use_ibl": True,
 55            "use_sun": True,
 56        },
 57        "Bright day with sun at -Y": {
 58            "ibl_intensity": 45000,
 59            "sun_intensity": 45000,
 60            "sun_dir": [0.577, 0.577, 0.577],
 61            # "ibl_rotation":
 62            "use_ibl": True,
 63            "use_sun": True,
 64        },
 65        "Bright day with sun at +Z": {
 66            "ibl_intensity": 45000,
 67            "sun_intensity": 45000,
 68            "sun_dir": [0.577, 0.577, -0.577],
 69            # "ibl_rotation":
 70            "use_ibl": True,
 71            "use_sun": True,
 72        },
 73        "Less Bright day with sun at +Y": {
 74            "ibl_intensity": 35000,
 75            "sun_intensity": 50000,
 76            "sun_dir": [0.577, -0.577, -0.577],
 77            # "ibl_rotation":
 78            "use_ibl": True,
 79            "use_sun": True,
 80        },
 81        "Less Bright day with sun at -Y": {
 82            "ibl_intensity": 35000,
 83            "sun_intensity": 50000,
 84            "sun_dir": [0.577, 0.577, 0.577],
 85            # "ibl_rotation":
 86            "use_ibl": True,
 87            "use_sun": True,
 88        },
 89        "Less Bright day with sun at +Z": {
 90            "ibl_intensity": 35000,
 91            "sun_intensity": 50000,
 92            "sun_dir": [0.577, 0.577, -0.577],
 93            # "ibl_rotation":
 94            "use_ibl": True,
 95            "use_sun": True,
 96        },
 97        POINT_CLOUD_PROFILE_NAME: {
 98            "ibl_intensity": 60000,
 99            "sun_intensity": 50000,
100            "use_ibl": True,
101            "use_sun": False,
102            # "ibl_rotation":
103        },
104    }
105
106    DEFAULT_MATERIAL_NAME = "Polished ceramic [default]"
107    PREFAB = {
108        DEFAULT_MATERIAL_NAME: {
109            "metallic": 0.0,
110            "roughness": 0.7,
111            "reflectance": 0.5,
112            "clearcoat": 0.2,
113            "clearcoat_roughness": 0.2,
114            "anisotropy": 0.0
115        },
116        "Metal (rougher)": {
117            "metallic": 1.0,
118            "roughness": 0.5,
119            "reflectance": 0.9,
120            "clearcoat": 0.0,
121            "clearcoat_roughness": 0.0,
122            "anisotropy": 0.0
123        },
124        "Metal (smoother)": {
125            "metallic": 1.0,
126            "roughness": 0.3,
127            "reflectance": 0.9,
128            "clearcoat": 0.0,
129            "clearcoat_roughness": 0.0,
130            "anisotropy": 0.0
131        },
132        "Plastic": {
133            "metallic": 0.0,
134            "roughness": 0.5,
135            "reflectance": 0.5,
136            "clearcoat": 0.5,
137            "clearcoat_roughness": 0.2,
138            "anisotropy": 0.0
139        },
140        "Glazed ceramic": {
141            "metallic": 0.0,
142            "roughness": 0.5,
143            "reflectance": 0.9,
144            "clearcoat": 1.0,
145            "clearcoat_roughness": 0.1,
146            "anisotropy": 0.0
147        },
148        "Clay": {
149            "metallic": 0.0,
150            "roughness": 1.0,
151            "reflectance": 0.5,
152            "clearcoat": 0.1,
153            "clearcoat_roughness": 0.287,
154            "anisotropy": 0.0
155        },
156    }
157
158    def __init__(self):
159        self.mouse_model = gui.SceneWidget.Controls.ROTATE_CAMERA
160        self.bg_color = gui.Color(1, 1, 1)
161        self.show_skybox = False
162        self.show_axes = False
163        self.use_ibl = True
164        self.use_sun = True
165        self.new_ibl_name = None  # clear to None after loading
166        self.ibl_intensity = 45000
167        self.sun_intensity = 45000
168        self.sun_dir = [0.577, -0.577, -0.577]
169        self.sun_color = gui.Color(1, 1, 1)
170
171        self.apply_material = True  # clear to False after processing
172        self._materials = {
173            Settings.LIT: rendering.MaterialRecord(),
174            Settings.UNLIT: rendering.MaterialRecord(),
175            Settings.NORMALS: rendering.MaterialRecord(),
176            Settings.DEPTH: rendering.MaterialRecord()
177        }
178        self._materials[Settings.LIT].base_color = [0.9, 0.9, 0.9, 1.0]
179        self._materials[Settings.LIT].shader = Settings.LIT
180        self._materials[Settings.UNLIT].base_color = [0.9, 0.9, 0.9, 1.0]
181        self._materials[Settings.UNLIT].shader = Settings.UNLIT
182        self._materials[Settings.NORMALS].shader = Settings.NORMALS
183        self._materials[Settings.DEPTH].shader = Settings.DEPTH
184
185        # Conveniently, assigning from self._materials[...] assigns a reference,
186        # not a copy, so if we change the property of a material, then switch
187        # to another one, then come back, the old setting will still be there.
188        self.material = self._materials[Settings.LIT]
189
190    def set_material(self, name):
191        self.material = self._materials[name]
192        self.apply_material = True
193
194    def apply_material_prefab(self, name):
195        assert (self.material.shader == Settings.LIT)
196        prefab = Settings.PREFAB[name]
197        for key, val in prefab.items():
198            setattr(self.material, "base_" + key, val)
199
200    def apply_lighting_profile(self, name):
201        profile = Settings.LIGHTING_PROFILES[name]
202        for key, val in profile.items():
203            setattr(self, key, val)
204
205
206class AppWindow:
207    MENU_OPEN = 1
208    MENU_EXPORT = 2
209    MENU_QUIT = 3
210    MENU_SHOW_SETTINGS = 11
211    MENU_ABOUT = 21
212
213    DEFAULT_IBL = "default"
214
215    MATERIAL_NAMES = ["Lit", "Unlit", "Normals", "Depth"]
216    MATERIAL_SHADERS = [
217        Settings.LIT, Settings.UNLIT, Settings.NORMALS, Settings.DEPTH
218    ]
219
220    def __init__(self, width, height):
221        self.settings = Settings()
222        resource_path = gui.Application.instance.resource_path
223        self.settings.new_ibl_name = resource_path + "/" + AppWindow.DEFAULT_IBL
224
225        self.window = gui.Application.instance.create_window(
226            "Open3D", width, height)
227        w = self.window  # to make the code more concise
228
229        # 3D widget
230        self._scene = gui.SceneWidget()
231        self._scene.scene = rendering.Open3DScene(w.renderer)
232        self._scene.set_on_sun_direction_changed(self._on_sun_dir)
233
234        # ---- Settings panel ----
235        # Rather than specifying sizes in pixels, which may vary in size based
236        # on the monitor, especially on macOS which has 220 dpi monitors, use
237        # the em-size. This way sizings will be proportional to the font size,
238        # which will create a more visually consistent size across platforms.
239        em = w.theme.font_size
240        separation_height = int(round(0.5 * em))
241
242        # Widgets are laid out in layouts: gui.Horiz, gui.Vert,
243        # gui.CollapsableVert, and gui.VGrid. By nesting the layouts we can
244        # achieve complex designs. Usually we use a vertical layout as the
245        # topmost widget, since widgets tend to be organized from top to bottom.
246        # Within that, we usually have a series of horizontal layouts for each
247        # row. All layouts take a spacing parameter, which is the spacing
248        # between items in the widget, and a margins parameter, which specifies
249        # the spacing of the left, top, right, bottom margins. (This acts like
250        # the 'padding' property in CSS.)
251        self._settings_panel = gui.Vert(
252            0, gui.Margins(0.25 * em, 0.25 * em, 0.25 * em, 0.25 * em))
253
254        # Create a collapsible vertical widget, which takes up enough vertical
255        # space for all its children when open, but only enough for text when
256        # closed. This is useful for property pages, so the user can hide sets
257        # of properties they rarely use.
258        view_ctrls = gui.CollapsableVert("View controls", 0.25 * em,
259                                         gui.Margins(em, 0, 0, 0))
260
261        self._arcball_button = gui.Button("Arcball")
262        self._arcball_button.horizontal_padding_em = 0.5
263        self._arcball_button.vertical_padding_em = 0
264        self._arcball_button.set_on_clicked(self._set_mouse_mode_rotate)
265        self._fly_button = gui.Button("Fly")
266        self._fly_button.horizontal_padding_em = 0.5
267        self._fly_button.vertical_padding_em = 0
268        self._fly_button.set_on_clicked(self._set_mouse_mode_fly)
269        self._model_button = gui.Button("Model")
270        self._model_button.horizontal_padding_em = 0.5
271        self._model_button.vertical_padding_em = 0
272        self._model_button.set_on_clicked(self._set_mouse_mode_model)
273        self._sun_button = gui.Button("Sun")
274        self._sun_button.horizontal_padding_em = 0.5
275        self._sun_button.vertical_padding_em = 0
276        self._sun_button.set_on_clicked(self._set_mouse_mode_sun)
277        self._ibl_button = gui.Button("Environment")
278        self._ibl_button.horizontal_padding_em = 0.5
279        self._ibl_button.vertical_padding_em = 0
280        self._ibl_button.set_on_clicked(self._set_mouse_mode_ibl)
281        view_ctrls.add_child(gui.Label("Mouse controls"))
282        # We want two rows of buttons, so make two horizontal layouts. We also
283        # want the buttons centered, which we can do be putting a stretch item
284        # as the first and last item. Stretch items take up as much space as
285        # possible, and since there are two, they will each take half the extra
286        # space, thus centering the buttons.
287        h = gui.Horiz(0.25 * em)  # row 1
288        h.add_stretch()
289        h.add_child(self._arcball_button)
290        h.add_child(self._fly_button)
291        h.add_child(self._model_button)
292        h.add_stretch()
293        view_ctrls.add_child(h)
294        h = gui.Horiz(0.25 * em)  # row 2
295        h.add_stretch()
296        h.add_child(self._sun_button)
297        h.add_child(self._ibl_button)
298        h.add_stretch()
299        view_ctrls.add_child(h)
300
301        self._show_skybox = gui.Checkbox("Show skymap")
302        self._show_skybox.set_on_checked(self._on_show_skybox)
303        view_ctrls.add_fixed(separation_height)
304        view_ctrls.add_child(self._show_skybox)
305
306        self._bg_color = gui.ColorEdit()
307        self._bg_color.set_on_value_changed(self._on_bg_color)
308
309        grid = gui.VGrid(2, 0.25 * em)
310        grid.add_child(gui.Label("BG Color"))
311        grid.add_child(self._bg_color)
312        view_ctrls.add_child(grid)
313
314        self._show_axes = gui.Checkbox("Show axes")
315        self._show_axes.set_on_checked(self._on_show_axes)
316        view_ctrls.add_fixed(separation_height)
317        view_ctrls.add_child(self._show_axes)
318
319        self._profiles = gui.Combobox()
320        for name in sorted(Settings.LIGHTING_PROFILES.keys()):
321            self._profiles.add_item(name)
322        self._profiles.add_item(Settings.CUSTOM_PROFILE_NAME)
323        self._profiles.set_on_selection_changed(self._on_lighting_profile)
324        view_ctrls.add_fixed(separation_height)
325        view_ctrls.add_child(gui.Label("Lighting profiles"))
326        view_ctrls.add_child(self._profiles)
327        self._settings_panel.add_fixed(separation_height)
328        self._settings_panel.add_child(view_ctrls)
329
330        advanced = gui.CollapsableVert("Advanced lighting", 0,
331                                       gui.Margins(em, 0, 0, 0))
332        advanced.set_is_open(False)
333
334        self._use_ibl = gui.Checkbox("HDR map")
335        self._use_ibl.set_on_checked(self._on_use_ibl)
336        self._use_sun = gui.Checkbox("Sun")
337        self._use_sun.set_on_checked(self._on_use_sun)
338        advanced.add_child(gui.Label("Light sources"))
339        h = gui.Horiz(em)
340        h.add_child(self._use_ibl)
341        h.add_child(self._use_sun)
342        advanced.add_child(h)
343
344        self._ibl_map = gui.Combobox()
345        for ibl in glob.glob(gui.Application.instance.resource_path +
346                             "/*_ibl.ktx"):
347
348            self._ibl_map.add_item(os.path.basename(ibl[:-8]))
349        self._ibl_map.selected_text = AppWindow.DEFAULT_IBL
350        self._ibl_map.set_on_selection_changed(self._on_new_ibl)
351        self._ibl_intensity = gui.Slider(gui.Slider.INT)
352        self._ibl_intensity.set_limits(0, 200000)
353        self._ibl_intensity.set_on_value_changed(self._on_ibl_intensity)
354        grid = gui.VGrid(2, 0.25 * em)
355        grid.add_child(gui.Label("HDR map"))
356        grid.add_child(self._ibl_map)
357        grid.add_child(gui.Label("Intensity"))
358        grid.add_child(self._ibl_intensity)
359        advanced.add_fixed(separation_height)
360        advanced.add_child(gui.Label("Environment"))
361        advanced.add_child(grid)
362
363        self._sun_intensity = gui.Slider(gui.Slider.INT)
364        self._sun_intensity.set_limits(0, 200000)
365        self._sun_intensity.set_on_value_changed(self._on_sun_intensity)
366        self._sun_dir = gui.VectorEdit()
367        self._sun_dir.set_on_value_changed(self._on_sun_dir)
368        self._sun_color = gui.ColorEdit()
369        self._sun_color.set_on_value_changed(self._on_sun_color)
370        grid = gui.VGrid(2, 0.25 * em)
371        grid.add_child(gui.Label("Intensity"))
372        grid.add_child(self._sun_intensity)
373        grid.add_child(gui.Label("Direction"))
374        grid.add_child(self._sun_dir)
375        grid.add_child(gui.Label("Color"))
376        grid.add_child(self._sun_color)
377        advanced.add_fixed(separation_height)
378        advanced.add_child(gui.Label("Sun (Directional light)"))
379        advanced.add_child(grid)
380
381        self._settings_panel.add_fixed(separation_height)
382        self._settings_panel.add_child(advanced)
383
384        material_settings = gui.CollapsableVert("Material settings", 0,
385                                                gui.Margins(em, 0, 0, 0))
386
387        self._shader = gui.Combobox()
388        self._shader.add_item(AppWindow.MATERIAL_NAMES[0])
389        self._shader.add_item(AppWindow.MATERIAL_NAMES[1])
390        self._shader.add_item(AppWindow.MATERIAL_NAMES[2])
391        self._shader.add_item(AppWindow.MATERIAL_NAMES[3])
392        self._shader.set_on_selection_changed(self._on_shader)
393        self._material_prefab = gui.Combobox()
394        for prefab_name in sorted(Settings.PREFAB.keys()):
395            self._material_prefab.add_item(prefab_name)
396        self._material_prefab.selected_text = Settings.DEFAULT_MATERIAL_NAME
397        self._material_prefab.set_on_selection_changed(self._on_material_prefab)
398        self._material_color = gui.ColorEdit()
399        self._material_color.set_on_value_changed(self._on_material_color)
400        self._point_size = gui.Slider(gui.Slider.INT)
401        self._point_size.set_limits(1, 10)
402        self._point_size.set_on_value_changed(self._on_point_size)
403
404        grid = gui.VGrid(2, 0.25 * em)
405        grid.add_child(gui.Label("Type"))
406        grid.add_child(self._shader)
407        grid.add_child(gui.Label("Material"))
408        grid.add_child(self._material_prefab)
409        grid.add_child(gui.Label("Color"))
410        grid.add_child(self._material_color)
411        grid.add_child(gui.Label("Point size"))
412        grid.add_child(self._point_size)
413        material_settings.add_child(grid)
414
415        self._settings_panel.add_fixed(separation_height)
416        self._settings_panel.add_child(material_settings)
417        # ----
418
419        # Normally our user interface can be children of all one layout (usually
420        # a vertical layout), which is then the only child of the window. In our
421        # case we want the scene to take up all the space and the settings panel
422        # to go above it. We can do this custom layout by providing an on_layout
423        # callback. The on_layout callback should set the frame
424        # (position + size) of every child correctly. After the callback is
425        # done the window will layout the grandchildren.
426        w.set_on_layout(self._on_layout)
427        w.add_child(self._scene)
428        w.add_child(self._settings_panel)
429
430        # ---- Menu ----
431        # The menu is global (because the macOS menu is global), so only create
432        # it once, no matter how many windows are created
433        if gui.Application.instance.menubar is None:
434            if isMacOS:
435                app_menu = gui.Menu()
436                app_menu.add_item("About", AppWindow.MENU_ABOUT)
437                app_menu.add_separator()
438                app_menu.add_item("Quit", AppWindow.MENU_QUIT)
439            file_menu = gui.Menu()
440            file_menu.add_item("Open...", AppWindow.MENU_OPEN)
441            file_menu.add_item("Export Current Image...", AppWindow.MENU_EXPORT)
442            if not isMacOS:
443                file_menu.add_separator()
444                file_menu.add_item("Quit", AppWindow.MENU_QUIT)
445            settings_menu = gui.Menu()
446            settings_menu.add_item("Lighting & Materials",
447                                   AppWindow.MENU_SHOW_SETTINGS)
448            settings_menu.set_checked(AppWindow.MENU_SHOW_SETTINGS, True)
449            help_menu = gui.Menu()
450            help_menu.add_item("About", AppWindow.MENU_ABOUT)
451
452            menu = gui.Menu()
453            if isMacOS:
454                # macOS will name the first menu item for the running application
455                # (in our case, probably "Python"), regardless of what we call
456                # it. This is the application menu, and it is where the
457                # About..., Preferences..., and Quit menu items typically go.
458                menu.add_menu("Example", app_menu)
459                menu.add_menu("File", file_menu)
460                menu.add_menu("Settings", settings_menu)
461                # Don't include help menu unless it has something more than
462                # About...
463            else:
464                menu.add_menu("File", file_menu)
465                menu.add_menu("Settings", settings_menu)
466                menu.add_menu("Help", help_menu)
467            gui.Application.instance.menubar = menu
468
469        # The menubar is global, but we need to connect the menu items to the
470        # window, so that the window can call the appropriate function when the
471        # menu item is activated.
472        w.set_on_menu_item_activated(AppWindow.MENU_OPEN, self._on_menu_open)
473        w.set_on_menu_item_activated(AppWindow.MENU_EXPORT,
474                                     self._on_menu_export)
475        w.set_on_menu_item_activated(AppWindow.MENU_QUIT, self._on_menu_quit)
476        w.set_on_menu_item_activated(AppWindow.MENU_SHOW_SETTINGS,
477                                     self._on_menu_toggle_settings_panel)
478        w.set_on_menu_item_activated(AppWindow.MENU_ABOUT, self._on_menu_about)
479        # ----
480
481        self._apply_settings()
482
483    def _apply_settings(self):
484        bg_color = [
485            self.settings.bg_color.red, self.settings.bg_color.green,
486            self.settings.bg_color.blue, self.settings.bg_color.alpha
487        ]
488        self._scene.scene.set_background(bg_color)
489        self._scene.scene.show_skybox(self.settings.show_skybox)
490        self._scene.scene.show_axes(self.settings.show_axes)
491        if self.settings.new_ibl_name is not None:
492            self._scene.scene.scene.set_indirect_light(
493                self.settings.new_ibl_name)
494            # Clear new_ibl_name, so we don't keep reloading this image every
495            # time the settings are applied.
496            self.settings.new_ibl_name = None
497        self._scene.scene.scene.enable_indirect_light(self.settings.use_ibl)
498        self._scene.scene.scene.set_indirect_light_intensity(
499            self.settings.ibl_intensity)
500        sun_color = [
501            self.settings.sun_color.red, self.settings.sun_color.green,
502            self.settings.sun_color.blue
503        ]
504        self._scene.scene.scene.set_sun_light(self.settings.sun_dir, sun_color,
505                                              self.settings.sun_intensity)
506        self._scene.scene.scene.enable_sun_light(self.settings.use_sun)
507
508        if self.settings.apply_material:
509            self._scene.scene.update_material(self.settings.material)
510            self.settings.apply_material = False
511
512        self._bg_color.color_value = self.settings.bg_color
513        self._show_skybox.checked = self.settings.show_skybox
514        self._show_axes.checked = self.settings.show_axes
515        self._use_ibl.checked = self.settings.use_ibl
516        self._use_sun.checked = self.settings.use_sun
517        self._ibl_intensity.int_value = self.settings.ibl_intensity
518        self._sun_intensity.int_value = self.settings.sun_intensity
519        self._sun_dir.vector_value = self.settings.sun_dir
520        self._sun_color.color_value = self.settings.sun_color
521        self._material_prefab.enabled = (
522            self.settings.material.shader == Settings.LIT)
523        c = gui.Color(self.settings.material.base_color[0],
524                      self.settings.material.base_color[1],
525                      self.settings.material.base_color[2],
526                      self.settings.material.base_color[3])
527        self._material_color.color_value = c
528        self._point_size.double_value = self.settings.material.point_size
529
530    def _on_layout(self, layout_context):
531        # The on_layout callback should set the frame (position + size) of every
532        # child correctly. After the callback is done the window will layout
533        # the grandchildren.
534        r = self.window.content_rect
535        self._scene.frame = r
536        width = 17 * layout_context.theme.font_size
537        height = min(
538            r.height,
539            self._settings_panel.calc_preferred_size(
540                layout_context, gui.Widget.Constraints()).height)
541        self._settings_panel.frame = gui.Rect(r.get_right() - width, r.y, width,
542                                              height)
543
544    def _set_mouse_mode_rotate(self):
545        self._scene.set_view_controls(gui.SceneWidget.Controls.ROTATE_CAMERA)
546
547    def _set_mouse_mode_fly(self):
548        self._scene.set_view_controls(gui.SceneWidget.Controls.FLY)
549
550    def _set_mouse_mode_sun(self):
551        self._scene.set_view_controls(gui.SceneWidget.Controls.ROTATE_SUN)
552
553    def _set_mouse_mode_ibl(self):
554        self._scene.set_view_controls(gui.SceneWidget.Controls.ROTATE_IBL)
555
556    def _set_mouse_mode_model(self):
557        self._scene.set_view_controls(gui.SceneWidget.Controls.ROTATE_MODEL)
558
559    def _on_bg_color(self, new_color):
560        self.settings.bg_color = new_color
561        self._apply_settings()
562
563    def _on_show_skybox(self, show):
564        self.settings.show_skybox = show
565        self._apply_settings()
566
567    def _on_show_axes(self, show):
568        self.settings.show_axes = show
569        self._apply_settings()
570
571    def _on_use_ibl(self, use):
572        self.settings.use_ibl = use
573        self._profiles.selected_text = Settings.CUSTOM_PROFILE_NAME
574        self._apply_settings()
575
576    def _on_use_sun(self, use):
577        self.settings.use_sun = use
578        self._profiles.selected_text = Settings.CUSTOM_PROFILE_NAME
579        self._apply_settings()
580
581    def _on_lighting_profile(self, name, index):
582        if name != Settings.CUSTOM_PROFILE_NAME:
583            self.settings.apply_lighting_profile(name)
584            self._apply_settings()
585
586    def _on_new_ibl(self, name, index):
587        self.settings.new_ibl_name = gui.Application.instance.resource_path + "/" + name
588        self._profiles.selected_text = Settings.CUSTOM_PROFILE_NAME
589        self._apply_settings()
590
591    def _on_ibl_intensity(self, intensity):
592        self.settings.ibl_intensity = int(intensity)
593        self._profiles.selected_text = Settings.CUSTOM_PROFILE_NAME
594        self._apply_settings()
595
596    def _on_sun_intensity(self, intensity):
597        self.settings.sun_intensity = int(intensity)
598        self._profiles.selected_text = Settings.CUSTOM_PROFILE_NAME
599        self._apply_settings()
600
601    def _on_sun_dir(self, sun_dir):
602        self.settings.sun_dir = sun_dir
603        self._profiles.selected_text = Settings.CUSTOM_PROFILE_NAME
604        self._apply_settings()
605
606    def _on_sun_color(self, color):
607        self.settings.sun_color = color
608        self._apply_settings()
609
610    def _on_shader(self, name, index):
611        self.settings.set_material(AppWindow.MATERIAL_SHADERS[index])
612        self._apply_settings()
613
614    def _on_material_prefab(self, name, index):
615        self.settings.apply_material_prefab(name)
616        self.settings.apply_material = True
617        self._apply_settings()
618
619    def _on_material_color(self, color):
620        self.settings.material.base_color = [
621            color.red, color.green, color.blue, color.alpha
622        ]
623        self.settings.apply_material = True
624        self._apply_settings()
625
626    def _on_point_size(self, size):
627        self.settings.material.point_size = int(size)
628        self.settings.apply_material = True
629        self._apply_settings()
630
631    def _on_menu_open(self):
632        dlg = gui.FileDialog(gui.FileDialog.OPEN, "Choose file to load",
633                             self.window.theme)
634        dlg.add_filter(
635            ".ply .stl .fbx .obj .off .gltf .glb",
636            "Triangle mesh files (.ply, .stl, .fbx, .obj, .off, "
637            ".gltf, .glb)")
638        dlg.add_filter(
639            ".xyz .xyzn .xyzrgb .ply .pcd .pts",
640            "Point cloud files (.xyz, .xyzn, .xyzrgb, .ply, "
641            ".pcd, .pts)")
642        dlg.add_filter(".ply", "Polygon files (.ply)")
643        dlg.add_filter(".stl", "Stereolithography files (.stl)")
644        dlg.add_filter(".fbx", "Autodesk Filmbox files (.fbx)")
645        dlg.add_filter(".obj", "Wavefront OBJ files (.obj)")
646        dlg.add_filter(".off", "Object file format (.off)")
647        dlg.add_filter(".gltf", "OpenGL transfer files (.gltf)")
648        dlg.add_filter(".glb", "OpenGL binary transfer files (.glb)")
649        dlg.add_filter(".xyz", "ASCII point cloud files (.xyz)")
650        dlg.add_filter(".xyzn", "ASCII point cloud with normals (.xyzn)")
651        dlg.add_filter(".xyzrgb",
652                       "ASCII point cloud files with colors (.xyzrgb)")
653        dlg.add_filter(".pcd", "Point Cloud Data files (.pcd)")
654        dlg.add_filter(".pts", "3D Points files (.pts)")
655        dlg.add_filter("", "All files")
656
657        # A file dialog MUST define on_cancel and on_done functions
658        dlg.set_on_cancel(self._on_file_dialog_cancel)
659        dlg.set_on_done(self._on_load_dialog_done)
660        self.window.show_dialog(dlg)
661
662    def _on_file_dialog_cancel(self):
663        self.window.close_dialog()
664
665    def _on_load_dialog_done(self, filename):
666        self.window.close_dialog()
667        self.load(filename)
668
669    def _on_menu_export(self):
670        dlg = gui.FileDialog(gui.FileDialog.SAVE, "Choose file to save",
671                             self.window.theme)
672        dlg.add_filter(".png", "PNG files (.png)")
673        dlg.set_on_cancel(self._on_file_dialog_cancel)
674        dlg.set_on_done(self._on_export_dialog_done)
675        self.window.show_dialog(dlg)
676
677    def _on_export_dialog_done(self, filename):
678        self.window.close_dialog()
679        frame = self._scene.frame
680        self.export_image(filename, frame.width, frame.height)
681
682    def _on_menu_quit(self):
683        gui.Application.instance.quit()
684
685    def _on_menu_toggle_settings_panel(self):
686        self._settings_panel.visible = not self._settings_panel.visible
687        gui.Application.instance.menubar.set_checked(
688            AppWindow.MENU_SHOW_SETTINGS, self._settings_panel.visible)
689
690    def _on_menu_about(self):
691        # Show a simple dialog. Although the Dialog is actually a widget, you can
692        # treat it similar to a Window for layout and put all the widgets in a
693        # layout which you make the only child of the Dialog.
694        em = self.window.theme.font_size
695        dlg = gui.Dialog("About")
696
697        # Add the text
698        dlg_layout = gui.Vert(em, gui.Margins(em, em, em, em))
699        dlg_layout.add_child(gui.Label("Open3D GUI Example"))
700
701        # Add the Ok button. We need to define a callback function to handle
702        # the click.
703        ok = gui.Button("OK")
704        ok.set_on_clicked(self._on_about_ok)
705
706        # We want the Ok button to be an the right side, so we need to add
707        # a stretch item to the layout, otherwise the button will be the size
708        # of the entire row. A stretch item takes up as much space as it can,
709        # which forces the button to be its minimum size.
710        h = gui.Horiz()
711        h.add_stretch()
712        h.add_child(ok)
713        h.add_stretch()
714        dlg_layout.add_child(h)
715
716        dlg.add_child(dlg_layout)
717        self.window.show_dialog(dlg)
718
719    def _on_about_ok(self):
720        self.window.close_dialog()
721
722    def load(self, path):
723        self._scene.scene.clear_geometry()
724
725        geometry = None
726        geometry_type = o3d.io.read_file_geometry_type(path)
727
728        mesh = None
729        if geometry_type & o3d.io.CONTAINS_TRIANGLES:
730            mesh = o3d.io.read_triangle_model(path)
731        if mesh is None:
732            print("[Info]", path, "appears to be a point cloud")
733            cloud = None
734            try:
735                cloud = o3d.io.read_point_cloud(path)
736            except Exception:
737                pass
738            if cloud is not None:
739                print("[Info] Successfully read", path)
740                if not cloud.has_normals():
741                    cloud.estimate_normals()
742                cloud.normalize_normals()
743                geometry = cloud
744            else:
745                print("[WARNING] Failed to read points", path)
746
747        if geometry is not None or mesh is not None:
748            try:
749                if mesh is not None:
750                    # Triangle model
751                    self._scene.scene.add_model("__model__", mesh)
752                else:
753                    # Point cloud
754                    self._scene.scene.add_geometry("__model__", geometry,
755                                                   self.settings.material)
756                bounds = self._scene.scene.bounding_box
757                self._scene.setup_camera(60, bounds, bounds.get_center())
758            except Exception as e:
759                print(e)
760
761    def export_image(self, path, width, height):
762
763        def on_image(image):
764            img = image
765
766            quality = 9  # png
767            if path.endswith(".jpg"):
768                quality = 100
769            o3d.io.write_image(path, img, quality)
770
771        self._scene.scene.scene.render_to_image(on_image)
772
773
774def main():
775    # We need to initialize the application, which finds the necessary shaders
776    # for rendering and prepares the cross-platform window abstraction.
777    gui.Application.instance.initialize()
778
779    w = AppWindow(1024, 768)
780
781    if len(sys.argv) > 1:
782        path = sys.argv[1]
783        if os.path.exists(path):
784            w.load(path)
785        else:
786            w.window.show_message_box("Error",
787                                      "Could not open file '" + path + "'")
788
789    # Run the event loop. This will not return until the last window is closed.
790    gui.Application.instance.run()
791
792
793if __name__ == "__main__":
794    main()