Visualization#

add_geometry.py#

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

all_widgets.py#

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

customized_visualization.py#

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

customized_visualization_key_action.py#

 1# ----------------------------------------------------------------------------
 2# -                        Open3D: www.open3d.org                            -
 3# ----------------------------------------------------------------------------
 4# Copyright (c) 2018-2024 www.open3d.org
 5# SPDX-License-Identifier: MIT
 6# ----------------------------------------------------------------------------
 7
 8import open3d as o3d
 9
10
11def custom_key_action_without_kb_repeat_delay(pcd):
12    rotating = False
13
14    vis = o3d.visualization.VisualizerWithKeyCallback()
15
16    def key_action_callback(vis, action, mods):
17        nonlocal rotating
18        print(action)
19        if action == 1:  # key down
20            rotating = True
21        elif action == 0:  # key up
22            rotating = False
23        elif action == 2:  # key repeat
24            pass
25        return True
26
27    def animation_callback(vis):
28        nonlocal rotating
29        if rotating:
30            ctr = vis.get_view_control()
31            ctr.rotate(10.0, 0.0)
32
33    # key_action_callback will be triggered when there's a keyboard press, release or repeat event
34    vis.register_key_action_callback(32, key_action_callback)  # space
35
36    # animation_callback is always repeatedly called by the visualizer
37    vis.register_animation_callback(animation_callback)
38
39    vis.create_window()
40    vis.add_geometry(pcd)
41    vis.run()
42
43
44def custom_mouse_action(pcd):
45
46    vis = o3d.visualization.VisualizerWithKeyCallback()
47    buttons = ['left', 'right', 'middle']
48    actions = ['up', 'down']
49    mods_name = ['shift', 'ctrl', 'alt', 'cmd']
50
51    def on_key_action(vis, action, mods):
52        print("on_key_action", action, mods)
53
54    vis.register_key_action_callback(ord("A"), on_key_action)
55
56    def on_mouse_move(vis, x, y):
57        print(f"on_mouse_move({x:.2f}, {y:.2f})")
58
59    def on_mouse_scroll(vis, x, y):
60        print(f"on_mouse_scroll({x:.2f}, {y:.2f})")
61
62    def on_mouse_button(vis, button, action, mods):
63        pressed_mods = " ".join(
64            [mods_name[i] for i in range(4) if mods & (1 << i)])
65        print(f"on_mouse_button: {buttons[button]}, {actions[action]}, " +
66              pressed_mods)
67
68    vis.register_mouse_move_callback(on_mouse_move)
69    vis.register_mouse_scroll_callback(on_mouse_scroll)
70    vis.register_mouse_button_callback(on_mouse_button)
71
72    vis.create_window()
73    vis.add_geometry(pcd)
74    vis.run()
75
76
77if __name__ == "__main__":
78    ply_data = o3d.data.PLYPointCloud()
79    pcd = o3d.io.read_point_cloud(ply_data.path)
80
81    print("Customized visualization with smooth key action "
82          "(without keyboard repeat delay). Press the space-bar.")
83    custom_key_action_without_kb_repeat_delay(pcd)
84    print("Customized visualization with mouse action.")
85    custom_mouse_action(pcd)

demo_scene.py#

  1# ----------------------------------------------------------------------------
  2# -                        Open3D: www.open3d.org                            -
  3# ----------------------------------------------------------------------------
  4# Copyright (c) 2018-2024 www.open3d.org
  5# SPDX-License-Identifier: MIT
  6# ----------------------------------------------------------------------------
  7"""Demo scene demonstrating models, built-in shapes, and materials"""
  8
  9import numpy as np
 10import open3d as o3d
 11import open3d.visualization as vis
 12
 13
 14def create_scene():
 15    """Creates the geometry and materials for the demo scene and returns a
 16    dictionary suitable for draw call
 17    """
 18    # Create some shapes for our scene
 19    a_cube = o3d.geometry.TriangleMesh.create_box(2,
 20                                                  4,
 21                                                  4,
 22                                                  create_uv_map=True,
 23                                                  map_texture_to_each_face=True)
 24    a_cube.compute_triangle_normals()
 25    a_cube.translate((-5, 0, -2))
 26    a_cube = o3d.t.geometry.TriangleMesh.from_legacy(a_cube)
 27
 28    a_sphere = o3d.geometry.TriangleMesh.create_sphere(2.5,
 29                                                       resolution=40,
 30                                                       create_uv_map=True)
 31    a_sphere.compute_vertex_normals()
 32    rotate_90 = o3d.geometry.get_rotation_matrix_from_xyz((-np.pi / 2, 0, 0))
 33    a_sphere.rotate(rotate_90)
 34    a_sphere.translate((5, 2.4, 0))
 35    a_sphere = o3d.t.geometry.TriangleMesh.from_legacy(a_sphere)
 36
 37    a_cylinder = o3d.geometry.TriangleMesh.create_cylinder(
 38        1.0, 4.0, 30, 4, True)
 39    a_cylinder.compute_triangle_normals()
 40    a_cylinder.rotate(rotate_90)
 41    a_cylinder.translate((10, 2, 0))
 42    a_cylinder = o3d.t.geometry.TriangleMesh.from_legacy(a_cylinder)
 43
 44    a_ico = o3d.geometry.TriangleMesh.create_icosahedron(1.25,
 45                                                         create_uv_map=True)
 46    a_ico.compute_triangle_normals()
 47    a_ico.translate((-10, 2, 0))
 48    a_ico = o3d.t.geometry.TriangleMesh.from_legacy(a_ico)
 49
 50    # Load an OBJ model for our scene
 51    helmet_data = o3d.data.FlightHelmetModel()
 52    helmet = o3d.io.read_triangle_model(helmet_data.path)
 53    helmet_parts = o3d.t.geometry.TriangleMesh.from_triangle_mesh_model(helmet)
 54
 55    # Create a ground plane
 56    ground_plane = o3d.geometry.TriangleMesh.create_box(
 57        50.0, 0.1, 50.0, create_uv_map=True, map_texture_to_each_face=True)
 58    ground_plane.compute_triangle_normals()
 59    rotate_180 = o3d.geometry.get_rotation_matrix_from_xyz((-np.pi, 0, 0))
 60    ground_plane.rotate(rotate_180)
 61    ground_plane.translate((-25.0, -0.1, -25.0))
 62    ground_plane.paint_uniform_color((1, 1, 1))
 63    ground_plane = o3d.t.geometry.TriangleMesh.from_legacy(ground_plane)
 64
 65    # Material to make ground plane more interesting - a rough piece of glass
 66    ground_plane.material = vis.Material("defaultLitSSR")
 67    ground_plane.material.scalar_properties['roughness'] = 0.15
 68    ground_plane.material.scalar_properties['reflectance'] = 0.72
 69    ground_plane.material.scalar_properties['transmission'] = 0.6
 70    ground_plane.material.scalar_properties['thickness'] = 0.3
 71    ground_plane.material.scalar_properties['absorption_distance'] = 0.1
 72    ground_plane.material.vector_properties['absorption_color'] = np.array(
 73        [0.82, 0.98, 0.972, 1.0])
 74    painted_plaster_texture_data = o3d.data.PaintedPlasterTexture()
 75    ground_plane.material.texture_maps['albedo'] = o3d.t.io.read_image(
 76        painted_plaster_texture_data.albedo_texture_path)
 77    ground_plane.material.texture_maps['normal'] = o3d.t.io.read_image(
 78        painted_plaster_texture_data.normal_texture_path)
 79    ground_plane.material.texture_maps['roughness'] = o3d.t.io.read_image(
 80        painted_plaster_texture_data.roughness_texture_path)
 81
 82    # Load textures and create materials for each of our demo items
 83    wood_floor_texture_data = o3d.data.WoodFloorTexture()
 84    a_cube.material = vis.Material('defaultLit')
 85    a_cube.material.texture_maps['albedo'] = o3d.t.io.read_image(
 86        wood_floor_texture_data.albedo_texture_path)
 87    a_cube.material.texture_maps['normal'] = o3d.t.io.read_image(
 88        wood_floor_texture_data.normal_texture_path)
 89    a_cube.material.texture_maps['roughness'] = o3d.t.io.read_image(
 90        wood_floor_texture_data.roughness_texture_path)
 91
 92    tiles_texture_data = o3d.data.TilesTexture()
 93    a_sphere.material = vis.Material('defaultLit')
 94    a_sphere.material.texture_maps['albedo'] = o3d.t.io.read_image(
 95        tiles_texture_data.albedo_texture_path)
 96    a_sphere.material.texture_maps['normal'] = o3d.t.io.read_image(
 97        tiles_texture_data.normal_texture_path)
 98    a_sphere.material.texture_maps['roughness'] = o3d.t.io.read_image(
 99        tiles_texture_data.roughness_texture_path)
100
101    terrazzo_texture_data = o3d.data.TerrazzoTexture()
102    a_ico.material = vis.Material('defaultLit')
103    a_ico.material.texture_maps['albedo'] = o3d.t.io.read_image(
104        terrazzo_texture_data.albedo_texture_path)
105    a_ico.material.texture_maps['normal'] = o3d.t.io.read_image(
106        terrazzo_texture_data.normal_texture_path)
107    a_ico.material.texture_maps['roughness'] = o3d.t.io.read_image(
108        terrazzo_texture_data.roughness_texture_path)
109
110    metal_texture_data = o3d.data.MetalTexture()
111    a_cylinder.material = vis.Material('defaultLit')
112    a_cylinder.material.texture_maps['albedo'] = o3d.t.io.read_image(
113        metal_texture_data.albedo_texture_path)
114    a_cylinder.material.texture_maps['normal'] = o3d.t.io.read_image(
115        metal_texture_data.normal_texture_path)
116    a_cylinder.material.texture_maps['roughness'] = o3d.t.io.read_image(
117        metal_texture_data.roughness_texture_path)
118    a_cylinder.material.texture_maps['metallic'] = o3d.t.io.read_image(
119        metal_texture_data.metallic_texture_path)
120
121    geoms = [{
122        "name": "plane",
123        "geometry": ground_plane
124    }, {
125        "name": "cube",
126        "geometry": a_cube
127    }, {
128        "name": "cylinder",
129        "geometry": a_cylinder
130    }, {
131        "name": "ico",
132        "geometry": a_ico
133    }, {
134        "name": "sphere",
135        "geometry": a_sphere
136    }]
137    # Load the helmet
138    for name, tmesh in helmet_parts.items():
139        geoms.append({
140            "name": name,
141            "geometry": tmesh.scale(10.0, (0.0, 0.0, 0.0))
142        })
143    return geoms
144
145
146if __name__ == "__main__":
147    geoms = create_scene()
148    vis.draw(geoms,
149             bg_color=(0.8, 0.9, 0.9, 1.0),
150             show_ui=True,
151             width=1920,
152             height=1080)

draw.py#

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

draw_webrtc.py#

 1# ----------------------------------------------------------------------------
 2# -                        Open3D: www.open3d.org                            -
 3# ----------------------------------------------------------------------------
 4# Copyright (c) 2018-2024 www.open3d.org
 5# SPDX-License-Identifier: MIT
 6# ----------------------------------------------------------------------------
 7
 8import open3d as o3d
 9
10if __name__ == "__main__":
11    o3d.visualization.webrtc_server.enable_webrtc()
12    cube_red = o3d.geometry.TriangleMesh.create_box(1, 2, 4)
13    cube_red.compute_vertex_normals()
14    cube_red.paint_uniform_color((1.0, 0.0, 0.0))
15    o3d.visualization.draw(cube_red)

headless_rendering.py#

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

interactive_visualization.py#

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

line_width.py#

 1# ----------------------------------------------------------------------------
 2# -                        Open3D: www.open3d.org                            -
 3# ----------------------------------------------------------------------------
 4# Copyright (c) 2018-2024 www.open3d.org
 5# SPDX-License-Identifier: MIT
 6# ----------------------------------------------------------------------------
 7
 8import open3d as o3d
 9import random
10
11NUM_LINES = 10
12
13
14def random_point():
15    return [5 * random.random(), 5 * random.random(), 5 * random.random()]
16
17
18def main():
19    pts = [random_point() for _ in range(0, 2 * NUM_LINES)]
20    line_indices = [[2 * i, 2 * i + 1] for i in range(0, NUM_LINES)]
21    colors = [[0.0, 0.0, 0.0] for _ in range(0, NUM_LINES)]
22
23    lines = o3d.geometry.LineSet()
24    lines.points = o3d.utility.Vector3dVector(pts)
25    lines.lines = o3d.utility.Vector2iVector(line_indices)
26    # The default color of the lines is white, which will be invisible on the
27    # default white background. So we either need to set the color of the lines
28    # or the base_color of the material.
29    lines.colors = o3d.utility.Vector3dVector(colors)
30
31    # Some platforms do not require OpenGL implementations to support wide lines,
32    # so the renderer requires a custom shader to implement this: "unlitLine".
33    # The line_width field is only used by this shader; all other shaders ignore
34    # it.
35    mat = o3d.visualization.rendering.MaterialRecord()
36    mat.shader = "unlitLine"
37    mat.line_width = 10  # note that this is scaled with respect to pixels,
38    # so will give different results depending on the
39    # scaling values of your system
40    o3d.visualization.draw({
41        "name": "lines",
42        "geometry": lines,
43        "material": mat
44    })
45
46
47if __name__ == "__main__":
48    main()

load_save_viewpoint.py#

 1# ----------------------------------------------------------------------------
 2# -                        Open3D: www.open3d.org                            -
 3# ----------------------------------------------------------------------------
 4# Copyright (c) 2018-2024 www.open3d.org
 5# SPDX-License-Identifier: MIT
 6# ----------------------------------------------------------------------------
 7
 8import open3d as o3d
 9
10
11def save_view_point(pcd, filename):
12    vis = o3d.visualization.Visualizer()
13    vis.create_window()
14    vis.add_geometry(pcd)
15    vis.run()  # user changes the view and press "q" to terminate
16    param = vis.get_view_control().convert_to_pinhole_camera_parameters()
17    o3d.io.write_pinhole_camera_parameters(filename, param)
18    vis.destroy_window()
19
20
21def load_view_point(pcd, filename):
22    vis = o3d.visualization.Visualizer()
23    vis.create_window()
24    ctr = vis.get_view_control()
25    param = o3d.io.read_pinhole_camera_parameters(filename)
26    vis.add_geometry(pcd)
27    ctr.convert_from_pinhole_camera_parameters(param)
28    vis.run()
29    vis.destroy_window()
30
31
32if __name__ == "__main__":
33    pcd_data = o3d.data.PCDPointCloud()
34    pcd = o3d.io.read_point_cloud(pcd_data.path)
35    save_view_point(pcd, "viewpoint.json")
36    load_view_point(pcd, "viewpoint.json")

mitsuba_material_estimation.py#

  1# ----------------------------------------------------------------------------
  2# -                        Open3D: www.open3d.org                            -
  3# ----------------------------------------------------------------------------
  4# Copyright (c) 2018-2024 www.open3d.org
  5# SPDX-License-Identifier: MIT
  6# ----------------------------------------------------------------------------
  7
  8import sys
  9import argparse
 10from pathlib import Path
 11import open3d as o3d
 12import mitsuba as mi
 13import drjit as dr
 14import numpy as np
 15import math
 16
 17
 18def make_mitsuba_scene(mesh, cam_xform, fov, width, height, principle_pts,
 19                       envmap):
 20    # Camera transform
 21    t_from_np = mi.ScalarTransform4f(cam_xform)
 22    # Transform necessary to get from Open3D's environment map coordinate system
 23    # to Mitsuba's
 24    env_t = mi.ScalarTransform4f.rotate(axis=[0, 0, 1],
 25                                        angle=90).rotate(axis=[1, 0, 0],
 26                                                         angle=90)
 27    scene_dict = {
 28        "type": "scene",
 29        "integrator": {
 30            'type': 'path'
 31        },
 32        "light": {
 33            "type": "envmap",
 34            "to_world": env_t,
 35            "bitmap": mi.Bitmap(envmap),
 36        },
 37        "sensor": {
 38            "type": "perspective",
 39            "fov": fov,
 40            "to_world": t_from_np,
 41            "principal_point_offset_x": principle_pts[0],
 42            "principal_point_offset_y": principle_pts[1],
 43            "thefilm": {
 44                "type": "hdrfilm",
 45                "width": width,
 46                "height": height,
 47            },
 48            "thesampler": {
 49                "type": "multijitter",
 50                "sample_count": 64,
 51            },
 52        },
 53        "themesh": mesh,
 54    }
 55
 56    scene = mi.load_dict(scene_dict)
 57    return scene
 58
 59
 60def run_estimation(mesh, cam_info, ref_image, env_width, iterations, tv_alpha):
 61    # Make Mitsuba mesh from Open3D mesh -- conversion will attach a Mitsuba
 62    # Principled BSDF to the mesh
 63    mesh_opt = mesh.to_mitsuba('themesh')
 64
 65    # Prepare empty environment map
 66    empty_envmap = np.ones((int(env_width / 2), env_width, 3))
 67
 68    # Create Mitsuba scene
 69    scene = make_mitsuba_scene(mesh_opt, cam_info[0], cam_info[1], cam_info[2],
 70                               cam_info[3], cam_info[4], empty_envmap)
 71
 72    def total_variation(image, alpha):
 73        diff1 = image[1:, :, :] - image[:-1, :, :]
 74        diff2 = image[:, 1:, :] - image[:, :-1, :]
 75        return alpha * (dr.sum(dr.abs(diff1)) / len(diff1) +
 76                        dr.sum(dr.abs(diff2)) / len(diff2))
 77
 78    def mse(image, ref_img):
 79        return dr.mean(dr.sqr(image - ref_img))
 80
 81    params = mi.traverse(scene)
 82    print(params)
 83
 84    # Create a Mitsuba Optimizer and configure it to optimize albedo and
 85    # environment maps
 86    opt = mi.ad.Adam(lr=0.05, mask_updates=True)
 87    opt['themesh.bsdf.base_color.data'] = params['themesh.bsdf.base_color.data']
 88    opt['light.data'] = params['light.data']
 89    params.update(opt)
 90
 91    integrator = mi.load_dict({'type': 'prb'})
 92    for i in range(iterations):
 93        img = mi.render(scene, params, spp=8, seed=i, integrator=integrator)
 94
 95        # Compute loss
 96        loss = mse(img, ref_image)
 97        # Apply TV regularization if requested
 98        if tv_alpha > 0.0:
 99            loss = loss + total_variation(opt['themesh.bsdf.base_color.data'],
100                                          tv_alpha)
101
102        # Backpropogate and step. Note: if we were optimizing over a larger set
103        # of inputs not just a single image we might want to step only every x
104        # number of inputs
105        dr.backward(loss)
106        opt.step()
107
108        # Make sure albedo values stay in allowed range
109        opt['themesh.bsdf.base_color.data'] = dr.clamp(
110            opt['themesh.bsdf.base_color.data'], 0.0, 1.0)
111        params.update(opt)
112        print(f'Iteration {i} complete')
113
114    # Done! Return the estimated maps
115    albedo_img = params['themesh.bsdf.base_color.data'].numpy()
116    envmap_img = params['light.data'].numpy()
117    return (albedo_img, envmap_img)
118
119
120def load_input_mesh(model_path, tex_dim):
121    mesh = o3d.t.io.read_triangle_mesh(model_path)
122    mesh.material.set_default_properties()
123    mesh.material.material_name = 'defaultLit'  # note: ignored by Mitsuba, just used to visualize in Open3D
124    mesh.material.texture_maps['albedo'] = o3d.t.geometry.Image(0.5 + np.zeros(
125        (tex_dim, tex_dim, 3), dtype=np.float32))
126    return mesh
127
128
129def load_input_data(object, camera_pose, input_image, tex_dim):
130    print(f'Loading {object}...')
131    mesh = load_input_mesh(object, tex_dim)
132
133    print(f'Loading camera pose from {camera_pose}...')
134    cam_npz = np.load(camera_pose)
135    img_width = cam_npz['width'].item()
136    img_height = cam_npz['height'].item()
137    cam_xform = np.linalg.inv(cam_npz['T'])
138    cam_xform = np.matmul(
139        cam_xform,
140        np.array([[-1, 0, 0, 0], [0, -1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]],
141                 dtype=np.float32))
142    fov = 2 * np.arctan(0.5 * img_width / cam_npz['K'][0, 0])
143    fov = (180.0 / math.pi) * fov.item()
144    camera = (cam_xform, fov, img_width, img_height, (0.0, 0.0))
145
146    print(f'Loading reference image from {input_image}...')
147    ref_img = o3d.t.io.read_image(str(input_image))
148    ref_img = ref_img.as_tensor()[:, :, 0:3].to(o3d.core.Dtype.Float32) / 255.0
149    bmp = mi.Bitmap(ref_img.numpy()).convert(srgb_gamma=False)
150    ref_img = mi.TensorXf(bmp)
151    return (mesh, camera, ref_img)
152
153
154if __name__ == '__main__':
155    parser = argparse.ArgumentParser(
156        description=
157        "Script that estimates texture and environment map from an input image and geometry. You can find data to test this script here: https://github.com/isl-org/open3d_downloads/releases/download/mitsuba-demos/raven_mitsuba.zip.",
158        formatter_class=argparse.ArgumentDefaultsHelpFormatter)
159    parser.add_argument(
160        'object_path',
161        type=Path,
162        help=
163        "Path to geometry for which to estimate albedo. It is assumed that in the same directory will be an object-name.npz which contains the camera pose information and an object-name.png which is the input image"
164    )
165    parser.add_argument('--env-width', type=int, default=1024)
166    parser.add_argument('--tex-width',
167                        type=int,
168                        default=2048,
169                        help="The dimensions of the texture")
170    parser.add_argument(
171        '--device',
172        default='cuda' if o3d.core.cuda.is_available() else 'cpu',
173        choices=('cpu', 'cuda'),
174        help="Run Mitsuba on 'cuda' or 'cpu'")
175    parser.add_argument('--iterations',
176                        type=int,
177                        default=40,
178                        help="Number of iterations")
179    parser.add_argument(
180        '--total-variation',
181        type=float,
182        default=0.01,
183        help="Factor to apply to total_variation loss. 0.0 disables TV")
184
185    if len(sys.argv) < 2:
186        parser.print_help(sys.stderr)
187        sys.exit(1)
188    args = parser.parse_args()
189    print("Arguments: ", vars(args))
190
191    # Initialize Mitsuba
192    if args.device == 'cpu':
193        mi.set_variant('llvm_ad_rgb')
194    else:
195        mi.set_variant('cuda_ad_rgb')
196
197    # Confirm that the 3 required inputs exist
198    object_path = args.object_path
199    object_name = object_path.stem
200    datadir = args.object_path.parent
201    camera_pose = datadir / (object_name + '.npz')
202    input_image = datadir / (object_name + '.png')
203    if not object_path.exists():
204        print(f'{object_path} does not exist!')
205        sys.exit()
206    if not camera_pose.exists():
207        print(f'{camera_pose} does not exist!')
208        sys.exit()
209    if not input_image.exists():
210        print(f'{input_image} does not exist!')
211        sys.exit()
212
213    # Load input data
214    mesh, cam_info, input_image = load_input_data(object_path, camera_pose,
215                                                  input_image, args.tex_width)
216
217    # Estimate albedo map
218    print('Running material estimation...')
219    albedo, envmap = run_estimation(mesh, cam_info, input_image, args.env_width,
220                                    args.iterations, args.total_variation)
221
222    # Save maps
223    def save_image(img, name, output_dir):
224        # scale to 0-255
225        texture = o3d.core.Tensor(img * 255.0).to(o3d.core.Dtype.UInt8)
226        texture = o3d.t.geometry.Image(texture)
227        o3d.t.io.write_image(str(output_dir / name), texture)
228
229    print('Saving final results...')
230    save_image(albedo, 'estimated_albedo.png', datadir)
231    mi.Bitmap(envmap).write(str(datadir / 'predicted_envmap.exr'))
232
233    # Visualize result with Open3D
234    mesh.material.texture_maps['albedo'] = o3d.t.io.read_image(
235        str(datadir / 'estimated_albedo.png'))
236    o3d.visualization.draw(mesh)

mouse_and_point_coord.py#

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

multiple_windows.py#

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

non_blocking_visualization.py#

 1# ----------------------------------------------------------------------------
 2# -                        Open3D: www.open3d.org                            -
 3# ----------------------------------------------------------------------------
 4# Copyright (c) 2018-2024 www.open3d.org
 5# SPDX-License-Identifier: MIT
 6# ----------------------------------------------------------------------------
 7
 8# examples/python/visualization/non_blocking_visualization.py
 9
10import open3d as o3d
11import numpy as np
12
13
14def prepare_data():
15    pcd_data = o3d.data.DemoICPPointClouds()
16    source_raw = o3d.io.read_point_cloud(pcd_data.paths[0])
17    target_raw = o3d.io.read_point_cloud(pcd_data.paths[1])
18    source = source_raw.voxel_down_sample(voxel_size=0.02)
19    target = target_raw.voxel_down_sample(voxel_size=0.02)
20
21    trans = [[0.862, 0.011, -0.507, 0.0], [-0.139, 0.967, -0.215, 0.7],
22             [0.487, 0.255, 0.835, -1.4], [0.0, 0.0, 0.0, 1.0]]
23    source.transform(trans)
24    flip_transform = [[1, 0, 0, 0], [0, -1, 0, 0], [0, 0, -1, 0], [0, 0, 0, 1]]
25    source.transform(flip_transform)
26    target.transform(flip_transform)
27    return source, target
28
29
30def demo_non_blocking_visualization():
31    o3d.utility.set_verbosity_level(o3d.utility.VerbosityLevel.Debug)
32
33    source, target = prepare_data()
34    vis = o3d.visualization.Visualizer()
35    vis.create_window()
36    vis.add_geometry(source)
37    vis.add_geometry(target)
38    threshold = 0.05
39    icp_iteration = 100
40    save_image = False
41
42    for i in range(icp_iteration):
43        reg_p2l = o3d.pipelines.registration.registration_icp(
44            source, target, threshold, np.identity(4),
45            o3d.pipelines.registration.TransformationEstimationPointToPlane(),
46            o3d.pipelines.registration.ICPConvergenceCriteria(max_iteration=1))
47        source.transform(reg_p2l.transformation)
48        vis.update_geometry(source)
49        vis.poll_events()
50        vis.update_renderer()
51        if save_image:
52            vis.capture_screen_image("temp_%04d.jpg" % i)
53    vis.destroy_window()
54
55    o3d.utility.set_verbosity_level(o3d.utility.VerbosityLevel.Info)
56
57
58if __name__ == '__main__':
59    demo_non_blocking_visualization()

non_english.py#

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

online_processing.py#

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

remote_visualizer.py#

 1# ----------------------------------------------------------------------------
 2# -                        Open3D: www.open3d.org                            -
 3# ----------------------------------------------------------------------------
 4# Copyright (c) 2018-2024 www.open3d.org
 5# SPDX-License-Identifier: MIT
 6# ----------------------------------------------------------------------------
 7"""This example shows Open3D's remote visualization feature using RPC
 8communication. To run this example, start the client first by running
 9
10python remote_visualizer.py client
11
12and then run the server by running
13
14python remote_visualizer.py server
15
16Port 51454 is used by default for communication. For remote visualization (client
17and server running on different machines), use ssh to forward the remote server
18port to your local computer:
19
20    ssh -N -R 51454:localhost:51454 user@remote_host
21
22See documentation for more details (e.g. to use a different port).
23"""
24import sys
25import numpy as np
26import open3d as o3d
27import open3d.visualization as vis
28
29
30def make_point_cloud(npts, center, radius, colorize):
31    pts = np.random.uniform(-radius, radius, size=[npts, 3]) + center
32    cloud = o3d.geometry.PointCloud()
33    cloud.points = o3d.utility.Vector3dVector(pts)
34    if colorize:
35        colors = np.random.uniform(0.0, 1.0, size=[npts, 3])
36        cloud.colors = o3d.utility.Vector3dVector(colors)
37    return cloud
38
39
40def server_time_animation():
41    orig = make_point_cloud(200, (0, 0, 0), 1.0, True)
42    clouds = [{"name": "t=0", "geometry": orig, "time": 0}]
43    drift_dir = (1.0, 0.0, 0.0)
44    expand = 1.0
45    n = 20
46    ev = o3d.visualization.ExternalVisualizer()
47    for i in range(1, n):
48        amount = float(i) / float(n - 1)
49        cloud = o3d.geometry.PointCloud()
50        pts = np.asarray(orig.points)
51        pts = pts * (1.0 + amount * expand) + [amount * v for v in drift_dir]
52        cloud.points = o3d.utility.Vector3dVector(pts)
53        cloud.colors = orig.colors
54        ev.set(obj=cloud, time=i, path=f"points at t={i}")
55        print('.', end='', flush=True)
56    print()
57
58
59def client_time_animation():
60    o3d.visualization.draw(title="Open3D - Remote Visualizer Client",
61                           show_ui=True,
62                           rpc_interface=True)
63
64
65if __name__ == "__main__":
66    assert len(sys.argv) == 2 and sys.argv[1] in ('client', 'server'), (
67        "Usage: python remote_visualizer.py [client|server]")
68    if sys.argv[1] == "client":
69        client_time_animation()
70    elif sys.argv[1] == "server":
71        server_time_animation()

remove_geometry.py#

 1# ----------------------------------------------------------------------------
 2# -                        Open3D: www.open3d.org                            -
 3# ----------------------------------------------------------------------------
 4# Copyright (c) 2018-2024 www.open3d.org
 5# SPDX-License-Identifier: MIT
 6# ----------------------------------------------------------------------------
 7
 8import open3d as o3d
 9import numpy as np
10import time
11import copy
12
13
14def visualize_non_blocking(vis, pcds):
15    for pcd in pcds:
16        vis.update_geometry(pcd)
17    vis.poll_events()
18    vis.update_renderer()
19
20
21pcd_data = o3d.data.PCDPointCloud()
22pcd_orig = o3d.io.read_point_cloud(pcd_data.path)
23flip_transform = [[1, 0, 0, 0], [0, -1, 0, 0], [0, 0, -1, 0], [0, 0, 0, 1]]
24pcd_orig.transform(flip_transform)
25n_pcd = 5
26pcds = []
27for i in range(n_pcd):
28    pcds.append(copy.deepcopy(pcd_orig))
29    trans = np.identity(4)
30    trans[:3, 3] = [3 * i, 0, 0]
31    pcds[i].transform(trans)
32
33vis = o3d.visualization.Visualizer()
34vis.create_window()
35start_time = time.time()
36added = [False] * n_pcd
37
38curr_sec = int(time.time() - start_time)
39prev_sec = curr_sec - 1
40
41while True:
42    curr_sec = int(time.time() - start_time)
43    if curr_sec - prev_sec == 1:
44        prev_sec = curr_sec
45
46        for i in range(n_pcd):
47            if curr_sec % (n_pcd * 2) == i and not added[i]:
48                vis.add_geometry(pcds[i])
49                added[i] = True
50                print("Adding %d" % i)
51            if curr_sec % (n_pcd * 2) == (i + n_pcd) and added[i]:
52                vis.remove_geometry(pcds[i])
53                added[i] = False
54                print("Removing %d" % i)
55
56    visualize_non_blocking(vis, pcds)

render_to_image.py#

 1# ----------------------------------------------------------------------------
 2# -                        Open3D: www.open3d.org                            -
 3# ----------------------------------------------------------------------------
 4# Copyright (c) 2018-2024 www.open3d.org
 5# SPDX-License-Identifier: MIT
 6# ----------------------------------------------------------------------------
 7
 8import open3d as o3d
 9import open3d.visualization.rendering as rendering
10
11
12def main():
13    render = rendering.OffscreenRenderer(640, 480)
14
15    yellow = rendering.MaterialRecord()
16    yellow.base_color = [1.0, 0.75, 0.0, 1.0]
17    yellow.shader = "defaultLit"
18
19    green = rendering.MaterialRecord()
20    green.base_color = [0.0, 0.5, 0.0, 1.0]
21    green.shader = "defaultLit"
22
23    grey = rendering.MaterialRecord()
24    grey.base_color = [0.7, 0.7, 0.7, 1.0]
25    grey.shader = "defaultLit"
26
27    white = rendering.MaterialRecord()
28    white.base_color = [1.0, 1.0, 1.0, 1.0]
29    white.shader = "defaultLit"
30
31    cyl = o3d.geometry.TriangleMesh.create_cylinder(.05, 3)
32    cyl.compute_vertex_normals()
33    cyl.translate([-2, 0, 1.5])
34    sphere = o3d.geometry.TriangleMesh.create_sphere(.2)
35    sphere.compute_vertex_normals()
36    sphere.translate([-2, 0, 3])
37
38    box = o3d.geometry.TriangleMesh.create_box(2, 2, 1)
39    box.compute_vertex_normals()
40    box.translate([-1, -1, 0])
41    solid = o3d.geometry.TriangleMesh.create_icosahedron(0.5)
42    solid.compute_triangle_normals()
43    solid.compute_vertex_normals()
44    solid.translate([0, 0, 1.75])
45
46    render.scene.add_geometry("cyl", cyl, green)
47    render.scene.add_geometry("sphere", sphere, yellow)
48    render.scene.add_geometry("box", box, grey)
49    render.scene.add_geometry("solid", solid, white)
50    render.setup_camera(60.0, [0, 0, 0], [0, 10, 0], [0, 0, 1])
51    render.scene.scene.set_sun_light([0.707, 0.0, -.707], [1.0, 1.0, 1.0],
52                                     75000)
53    render.scene.scene.enable_sun_light(True)
54    render.scene.show_axes(True)
55
56    img = render.render_to_image()
57    print("Saving image at test.png")
58    o3d.io.write_image("test.png", img, 9)
59
60    render.setup_camera(60.0, [0, 0, 0], [-10, 0, 0], [0, 0, 1])
61    img = render.render_to_image()
62    print("Saving image at test2.png")
63    o3d.io.write_image("test2.png", img, 9)
64
65
66if __name__ == "__main__":
67    main()

tensorboard_pytorch.py#

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

tensorboard_tensorflow.py#

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

text3d.py#

 1# ----------------------------------------------------------------------------
 2# -                        Open3D: www.open3d.org                            -
 3# ----------------------------------------------------------------------------
 4# Copyright (c) 2018-2024 www.open3d.org
 5# SPDX-License-Identifier: MIT
 6# ----------------------------------------------------------------------------
 7
 8import numpy as np
 9import open3d as o3d
10import open3d.visualization.gui as gui
11import open3d.visualization.rendering as rendering
12
13
14def make_point_cloud(npts, center, radius):
15    pts = np.random.uniform(-radius, radius, size=[npts, 3]) + center
16    cloud = o3d.geometry.PointCloud()
17    cloud.points = o3d.utility.Vector3dVector(pts)
18    colors = np.random.uniform(0.0, 1.0, size=[npts, 3])
19    cloud.colors = o3d.utility.Vector3dVector(colors)
20    return cloud
21
22
23def high_level():
24    app = gui.Application.instance
25    app.initialize()
26
27    points = make_point_cloud(100, (0, 0, 0), 1.0)
28
29    vis = o3d.visualization.O3DVisualizer("Open3D - 3D Text", 1024, 768)
30    vis.show_settings = True
31    vis.add_geometry("Points", points)
32    for idx in range(0, len(points.points)):
33        vis.add_3d_label(points.points[idx], "{}".format(idx))
34    vis.reset_camera_to_default()
35
36    app.add_window(vis)
37    app.run()
38
39
40def low_level():
41    app = gui.Application.instance
42    app.initialize()
43
44    points = make_point_cloud(100, (0, 0, 0), 1.0)
45
46    w = app.create_window("Open3D - 3D Text", 1024, 768)
47    widget3d = gui.SceneWidget()
48    widget3d.scene = rendering.Open3DScene(w.renderer)
49    mat = rendering.MaterialRecord()
50    mat.shader = "defaultUnlit"
51    mat.point_size = 5 * w.scaling
52    widget3d.scene.add_geometry("Points", points, mat)
53    for idx in range(0, len(points.points)):
54        l = widget3d.add_3d_label(points.points[idx], "{}".format(idx))
55        l.color = gui.Color(points.colors[idx][0], points.colors[idx][1],
56                            points.colors[idx][2])
57        l.scale = np.random.uniform(0.5, 3.0)
58    bbox = widget3d.scene.bounding_box
59    widget3d.setup_camera(60.0, bbox, bbox.get_center())
60    w.add_child(widget3d)
61
62    app.run()
63
64
65if __name__ == "__main__":
66    high_level()
67    low_level()

textured_mesh.py#

 1# ----------------------------------------------------------------------------
 2# -                        Open3D: www.open3d.org                            -
 3# ----------------------------------------------------------------------------
 4# Copyright (c) 2018-2024 www.open3d.org
 5# SPDX-License-Identifier: MIT
 6# ----------------------------------------------------------------------------
 7
 8import sys
 9import os
10import open3d as o3d
11
12
13def main():
14    if len(sys.argv) < 2:
15        print("""Usage: textured-mesh.py [model directory]
16    This example will load [model directory].obj plus any of albedo, normal,
17    ao, metallic and roughness textures present. The textures should be named
18    albedo.png, normal.png, ao.png, metallic.png and roughness.png
19    respectively.""")
20        sys.exit()
21
22    model_dir = os.path.normpath(os.path.realpath(sys.argv[1]))
23    model_name = os.path.join(model_dir, os.path.basename(model_dir) + ".obj")
24    mesh = o3d.t.geometry.TriangleMesh.from_legacy(
25        o3d.io.read_triangle_mesh(model_name))
26    material = mesh.material
27    material.material_name = "defaultLit"
28
29    names_to_o3dprop = {"ao": "ambient_occlusion"}
30    for texture in ("albedo", "normal", "ao", "metallic", "roughness"):
31        texture_file = os.path.join(model_dir, texture + ".png")
32        if os.path.exists(texture_file):
33            texture = names_to_o3dprop.get(texture, texture)
34            material.texture_maps[texture] = o3d.t.io.read_image(texture_file)
35    if "metallic" in material.texture_maps:
36        material.scalar_properties["metallic"] = 1.0
37
38    o3d.visualization.draw(mesh, title=model_name)
39
40
41if __name__ == "__main__":
42    main()

textured_model.py#

 1# ----------------------------------------------------------------------------
 2# -                        Open3D: www.open3d.org                            -
 3# ----------------------------------------------------------------------------
 4# Copyright (c) 2018-2024 www.open3d.org
 5# SPDX-License-Identifier: MIT
 6# ----------------------------------------------------------------------------
 7
 8import sys
 9import os
10import open3d as o3d
11
12
13def main():
14    if len(sys.argv) < 2:
15        print("""Usage: texture-model.py [model directory]
16    This example will load [model directory].obj plus any of albedo, normal,
17    ao, metallic and roughness textures present.""")
18        sys.exit()
19
20    model_dir = sys.argv[1]
21    model_name = os.path.join(model_dir, os.path.basename(model_dir) + ".obj")
22    model = o3d.io.read_triangle_mesh(model_name)
23    material = o3d.visualization.rendering.MaterialRecord()
24    material.shader = "defaultLit"
25
26    albedo_name = os.path.join(model_dir, "albedo.png")
27    normal_name = os.path.join(model_dir, "normal.png")
28    ao_name = os.path.join(model_dir, "ao.png")
29    metallic_name = os.path.join(model_dir, "metallic.png")
30    roughness_name = os.path.join(model_dir, "roughness.png")
31    if os.path.exists(albedo_name):
32        material.albedo_img = o3d.io.read_image(albedo_name)
33    if os.path.exists(normal_name):
34        material.normal_img = o3d.io.read_image(normal_name)
35    if os.path.exists(ao_name):
36        material.ao_img = o3d.io.read_image(ao_name)
37    if os.path.exists(metallic_name):
38        material.base_metallic = 1.0
39        material.metallic_img = o3d.io.read_image(metallic_name)
40    if os.path.exists(roughness_name):
41        material.roughness_img = o3d.io.read_image(roughness_name)
42
43    o3d.visualization.draw([{
44        "name": "cube",
45        "geometry": model,
46        "material": material
47    }])
48
49
50if __name__ == "__main__":
51    main()

to_mitsuba.py#

  1# ----------------------------------------------------------------------------
  2# -                        Open3D: www.open3d.org                            -
  3# ----------------------------------------------------------------------------
  4# Copyright (c) 2018-2024 www.open3d.org
  5# SPDX-License-Identifier: MIT
  6# ----------------------------------------------------------------------------
  7
  8import open3d as o3d
  9import mitsuba as mi
 10
 11
 12def render_mesh(mesh, mesh_center):
 13    scene = mi.load_dict({
 14        'type': 'scene',
 15        'integrator': {
 16            'type': 'path'
 17        },
 18        'light': {
 19            'type': 'constant',
 20            'radiance': {
 21                'type': 'rgb',
 22                'value': 1.0
 23            }
 24            # NOTE: For better results comment out the constant emitter above
 25            # and uncomment out the lines below changing the filename to an HDRI
 26            # envmap you have.
 27            # 'type': 'envmap',
 28            # 'filename': '/home/renes/Downloads/solitude_interior_4k.exr'
 29        },
 30        'sensor': {
 31            'type':
 32                'perspective',
 33            'focal_length':
 34                '50mm',
 35            'to_world':
 36                mi.ScalarTransform4f.look_at(origin=[0, 0, 5],
 37                                             target=mesh_center,
 38                                             up=[0, 1, 0]),
 39            'thefilm': {
 40                'type': 'hdrfilm',
 41                'width': 1024,
 42                'height': 768,
 43            },
 44            'thesampler': {
 45                'type': 'multijitter',
 46                'sample_count': 64,
 47            },
 48        },
 49        'themesh': mesh,
 50    })
 51
 52    img = mi.render(scene, spp=256)
 53    return img
 54
 55
 56# Default to LLVM variant which should be available on all
 57# platforms. If you have a system with a CUDA device then comment out LLVM
 58# variant and uncomment cuda variant
 59mi.set_variant('llvm_ad_rgb')
 60# mi.set_variant('cuda_ad_rgb')
 61
 62# Load mesh and maps using Open3D
 63dataset = o3d.data.MonkeyModel()
 64mesh = o3d.t.io.read_triangle_mesh(dataset.path)
 65mesh_center = mesh.get_axis_aligned_bounding_box().get_center()
 66mesh.material.set_default_properties()
 67mesh.material.material_name = 'defaultLit'
 68mesh.material.scalar_properties['metallic'] = 1.0
 69mesh.material.texture_maps['albedo'] = o3d.t.io.read_image(
 70    dataset.path_map['albedo'])
 71mesh.material.texture_maps['roughness'] = o3d.t.io.read_image(
 72    dataset.path_map['roughness'])
 73mesh.material.texture_maps['metallic'] = o3d.t.io.read_image(
 74    dataset.path_map['metallic'])
 75
 76print('Render mesh with material converted to Mitsuba principled BSDF')
 77mi_mesh = mesh.to_mitsuba('monkey')
 78img = render_mesh(mi_mesh, mesh_center.numpy())
 79mi.Bitmap(img).write('test.exr')
 80
 81print('Render mesh with normal-mapped prnincipled BSDF')
 82mesh.material.texture_maps['normal'] = o3d.t.io.read_image(
 83    dataset.path_map['normal'])
 84mi_mesh = mesh.to_mitsuba('monkey')
 85img = render_mesh(mi_mesh, mesh_center.numpy())
 86mi.Bitmap(img).write('test2.exr')
 87
 88print('Rendering mesh with Mitsuba smooth plastic BSDF')
 89bsdf_smooth_plastic = mi.load_dict({
 90    'type': 'plastic',
 91    'diffuse_reflectance': {
 92        'type': 'rgb',
 93        'value': [0.1, 0.27, 0.36]
 94    },
 95    'int_ior': 1.9
 96})
 97mi_mesh = mesh.to_mitsuba('monkey', bsdf=bsdf_smooth_plastic)
 98img = render_mesh(mi_mesh, mesh_center.numpy())
 99mi.Bitmap(img).write('test3.exr')
100
101# Render with Open3D
102o3d.visualization.draw(mesh)

video.py#

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

vis_gui.py#

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