Visualization

add_geometry.py

  1# ----------------------------------------------------------------------------
  2# -                        Open3D: www.open3d.org                            -
  3# ----------------------------------------------------------------------------
  4# The MIT License (MIT)
  5#
  6# Copyright (c) 2018-2021 www.open3d.org
  7#
  8# Permission is hereby granted, free of charge, to any person obtaining a copy
  9# of this software and associated documentation files (the "Software"), to deal
 10# in the Software without restriction, including without limitation the rights
 11# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 12# copies of the Software, and to permit persons to whom the Software is
 13# furnished to do so, subject to the following conditions:
 14#
 15# The above copyright notice and this permission notice shall be included in
 16# all copies or substantial portions of the Software.
 17#
 18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 19# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 20# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 21# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 22# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 23# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 24# IN THE SOFTWARE.
 25# ----------------------------------------------------------------------------
 26
 27import open3d as o3d
 28import open3d.visualization.gui as gui
 29import open3d.visualization.rendering as rendering
 30import platform
 31import random
 32import threading
 33import time
 34
 35isMacOS = (platform.system() == "Darwin")
 36
 37
 38# This example shows two methods of adding geometry to an existing scene.
 39# 1) add via a UI callback (in this case a menu, but a button would be similar,
 40#    you would call `button.set_on_clicked(self.on_menu_sphere_)` when
 41#    configuring the button. See `on_menu_sphere()`.
 42# 2) add asynchronously by polling from another thread. GUI functions must be
 43#    called from the UI thread, so use Application.post_to_main_thread().
 44#    See `on_menu_random()`.
 45# Running the example will show a simple window with a Debug menu item with the
 46# two different options. The second method will add random spheres for
 47# 20 seconds, during which time you can be interacting with the scene, rotating,
 48# etc.
 49class SpheresApp:
 50    MENU_SPHERE = 1
 51    MENU_RANDOM = 2
 52    MENU_QUIT = 3
 53
 54    def __init__(self):
 55        self._id = 0
 56        self.window = gui.Application.instance.create_window(
 57            "Add Spheres Example", 1024, 768)
 58        self.scene = gui.SceneWidget()
 59        self.scene.scene = rendering.Open3DScene(self.window.renderer)
 60        self.scene.scene.set_background([1, 1, 1, 1])
 61        self.scene.scene.scene.set_sun_light(
 62            [-1, -1, -1],  # direction
 63            [1, 1, 1],  # color
 64            100000)  # intensity
 65        self.scene.scene.scene.enable_sun_light(True)
 66        bbox = o3d.geometry.AxisAlignedBoundingBox([-10, -10, -10],
 67                                                   [10, 10, 10])
 68        self.scene.setup_camera(60, bbox, [0, 0, 0])
 69
 70        self.window.add_child(self.scene)
 71
 72        # The menu is global (because the macOS menu is global), so only create
 73        # it once, no matter how many windows are created
 74        if gui.Application.instance.menubar is None:
 75            if isMacOS:
 76                app_menu = gui.Menu()
 77                app_menu.add_item("Quit", SpheresApp.MENU_QUIT)
 78            debug_menu = gui.Menu()
 79            debug_menu.add_item("Add Sphere", SpheresApp.MENU_SPHERE)
 80            debug_menu.add_item("Add Random Spheres", SpheresApp.MENU_RANDOM)
 81            if not isMacOS:
 82                debug_menu.add_separator()
 83                debug_menu.add_item("Quit", SpheresApp.MENU_QUIT)
 84
 85            menu = gui.Menu()
 86            if isMacOS:
 87                # macOS will name the first menu item for the running application
 88                # (in our case, probably "Python"), regardless of what we call
 89                # it. This is the application menu, and it is where the
 90                # About..., Preferences..., and Quit menu items typically go.
 91                menu.add_menu("Example", app_menu)
 92                menu.add_menu("Debug", debug_menu)
 93            else:
 94                menu.add_menu("Debug", debug_menu)
 95            gui.Application.instance.menubar = menu
 96
 97        # The menubar is global, but we need to connect the menu items to the
 98        # window, so that the window can call the appropriate function when the
 99        # menu item is activated.
100        self.window.set_on_menu_item_activated(SpheresApp.MENU_SPHERE,
101                                               self._on_menu_sphere)
102        self.window.set_on_menu_item_activated(SpheresApp.MENU_RANDOM,
103                                               self._on_menu_random)
104        self.window.set_on_menu_item_activated(SpheresApp.MENU_QUIT,
105                                               self._on_menu_quit)
106
107    def add_sphere(self):
108        self._id += 1
109        mat = rendering.MaterialRecord()
110        mat.base_color = [
111            random.random(),
112            random.random(),
113            random.random(), 1.0
114        ]
115        mat.shader = "defaultLit"
116        sphere = o3d.geometry.TriangleMesh.create_sphere(0.5)
117        sphere.compute_vertex_normals()
118        sphere.translate([
119            10.0 * random.uniform(-1.0, 1.0), 10.0 * random.uniform(-1.0, 1.0),
120            10.0 * random.uniform(-1.0, 1.0)
121        ])
122        self.scene.scene.add_geometry("sphere" + str(self._id), sphere, mat)
123
124    def _on_menu_sphere(self):
125        # GUI callbacks happen on the main thread, so we can do everything
126        # normally here.
127        self.add_sphere()
128
129    def _on_menu_random(self):
130        # This adds spheres asynchronously. This pattern is useful if you have
131        # data coming in from another source than user interaction.
132        def thread_main():
133            for _ in range(0, 20):
134                # We can only modify GUI objects on the main thread, so we
135                # need to post the function to call to the main thread.
136                gui.Application.instance.post_to_main_thread(
137                    self.window, self.add_sphere)
138                time.sleep(1)
139
140        threading.Thread(target=thread_main).start()
141
142    def _on_menu_quit(self):
143        gui.Application.instance.quit()
144
145
146def main():
147    gui.Application.instance.initialize()
148    SpheresApp()
149    gui.Application.instance.run()
150
151
152if __name__ == "__main__":
153    main()

all_widgets.py

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

customized_visualization.py

  1# ----------------------------------------------------------------------------
  2# -                        Open3D: www.open3d.org                            -
  3# ----------------------------------------------------------------------------
  4# The MIT License (MIT)
  5#
  6# Copyright (c) 2018-2021 www.open3d.org
  7#
  8# Permission is hereby granted, free of charge, to any person obtaining a copy
  9# of this software and associated documentation files (the "Software"), to deal
 10# in the Software without restriction, including without limitation the rights
 11# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 12# copies of the Software, and to permit persons to whom the Software is
 13# furnished to do so, subject to the following conditions:
 14#
 15# The above copyright notice and this permission notice shall be included in
 16# all copies or substantial portions of the Software.
 17#
 18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 19# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 20# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 21# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 22# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 23# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 24# IN THE SOFTWARE.
 25# ----------------------------------------------------------------------------
 26
 27import os
 28import open3d as o3d
 29import numpy as np
 30import matplotlib.pyplot as plt
 31
 32pyexample_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 33test_data_path = os.path.join(os.path.dirname(pyexample_path), 'test_data')
 34
 35
 36def custom_draw_geometry(pcd):
 37    # The following code achieves the same effect as:
 38    # o3d.visualization.draw_geometries([pcd])
 39    vis = o3d.visualization.Visualizer()
 40    vis.create_window()
 41    vis.add_geometry(pcd)
 42    vis.run()
 43    vis.destroy_window()
 44
 45
 46def custom_draw_geometry_with_custom_fov(pcd, fov_step):
 47    vis = o3d.visualization.Visualizer()
 48    vis.create_window()
 49    vis.add_geometry(pcd)
 50    ctr = vis.get_view_control()
 51    print("Field of view (before changing) %.2f" % ctr.get_field_of_view())
 52    ctr.change_field_of_view(step=fov_step)
 53    print("Field of view (after changing) %.2f" % ctr.get_field_of_view())
 54    vis.run()
 55    vis.destroy_window()
 56
 57
 58def custom_draw_geometry_with_rotation(pcd):
 59
 60    def rotate_view(vis):
 61        ctr = vis.get_view_control()
 62        ctr.rotate(10.0, 0.0)
 63        return False
 64
 65    o3d.visualization.draw_geometries_with_animation_callback([pcd],
 66                                                              rotate_view)
 67
 68
 69def custom_draw_geometry_load_option(pcd):
 70    vis = o3d.visualization.Visualizer()
 71    vis.create_window()
 72    vis.add_geometry(pcd)
 73    vis.get_render_option().load_from_json(
 74        os.path.join(test_data_path, 'renderoption.json'))
 75    vis.run()
 76    vis.destroy_window()
 77
 78
 79def custom_draw_geometry_with_key_callback(pcd):
 80
 81    def change_background_to_black(vis):
 82        opt = vis.get_render_option()
 83        opt.background_color = np.asarray([0, 0, 0])
 84        return False
 85
 86    def load_render_option(vis):
 87        vis.get_render_option().load_from_json(
 88            os.path.join(test_data_path, 'renderoption.json'))
 89        return False
 90
 91    def capture_depth(vis):
 92        depth = vis.capture_depth_float_buffer()
 93        plt.imshow(np.asarray(depth))
 94        plt.show()
 95        return False
 96
 97    def capture_image(vis):
 98        image = vis.capture_screen_float_buffer()
 99        plt.imshow(np.asarray(image))
100        plt.show()
101        return False
102
103    key_to_callback = {}
104    key_to_callback[ord("K")] = change_background_to_black
105    key_to_callback[ord("R")] = load_render_option
106    key_to_callback[ord(",")] = capture_depth
107    key_to_callback[ord(".")] = capture_image
108    o3d.visualization.draw_geometries_with_key_callbacks([pcd], key_to_callback)
109
110
111def custom_draw_geometry_with_camera_trajectory(pcd):
112    custom_draw_geometry_with_camera_trajectory.index = -1
113    custom_draw_geometry_with_camera_trajectory.trajectory =\
114            o3d.io.read_pinhole_camera_trajectory(
115                os.path.join(test_data_path, 'camera_trajectory.json'))
116    custom_draw_geometry_with_camera_trajectory.vis = o3d.visualization.Visualizer(
117    )
118    image_path = os.path.join(test_data_path, 'image')
119    if not os.path.exists(image_path):
120        os.makedirs(image_path)
121    depth_path = os.path.join(test_data_path, 'depth')
122    if not os.path.exists(depth_path):
123        os.makedirs(depth_path)
124    render_option_path = os.path.join(test_data_path, 'renderoption.json')
125
126    def move_forward(vis):
127        # This function is called within the o3d.visualization.Visualizer::run() loop
128        # The run loop calls the function, then re-render
129        # So the sequence in this function is to:
130        # 1. Capture frame
131        # 2. index++, check ending criteria
132        # 3. Set camera
133        # 4. (Re-render)
134        ctr = vis.get_view_control()
135        glb = custom_draw_geometry_with_camera_trajectory
136        if glb.index >= 0:
137            print("Capture image {:05d}".format(glb.index))
138            depth = vis.capture_depth_float_buffer(False)
139            image = vis.capture_screen_float_buffer(False)
140            plt.imsave(os.path.join(depth_path, '{:05d}.png'.format(glb.index)),\
141                    np.asarray(depth), dpi = 1)
142            plt.imsave(os.path.join(image_path, '{:05d}.png'.format(glb.index)),\
143                    np.asarray(image), dpi = 1)
144            # vis.capture_depth_image("depth/{:05d}.png".format(glb.index), False)
145            # vis.capture_screen_image("image/{:05d}.png".format(glb.index), False)
146        glb.index = glb.index + 1
147        if glb.index < len(glb.trajectory.parameters):
148            ctr.convert_from_pinhole_camera_parameters(
149                glb.trajectory.parameters[glb.index], allow_arbitrary=True)
150        else:
151            custom_draw_geometry_with_camera_trajectory.vis.\
152                    register_animation_callback(None)
153        return False
154
155    vis = custom_draw_geometry_with_camera_trajectory.vis
156    vis.create_window()
157    vis.add_geometry(pcd)
158    vis.get_render_option().load_from_json(render_option_path)
159    vis.register_animation_callback(move_forward)
160    vis.run()
161    vis.destroy_window()
162
163
164if __name__ == "__main__":
165    pcd_data = o3d.data.PCDPointCloud()
166    pcd = o3d.io.read_point_cloud(pcd_data.path)
167
168    print("1. Customized visualization to mimic DrawGeometry")
169    custom_draw_geometry(pcd)
170
171    print("2. Changing field of view")
172    custom_draw_geometry_with_custom_fov(pcd, 90.0)
173    custom_draw_geometry_with_custom_fov(pcd, -90.0)
174
175    print("3. Customized visualization with a rotating view")
176    custom_draw_geometry_with_rotation(pcd)
177
178    print("4. Customized visualization showing normal rendering")
179    custom_draw_geometry_load_option(pcd)
180
181    print("5. Customized visualization with key press callbacks")
182    print("   Press 'K' to change background color to black")
183    print("   Press 'R' to load a customized render option, showing normals")
184    print("   Press ',' to capture the depth buffer and show it")
185    print("   Press '.' to capture the screen and show it")
186    custom_draw_geometry_with_key_callback(pcd)
187
188    print("6. Customized visualization playing a camera trajectory")
189    custom_draw_geometry_with_camera_trajectory(pcd)

customized_visualization_key_action.py

 1# ----------------------------------------------------------------------------
 2# -                        Open3D: www.open3d.org                            -
 3# ----------------------------------------------------------------------------
 4# The MIT License (MIT)
 5#
 6# Copyright (c) 2018-2021 www.open3d.org
 7#
 8# Permission is hereby granted, free of charge, to any person obtaining a copy
 9# of this software and associated documentation files (the "Software"), to deal
10# in the Software without restriction, including without limitation the rights
11# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12# copies of the Software, and to permit persons to whom the Software is
13# furnished to do so, subject to the following conditions:
14#
15# The above copyright notice and this permission notice shall be included in
16# all copies or substantial portions of the Software.
17#
18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
24# IN THE SOFTWARE.
25# ----------------------------------------------------------------------------
26
27import open3d as o3d
28
29
30def custom_key_action_without_kb_repeat_delay(pcd):
31    rotating = False
32
33    vis = o3d.visualization.VisualizerWithKeyCallback()
34
35    def key_action_callback(vis, action, mods):
36        nonlocal rotating
37        print(action)
38        if action == 1:  # key down
39            rotating = True
40        elif action == 0:  # key up
41            rotating = False
42        elif action == 2:  # key repeat
43            pass
44        return True
45
46    def animation_callback(vis):
47        nonlocal rotating
48        if rotating:
49            ctr = vis.get_view_control()
50            ctr.rotate(10.0, 0.0)
51
52    # key_action_callback will be triggered when there's a keyboard press, release or repeat event
53    vis.register_key_action_callback(32, key_action_callback)  # space
54
55    # animation_callback is always repeatedly called by the visualizer
56    vis.register_animation_callback(animation_callback)
57
58    vis.create_window()
59    vis.add_geometry(pcd)
60    vis.run()
61
62
63if __name__ == "__main__":
64    ply_data = o3d.data.PLYPointCloud()
65    pcd = o3d.io.read_point_cloud(ply_data.path)
66
67    print(
68        "Customized visualization with smooth key action (without keyboard repeat delay)"
69    )
70    custom_key_action_without_kb_repeat_delay(pcd)

demo_scene.py

  1# ----------------------------------------------------------------------------
  2# -                        Open3D: www.open3d.org                            -
  3# ----------------------------------------------------------------------------
  4# The MIT License (MIT)
  5#
  6# Copyright (c) 2018-2021 www.open3d.org
  7#
  8# Permission is hereby granted, free of charge, to any person obtaining a copy
  9# of this software and associated documentation files (the "Software"), to deal
 10# in the Software without restriction, including without limitation the rights
 11# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 12# copies of the Software, and to permit persons to whom the Software is
 13# furnished to do so, subject to the following conditions:
 14#
 15# The above copyright notice and this permission notice shall be included in
 16# all copies or substantial portions of the Software.
 17#
 18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 19# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 20# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 21# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 22# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 23# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 24# IN THE SOFTWARE.
 25# ----------------------------------------------------------------------------
 26"""Demo scene demonstrating models, built-in shapes, and materials"""
 27
 28import math
 29import numpy as np
 30import os
 31import open3d as o3d
 32import open3d.visualization as vis
 33
 34
 35# Check for assets
 36def check_for_required_assets():
 37    '''
 38    Check for demo scene assets and print usage if necessary
 39    '''
 40    if not os.path.exists("examples/test_data/demo_scene_assets"):
 41        print("""This demo requires assets that appear to be missing.
 42Please execute the follow commands:
 43```
 44cd examples/test_data
 45wget https://github.com/isl-org/open3d_downloads/releases/download/o3d_demo_scene/demo_scene_assets.tgz
 46tar xzvf demo_scene_assets.tgz
 47cd ../..
 48python examples/python/visualization/demo_scene.py
 49```
 50""")
 51        exit(1)
 52
 53
 54def create_material(directory, name):
 55    '''
 56    Convenience function for creating material and loading a set of associated shaders
 57    '''
 58    mat = vis.Material('defaultLit')
 59    # Color, Roughness and Normal textures are always present
 60    mat.texture_maps['albedo'] = o3d.t.io.read_image(
 61        os.path.join(directory, name + '_Color.jpg'))
 62    mat.texture_maps['roughness'] = o3d.t.io.read_image(
 63        os.path.join(directory, name + '_Roughness.jpg'))
 64    mat.texture_maps['normal'] = o3d.t.io.read_image(
 65        os.path.join(directory, name + '_NormalDX.jpg'))
 66    # Ambient occlusion and metal textures are not always available
 67    # NOTE: Checking for their existence is not necessary but checking first
 68    # avoids annoying warning output
 69    ao_img_name = os.path.join(directory, name + '_AmbientOcclusion.jpg')
 70    metallic_img_name = os.path.join(directory, name + '_Metalness.jpg')
 71    if os.path.exists(ao_img_name):
 72        mat.texture_maps['ambient_occlusion'] = o3d.t.io.read_image(ao_img_name)
 73    if os.path.exists(metallic_img_name):
 74        mat.texture_maps['metallic'] = o3d.t.io.read_image(metallic_img_name)
 75        mat.scalar_properties['metallic'] = 1.0
 76    return mat
 77
 78
 79def convert_material_record(mat_record):
 80    mat = vis.Material('defaultLit')
 81    # Convert scalar paremeters
 82    mat.vector_properties['base_color'] = mat_record.base_color
 83    mat.scalar_properties['metallic'] = mat_record.base_metallic
 84    mat.scalar_properties['roughness'] = mat_record.base_roughness
 85    mat.scalar_properties['reflectance'] = mat_record.base_reflectance
 86    mat.texture_maps['albedo'] = o3d.t.geometry.Image.from_legacy(
 87        mat_record.albedo_img)
 88    mat.texture_maps['normal'] = o3d.t.geometry.Image.from_legacy(
 89        mat_record.normal_img)
 90    mat.texture_maps['ao_rough_metal'] = o3d.t.geometry.Image.from_legacy(
 91        mat_record.ao_rough_metal_img)
 92    return mat
 93
 94
 95def create_scene():
 96    '''
 97    Creates the geometry and materials for the demo scene and returns a dictionary suitable for draw call
 98    '''
 99    # Create some shapes for our scene
100    a_cube = o3d.geometry.TriangleMesh.create_box(2,
101                                                  4,
102                                                  4,
103                                                  create_uv_map=True,
104                                                  map_texture_to_each_face=True)
105    a_cube.compute_triangle_normals()
106    a_cube.translate((-5, 0, -2))
107    a_cube = o3d.t.geometry.TriangleMesh.from_legacy(a_cube)
108
109    a_sphere = o3d.geometry.TriangleMesh.create_sphere(2.5,
110                                                       resolution=40,
111                                                       create_uv_map=True)
112    a_sphere.compute_vertex_normals()
113    rotate_90 = o3d.geometry.get_rotation_matrix_from_xyz((-math.pi / 2, 0, 0))
114    a_sphere.rotate(rotate_90)
115    a_sphere.translate((5, 2.4, 0))
116    a_sphere = o3d.t.geometry.TriangleMesh.from_legacy(a_sphere)
117
118    a_cylinder = o3d.geometry.TriangleMesh.create_cylinder(
119        1.0, 4.0, 30, 4, True)
120    a_cylinder.compute_triangle_normals()
121    a_cylinder.rotate(rotate_90)
122    a_cylinder.translate((10, 2, 0))
123    a_cylinder = o3d.t.geometry.TriangleMesh.from_legacy(a_cylinder)
124
125    a_ico = o3d.geometry.TriangleMesh.create_icosahedron(1.25,
126                                                         create_uv_map=True)
127    a_ico.compute_triangle_normals()
128    a_ico.translate((-10, 2, 0))
129    a_ico = o3d.t.geometry.TriangleMesh.from_legacy(a_ico)
130
131    # Load an OBJ model for our scene
132    helmet = o3d.io.read_triangle_model(
133        "examples/test_data/demo_scene_assets/FlightHelmet.gltf")
134    helmet_parts = []
135    for m in helmet.meshes:
136        # m.mesh.paint_uniform_color((1.0, 0.75, 0.3))
137        m.mesh.scale(10.0, (0.0, 0.0, 0.0))
138        helmet_parts.append(m)
139
140    # Create a ground plane
141    ground_plane = o3d.geometry.TriangleMesh.create_box(
142        50.0, 0.1, 50.0, create_uv_map=True, map_texture_to_each_face=True)
143    ground_plane.compute_triangle_normals()
144    rotate_180 = o3d.geometry.get_rotation_matrix_from_xyz((-math.pi, 0, 0))
145    ground_plane.rotate(rotate_180)
146    ground_plane.translate((-25.0, -0.1, -25.0))
147    ground_plane.paint_uniform_color((1, 1, 1))
148    ground_plane = o3d.t.geometry.TriangleMesh.from_legacy(ground_plane)
149
150    # Material to make ground plane more interesting - a rough piece of glass
151    mat_ground = vis.Material("defaultLitSSR")
152    mat_ground.scalar_properties['roughness'] = 0.15
153    mat_ground.scalar_properties['reflectance'] = 0.72
154    mat_ground.scalar_properties['transmission'] = 0.6
155    mat_ground.scalar_properties['thickness'] = 0.3
156    mat_ground.scalar_properties['absorption_distance'] = 0.1
157    mat_ground.vector_properties['absorption_color'] = np.array(
158        [0.82, 0.98, 0.972, 1.0])
159    mat_ground.texture_maps['albedo'] = o3d.t.io.read_image(
160        "examples/test_data/demo_scene_assets/PaintedPlaster017_Color.jpg")
161    mat_ground.texture_maps['roughness'] = o3d.t.io.read_image(
162        "examples/test_data/demo_scene_assets/noiseTexture.png")
163    mat_ground.texture_maps['normal'] = o3d.t.io.read_image(
164        "examples/test_data/demo_scene_assets/PaintedPlaster017_NormalDX.jpg")
165    ground_plane.material = mat_ground
166
167    # Load textures and create materials for each of our demo items
168    a_cube.material = create_material("examples/test_data/demo_scene_assets",
169                                      "WoodFloor050")
170    a_sphere.material = create_material("examples/test_data/demo_scene_assets",
171                                        "Tiles074")
172    a_ico.material = create_material("examples/test_data/demo_scene_assets",
173                                     "Terrazzo018")
174    a_cylinder.material = create_material(
175        "examples/test_data/demo_scene_assets", "Metal008")
176
177    geoms = [{
178        "name": "plane",
179        "geometry": ground_plane
180    }, {
181        "name": "cube",
182        "geometry": a_cube
183    }, {
184        "name": "cylinder",
185        "geometry": a_cylinder
186    }, {
187        "name": "ico",
188        "geometry": a_ico
189    }, {
190        "name": "sphere",
191        "geometry": a_sphere
192    }]
193    # Load the helmet
194    for part in helmet_parts:
195        name = part.mesh_name
196        tgeom = o3d.t.geometry.TriangleMesh.from_legacy(part.mesh)
197        tgeom.material = convert_material_record(
198            helmet.materials[part.material_idx])
199        geoms.append({"name": name, "geometry": tgeom})
200    return geoms
201
202
203if __name__ == "__main__":
204    check_for_required_assets()
205    geoms = create_scene()
206    vis.draw(geoms,
207             bg_color=(0.8, 0.9, 0.9, 1.0),
208             show_ui=True,
209             width=1920,
210             height=1080)

draw.py

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

draw_webrtc.py

 1# ----------------------------------------------------------------------------
 2# -                        Open3D: www.open3d.org                            -
 3# ----------------------------------------------------------------------------
 4# The MIT License (MIT)
 5#
 6# Copyright (c) 2018-2021 www.open3d.org
 7#
 8# Permission is hereby granted, free of charge, to any person obtaining a copy
 9# of this software and associated documentation files (the "Software"), to deal
10# in the Software without restriction, including without limitation the rights
11# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12# copies of the Software, and to permit persons to whom the Software is
13# furnished to do so, subject to the following conditions:
14#
15# The above copyright notice and this permission notice shall be included in
16# all copies or substantial portions of the Software.
17#
18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
24# IN THE SOFTWARE.
25# ----------------------------------------------------------------------------
26
27import open3d as o3d
28
29if __name__ == "__main__":
30    o3d.visualization.webrtc_server.enable_webrtc()
31    cube_red = o3d.geometry.TriangleMesh.create_box(1, 2, 4)
32    cube_red.compute_vertex_normals()
33    cube_red.paint_uniform_color((1.0, 0.0, 0.0))
34    o3d.visualization.draw(cube_red)

headless_rendering.py

  1# ----------------------------------------------------------------------------
  2# -                        Open3D: www.open3d.org                            -
  3# ----------------------------------------------------------------------------
  4# The MIT License (MIT)
  5#
  6# Copyright (c) 2018-2021 www.open3d.org
  7#
  8# Permission is hereby granted, free of charge, to any person obtaining a copy
  9# of this software and associated documentation files (the "Software"), to deal
 10# in the Software without restriction, including without limitation the rights
 11# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 12# copies of the Software, and to permit persons to whom the Software is
 13# furnished to do so, subject to the following conditions:
 14#
 15# The above copyright notice and this permission notice shall be included in
 16# all copies or substantial portions of the Software.
 17#
 18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 19# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 20# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 21# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 22# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 23# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 24# IN THE SOFTWARE.
 25# ----------------------------------------------------------------------------
 26
 27import os
 28import open3d as o3d
 29import numpy as np
 30import matplotlib.pyplot as plt
 31
 32pyexample_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 33test_data_path = os.path.join(os.path.dirname(pyexample_path), 'test_data')
 34
 35
 36def custom_draw_geometry_with_camera_trajectory(pcd):
 37    custom_draw_geometry_with_camera_trajectory.index = -1
 38    custom_draw_geometry_with_camera_trajectory.trajectory =\
 39            o3d.io.read_pinhole_camera_trajectory(
 40                os.path.join(test_data_path, 'camera_trajectory.json'))
 41    custom_draw_geometry_with_camera_trajectory.vis = o3d.visualization.Visualizer(
 42    )
 43    image_path = os.path.join(test_data_path, 'image')
 44    if not os.path.exists(image_path):
 45        os.makedirs(image_path)
 46    depth_path = os.path.join(test_data_path, 'depth')
 47    if not os.path.exists(depth_path):
 48        os.makedirs(depth_path)
 49
 50    def move_forward(vis):
 51        # This function is called within the o3d.visualization.Visualizer::run() loop
 52        # The run loop calls the function, then re-render
 53        # So the sequence in this function is to:
 54        # 1. Capture frame
 55        # 2. index++, check ending criteria
 56        # 3. Set camera
 57        # 4. (Re-render)
 58        ctr = vis.get_view_control()
 59        glb = custom_draw_geometry_with_camera_trajectory
 60        if glb.index >= 0:
 61            print("Capture image {:05d}".format(glb.index))
 62            depth = vis.capture_depth_float_buffer(False)
 63            image = vis.capture_screen_float_buffer(False)
 64            plt.imsave(os.path.join(depth_path, "{:05d}.png".format(glb.index)),\
 65                    np.asarray(depth), dpi = 1)
 66            plt.imsave(os.path.join(image_path, "{:05d}.png".format(glb.index)),\
 67                    np.asarray(image), dpi = 1)
 68            #vis.capture_depth_image("depth/{:05d}.png".format(glb.index), False)
 69            #vis.capture_screen_image("image/{:05d}.png".format(glb.index), False)
 70        glb.index = glb.index + 1
 71        if glb.index < len(glb.trajectory.parameters):
 72            ctr.convert_from_pinhole_camera_parameters(
 73                glb.trajectory.parameters[glb.index])
 74        else:
 75            custom_draw_geometry_with_camera_trajectory.vis.\
 76                    register_animation_callback(None)
 77        return False
 78
 79    vis = custom_draw_geometry_with_camera_trajectory.vis
 80    vis.create_window()
 81    vis.add_geometry(pcd)
 82    vis.get_render_option().load_from_json(
 83        os.path.join(test_data_path, 'renderoption.json'))
 84    vis.register_animation_callback(move_forward)
 85    vis.run()
 86    vis.destroy_window()
 87
 88
 89if __name__ == "__main__":
 90    if not o3d._build_config['ENABLE_HEADLESS_RENDERING']:
 91        print("Headless rendering is not enabled. "
 92              "Please rebuild Open3D with ENABLE_HEADLESS_RENDERING=ON")
 93        exit(1)
 94
 95    sample_ply_data = o3d.data.PLYPointCloud()
 96    pcd = o3d.io.read_point_cloud(sample_ply_data.path)
 97
 98    print("Customized visualization playing a camera trajectory. "
 99          "Press ctrl+z to terminate.")
100    custom_draw_geometry_with_camera_trajectory(pcd)

interactive_visualization.py

  1# ----------------------------------------------------------------------------
  2# -                        Open3D: www.open3d.org                            -
  3# ----------------------------------------------------------------------------
  4# The MIT License (MIT)
  5#
  6# Copyright (c) 2018-2021 www.open3d.org
  7#
  8# Permission is hereby granted, free of charge, to any person obtaining a copy
  9# of this software and associated documentation files (the "Software"), to deal
 10# in the Software without restriction, including without limitation the rights
 11# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 12# copies of the Software, and to permit persons to whom the Software is
 13# furnished to do so, subject to the following conditions:
 14#
 15# The above copyright notice and this permission notice shall be included in
 16# all copies or substantial portions of the Software.
 17#
 18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 19# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 20# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 21# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 22# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 23# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 24# IN THE SOFTWARE.
 25# ----------------------------------------------------------------------------
 26
 27# examples/python/visualization/interactive_visualization.py
 28
 29import numpy as np
 30import copy
 31import open3d as o3d
 32
 33
 34def demo_crop_geometry():
 35    print("Demo for manual geometry cropping")
 36    print(
 37        "1) Press 'Y' twice to align geometry with negative direction of y-axis"
 38    )
 39    print("2) Press 'K' to lock screen and to switch to selection mode")
 40    print("3) Drag for rectangle selection,")
 41    print("   or use ctrl + left click for polygon selection")
 42    print("4) Press 'C' to get a selected geometry and to save it")
 43    print("5) Press 'F' to switch to freeview mode")
 44    pcd_data = o3d.data.DemoICPPointClouds()
 45    pcd = o3d.io.read_point_cloud(pcd_data.paths[0])
 46    o3d.visualization.draw_geometries_with_editing([pcd])
 47
 48
 49def draw_registration_result(source, target, transformation):
 50    source_temp = copy.deepcopy(source)
 51    target_temp = copy.deepcopy(target)
 52    source_temp.paint_uniform_color([1, 0.706, 0])
 53    target_temp.paint_uniform_color([0, 0.651, 0.929])
 54    source_temp.transform(transformation)
 55    o3d.visualization.draw_geometries([source_temp, target_temp])
 56
 57
 58def pick_points(pcd):
 59    print("")
 60    print(
 61        "1) Please pick at least three correspondences using [shift + left click]"
 62    )
 63    print("   Press [shift + right click] to undo point picking")
 64    print("2) After picking points, press 'Q' to close the window")
 65    vis = o3d.visualization.VisualizerWithEditing()
 66    vis.create_window()
 67    vis.add_geometry(pcd)
 68    vis.run()  # user picks points
 69    vis.destroy_window()
 70    print("")
 71    return vis.get_picked_points()
 72
 73
 74def demo_manual_registration():
 75    print("Demo for manual ICP")
 76    pcd_data = o3d.data.DemoICPPointClouds()
 77    source = o3d.io.read_point_cloud(pcd_data.paths[0])
 78    target = o3d.io.read_point_cloud(pcd_data.paths[2])
 79    print("Visualization of two point clouds before manual alignment")
 80    draw_registration_result(source, target, np.identity(4))
 81
 82    # pick points from two point clouds and builds correspondences
 83    picked_id_source = pick_points(source)
 84    picked_id_target = pick_points(target)
 85    assert (len(picked_id_source) >= 3 and len(picked_id_target) >= 3)
 86    assert (len(picked_id_source) == len(picked_id_target))
 87    corr = np.zeros((len(picked_id_source), 2))
 88    corr[:, 0] = picked_id_source
 89    corr[:, 1] = picked_id_target
 90
 91    # estimate rough transformation using correspondences
 92    print("Compute a rough transform using the correspondences given by user")
 93    p2p = o3d.pipelines.registration.TransformationEstimationPointToPoint()
 94    trans_init = p2p.compute_transformation(source, target,
 95                                            o3d.utility.Vector2iVector(corr))
 96
 97    # point-to-point ICP for refinement
 98    print("Perform point-to-point ICP refinement")
 99    threshold = 0.03  # 3cm distance threshold
100    reg_p2p = o3d.pipelines.registration.registration_icp(
101        source, target, threshold, trans_init,
102        o3d.pipelines.registration.TransformationEstimationPointToPoint())
103    draw_registration_result(source, target, reg_p2p.transformation)
104    print("")
105
106
107if __name__ == "__main__":
108    demo_crop_geometry()
109    demo_manual_registration()

line_width.py

 1# ----------------------------------------------------------------------------
 2# -                        Open3D: www.open3d.org                            -
 3# ----------------------------------------------------------------------------
 4# The MIT License (MIT)
 5#
 6# Copyright (c) 2018-2021 www.open3d.org
 7#
 8# Permission is hereby granted, free of charge, to any person obtaining a copy
 9# of this software and associated documentation files (the "Software"), to deal
10# in the Software without restriction, including without limitation the rights
11# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12# copies of the Software, and to permit persons to whom the Software is
13# furnished to do so, subject to the following conditions:
14#
15# The above copyright notice and this permission notice shall be included in
16# all copies or substantial portions of the Software.
17#
18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
24# IN THE SOFTWARE.
25# ----------------------------------------------------------------------------
26
27import open3d as o3d
28import random
29
30NUM_LINES = 10
31
32
33def random_point():
34    return [5 * random.random(), 5 * random.random(), 5 * random.random()]
35
36
37def main():
38    pts = [random_point() for _ in range(0, 2 * NUM_LINES)]
39    line_indices = [[2 * i, 2 * i + 1] for i in range(0, NUM_LINES)]
40    colors = [[0.0, 0.0, 0.0] for _ in range(0, NUM_LINES)]
41
42    lines = o3d.geometry.LineSet()
43    lines.points = o3d.utility.Vector3dVector(pts)
44    lines.lines = o3d.utility.Vector2iVector(line_indices)
45    # The default color of the lines is white, which will be invisible on the
46    # default white background. So we either need to set the color of the lines
47    # or the base_color of the material.
48    lines.colors = o3d.utility.Vector3dVector(colors)
49
50    # Some platforms do not require OpenGL implementations to support wide lines,
51    # so the renderer requires a custom shader to implement this: "unlitLine".
52    # The line_width field is only used by this shader; all other shaders ignore
53    # it.
54    mat = o3d.visualization.rendering.MaterialRecord()
55    mat.shader = "unlitLine"
56    mat.line_width = 10  # note that this is scaled with respect to pixels,
57    # so will give different results depending on the
58    # scaling values of your system
59    o3d.visualization.draw({
60        "name": "lines",
61        "geometry": lines,
62        "material": mat
63    })
64
65
66if __name__ == "__main__":
67    main()

load_save_viewpoint.py

 1# ----------------------------------------------------------------------------
 2# -                        Open3D: www.open3d.org                            -
 3# ----------------------------------------------------------------------------
 4# The MIT License (MIT)
 5#
 6# Copyright (c) 2018-2021 www.open3d.org
 7#
 8# Permission is hereby granted, free of charge, to any person obtaining a copy
 9# of this software and associated documentation files (the "Software"), to deal
10# in the Software without restriction, including without limitation the rights
11# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12# copies of the Software, and to permit persons to whom the Software is
13# furnished to do so, subject to the following conditions:
14#
15# The above copyright notice and this permission notice shall be included in
16# all copies or substantial portions of the Software.
17#
18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
24# IN THE SOFTWARE.
25# ----------------------------------------------------------------------------
26
27import open3d as o3d
28
29
30def save_view_point(pcd, filename):
31    vis = o3d.visualization.Visualizer()
32    vis.create_window()
33    vis.add_geometry(pcd)
34    vis.run()  # user changes the view and press "q" to terminate
35    param = vis.get_view_control().convert_to_pinhole_camera_parameters()
36    o3d.io.write_pinhole_camera_parameters(filename, param)
37    vis.destroy_window()
38
39
40def load_view_point(pcd, filename):
41    vis = o3d.visualization.Visualizer()
42    vis.create_window()
43    ctr = vis.get_view_control()
44    param = o3d.io.read_pinhole_camera_parameters(filename)
45    vis.add_geometry(pcd)
46    ctr.convert_from_pinhole_camera_parameters(param)
47    vis.run()
48    vis.destroy_window()
49
50
51if __name__ == "__main__":
52    pcd_data = o3d.data.PCDPointCloud()
53    pcd = o3d.io.read_point_cloud(pcd_data.path)
54    save_view_point(pcd, "viewpoint.json")
55    load_view_point(pcd, "viewpoint.json")

mouse_and_point_coord.py

  1# ----------------------------------------------------------------------------
  2# -                        Open3D: www.open3d.org                            -
  3# ----------------------------------------------------------------------------
  4# The MIT License (MIT)
  5#
  6# Copyright (c) 2018-2021 www.open3d.org
  7#
  8# Permission is hereby granted, free of charge, to any person obtaining a copy
  9# of this software and associated documentation files (the "Software"), to deal
 10# in the Software without restriction, including without limitation the rights
 11# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 12# copies of the Software, and to permit persons to whom the Software is
 13# furnished to do so, subject to the following conditions:
 14#
 15# The above copyright notice and this permission notice shall be included in
 16# all copies or substantial portions of the Software.
 17#
 18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 19# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 20# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 21# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 22# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 23# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 24# IN THE SOFTWARE.
 25# ----------------------------------------------------------------------------
 26
 27import numpy as np
 28import open3d as o3d
 29import open3d.visualization.gui as gui
 30import open3d.visualization.rendering as rendering
 31
 32
 33# This example displays a point cloud and if you Ctrl-click on a point
 34# (Cmd-click on macOS) it will show the coordinates of the point.
 35# This example illustrates:
 36# - custom mouse handling on SceneWidget
 37# - getting a the depth value of a point (OpenGL depth)
 38# - converting from a window point + OpenGL depth to world coordinate
 39class ExampleApp:
 40
 41    def __init__(self, cloud):
 42        # We will create a SceneWidget that fills the entire window, and then
 43        # a label in the lower left on top of the SceneWidget to display the
 44        # coordinate.
 45        app = gui.Application.instance
 46        self.window = app.create_window("Open3D - GetCoord Example", 1024, 768)
 47        # Since we want the label on top of the scene, we cannot use a layout,
 48        # so we need to manually layout the window's children.
 49        self.window.set_on_layout(self._on_layout)
 50        self.widget3d = gui.SceneWidget()
 51        self.window.add_child(self.widget3d)
 52        self.info = gui.Label("")
 53        self.info.visible = False
 54        self.window.add_child(self.info)
 55
 56        self.widget3d.scene = rendering.Open3DScene(self.window.renderer)
 57
 58        mat = rendering.MaterialRecord()
 59        mat.shader = "defaultUnlit"
 60        # Point size is in native pixels, but "pixel" means different things to
 61        # different platforms (macOS, in particular), so multiply by Window scale
 62        # factor.
 63        mat.point_size = 3 * self.window.scaling
 64        self.widget3d.scene.add_geometry("Point Cloud", cloud, mat)
 65
 66        bounds = self.widget3d.scene.bounding_box
 67        center = bounds.get_center()
 68        self.widget3d.setup_camera(60, bounds, center)
 69        self.widget3d.look_at(center, center - [0, 0, 3], [0, -1, 0])
 70
 71        self.widget3d.set_on_mouse(self._on_mouse_widget3d)
 72
 73    def _on_layout(self, layout_context):
 74        r = self.window.content_rect
 75        self.widget3d.frame = r
 76        pref = self.info.calc_preferred_size(layout_context,
 77                                             gui.Widget.Constraints())
 78        self.info.frame = gui.Rect(r.x,
 79                                   r.get_bottom() - pref.height, pref.width,
 80                                   pref.height)
 81
 82    def _on_mouse_widget3d(self, event):
 83        # We could override BUTTON_DOWN without a modifier, but that would
 84        # interfere with manipulating the scene.
 85        if event.type == gui.MouseEvent.Type.BUTTON_DOWN and event.is_modifier_down(
 86                gui.KeyModifier.CTRL):
 87
 88            def depth_callback(depth_image):
 89                # Coordinates are expressed in absolute coordinates of the
 90                # window, but to dereference the image correctly we need them
 91                # relative to the origin of the widget. Note that even if the
 92                # scene widget is the only thing in the window, if a menubar
 93                # exists it also takes up space in the window (except on macOS).
 94                x = event.x - self.widget3d.frame.x
 95                y = event.y - self.widget3d.frame.y
 96                # Note that np.asarray() reverses the axes.
 97                depth = np.asarray(depth_image)[y, x]
 98
 99                if depth == 1.0:  # clicked on nothing (i.e. the far plane)
100                    text = ""
101                else:
102                    world = self.widget3d.scene.camera.unproject(
103                        event.x, event.y, depth, self.widget3d.frame.width,
104                        self.widget3d.frame.height)
105                    text = "({:.3f}, {:.3f}, {:.3f})".format(
106                        world[0], world[1], world[2])
107
108                # This is not called on the main thread, so we need to
109                # post to the main thread to safely access UI items.
110                def update_label():
111                    self.info.text = text
112                    self.info.visible = (text != "")
113                    # We are sizing the info label to be exactly the right size,
114                    # so since the text likely changed width, we need to
115                    # re-layout to set the new frame.
116                    self.window.set_needs_layout()
117
118                gui.Application.instance.post_to_main_thread(
119                    self.window, update_label)
120
121            self.widget3d.scene.scene.render_to_depth_image(depth_callback)
122            return gui.Widget.EventCallbackResult.HANDLED
123        return gui.Widget.EventCallbackResult.IGNORED
124
125
126def main():
127    app = gui.Application.instance
128    app.initialize()
129
130    # This example will also work with a triangle mesh, or any 3D object.
131    # If you use a triangle mesh you will probably want to set the material
132    # shader to "defaultLit" instead of "defaultUnlit".
133    pcd_data = o3d.data.DemoICPPointClouds()
134    cloud = o3d.io.read_point_cloud(pcd_data.paths[0])
135    ex = ExampleApp(cloud)
136
137    app.run()
138
139
140if __name__ == "__main__":
141    main()

multiple_windows.py

  1# ----------------------------------------------------------------------------
  2# -                        Open3D: www.open3d.org                            -
  3# ----------------------------------------------------------------------------
  4# The MIT License (MIT)
  5#
  6# Copyright (c) 2018-2021 www.open3d.org
  7#
  8# Permission is hereby granted, free of charge, to any person obtaining a copy
  9# of this software and associated documentation files (the "Software"), to deal
 10# in the Software without restriction, including without limitation the rights
 11# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 12# copies of the Software, and to permit persons to whom the Software is
 13# furnished to do so, subject to the following conditions:
 14#
 15# The above copyright notice and this permission notice shall be included in
 16# all copies or substantial portions of the Software.
 17#
 18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 19# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 20# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 21# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 22# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 23# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 24# IN THE SOFTWARE.
 25# ----------------------------------------------------------------------------
 26
 27import numpy as np
 28import open3d as o3d
 29import threading
 30import time
 31
 32CLOUD_NAME = "points"
 33
 34
 35def main():
 36    MultiWinApp().run()
 37
 38
 39class MultiWinApp:
 40
 41    def __init__(self):
 42        self.is_done = False
 43        self.n_snapshots = 0
 44        self.cloud = None
 45        self.main_vis = None
 46        self.snapshot_pos = None
 47
 48    def run(self):
 49        app = o3d.visualization.gui.Application.instance
 50        app.initialize()
 51
 52        self.main_vis = o3d.visualization.O3DVisualizer(
 53            "Open3D - Multi-Window Demo")
 54        self.main_vis.add_action("Take snapshot in new window",
 55                                 self.on_snapshot)
 56        self.main_vis.set_on_close(self.on_main_window_closing)
 57
 58        app.add_window(self.main_vis)
 59        self.snapshot_pos = (self.main_vis.os_frame.x, self.main_vis.os_frame.y)
 60
 61        threading.Thread(target=self.update_thread).start()
 62
 63        app.run()
 64
 65    def on_snapshot(self, vis):
 66        self.n_snapshots += 1
 67        self.snapshot_pos = (self.snapshot_pos[0] + 50,
 68                             self.snapshot_pos[1] + 50)
 69        title = "Open3D - Multi-Window Demo (Snapshot #" + str(
 70            self.n_snapshots) + ")"
 71        new_vis = o3d.visualization.O3DVisualizer(title)
 72        mat = o3d.visualization.rendering.MaterialRecord()
 73        mat.shader = "defaultUnlit"
 74        new_vis.add_geometry(CLOUD_NAME + " #" + str(self.n_snapshots),
 75                             self.cloud, mat)
 76        new_vis.reset_camera_to_default()
 77        bounds = self.cloud.get_axis_aligned_bounding_box()
 78        extent = bounds.get_extent()
 79        new_vis.setup_camera(60, bounds.get_center(),
 80                             bounds.get_center() + [0, 0, -3], [0, -1, 0])
 81        o3d.visualization.gui.Application.instance.add_window(new_vis)
 82        new_vis.os_frame = o3d.visualization.gui.Rect(self.snapshot_pos[0],
 83                                                      self.snapshot_pos[1],
 84                                                      new_vis.os_frame.width,
 85                                                      new_vis.os_frame.height)
 86
 87    def on_main_window_closing(self):
 88        self.is_done = True
 89        return True  # False would cancel the close
 90
 91    def update_thread(self):
 92        # This is NOT the UI thread, need to call post_to_main_thread() to update
 93        # the scene or any part of the UI.
 94        pcd_data = o3d.data.DemoICPPointClouds()
 95        self.cloud = o3d.io.read_point_cloud(pcd_data.paths[0])
 96        bounds = self.cloud.get_axis_aligned_bounding_box()
 97        extent = bounds.get_extent()
 98
 99        def add_first_cloud():
100            mat = o3d.visualization.rendering.MaterialRecord()
101            mat.shader = "defaultUnlit"
102            self.main_vis.add_geometry(CLOUD_NAME, self.cloud, mat)
103            self.main_vis.reset_camera_to_default()
104            self.main_vis.setup_camera(60, bounds.get_center(),
105                                       bounds.get_center() + [0, 0, -3],
106                                       [0, -1, 0])
107
108        o3d.visualization.gui.Application.instance.post_to_main_thread(
109            self.main_vis, add_first_cloud)
110
111        while not self.is_done:
112            time.sleep(0.1)
113
114            # Perturb the cloud with a random walk to simulate an actual read
115            pts = np.asarray(self.cloud.points)
116            magnitude = 0.005 * extent
117            displacement = magnitude * (np.random.random_sample(pts.shape) -
118                                        0.5)
119            new_pts = pts + displacement
120            self.cloud.points = o3d.utility.Vector3dVector(new_pts)
121
122            def update_cloud():
123                # Note: if the number of points is less than or equal to the
124                #       number of points in the original object that was added,
125                #       using self.scene.update_geometry() will be faster.
126                #       Requires that the point cloud be a t.PointCloud.
127                self.main_vis.remove_geometry(CLOUD_NAME)
128                mat = o3d.visualization.rendering.MaterialRecord()
129                mat.shader = "defaultUnlit"
130                self.main_vis.add_geometry(CLOUD_NAME, self.cloud, mat)
131
132            if self.is_done:  # might have changed while sleeping
133                break
134            o3d.visualization.gui.Application.instance.post_to_main_thread(
135                self.main_vis, update_cloud)
136
137
138if __name__ == "__main__":
139    main()

non_blocking_visualization.py

 1# ----------------------------------------------------------------------------
 2# -                        Open3D: www.open3d.org                            -
 3# ----------------------------------------------------------------------------
 4# The MIT License (MIT)
 5#
 6# Copyright (c) 2018-2021 www.open3d.org
 7#
 8# Permission is hereby granted, free of charge, to any person obtaining a copy
 9# of this software and associated documentation files (the "Software"), to deal
10# in the Software without restriction, including without limitation the rights
11# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12# copies of the Software, and to permit persons to whom the Software is
13# furnished to do so, subject to the following conditions:
14#
15# The above copyright notice and this permission notice shall be included in
16# all copies or substantial portions of the Software.
17#
18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
24# IN THE SOFTWARE.
25# ----------------------------------------------------------------------------
26
27# examples/python/visualization/non_blocking_visualization.py
28
29import open3d as o3d
30import numpy as np
31
32if __name__ == "__main__":
33    o3d.utility.set_verbosity_level(o3d.utility.VerbosityLevel.Debug)
34    pcd_data = o3d.data.DemoICPPointClouds()
35    source_raw = o3d.io.read_point_cloud(pcd_data.paths[0])
36    target_raw = o3d.io.read_point_cloud(pcd_data.paths[1])
37
38    source = source_raw.voxel_down_sample(voxel_size=0.02)
39    target = target_raw.voxel_down_sample(voxel_size=0.02)
40    trans = [[0.862, 0.011, -0.507, 0.0], [-0.139, 0.967, -0.215, 0.7],
41             [0.487, 0.255, 0.835, -1.4], [0.0, 0.0, 0.0, 1.0]]
42    source.transform(trans)
43
44    flip_transform = [[1, 0, 0, 0], [0, -1, 0, 0], [0, 0, -1, 0], [0, 0, 0, 1]]
45    source.transform(flip_transform)
46    target.transform(flip_transform)
47
48    vis = o3d.visualization.Visualizer()
49    vis.create_window()
50    vis.add_geometry(source)
51    vis.add_geometry(target)
52    threshold = 0.05
53    icp_iteration = 100
54    save_image = False
55
56    for i in range(icp_iteration):
57        reg_p2l = o3d.pipelines.registration.registration_icp(
58            source, target, threshold, np.identity(4),
59            o3d.pipelines.registration.TransformationEstimationPointToPlane(),
60            o3d.pipelines.registration.ICPConvergenceCriteria(max_iteration=1))
61        source.transform(reg_p2l.transformation)
62        vis.update_geometry(source)
63        vis.poll_events()
64        vis.update_renderer()
65        if save_image:
66            vis.capture_screen_image("temp_%04d.jpg" % i)
67    vis.destroy_window()
68    o3d.utility.set_verbosity_level(o3d.utility.VerbosityLevel.Info)

non_english.py

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

online_processing.py

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

remove_geometry.py

 1# ----------------------------------------------------------------------------
 2# -                        Open3D: www.open3d.org                            -
 3# ----------------------------------------------------------------------------
 4# The MIT License (MIT)
 5#
 6# Copyright (c) 2018-2021 www.open3d.org
 7#
 8# Permission is hereby granted, free of charge, to any person obtaining a copy
 9# of this software and associated documentation files (the "Software"), to deal
10# in the Software without restriction, including without limitation the rights
11# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12# copies of the Software, and to permit persons to whom the Software is
13# furnished to do so, subject to the following conditions:
14#
15# The above copyright notice and this permission notice shall be included in
16# all copies or substantial portions of the Software.
17#
18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
24# IN THE SOFTWARE.
25# ----------------------------------------------------------------------------
26
27import open3d as o3d
28import numpy as np
29import time
30import copy
31
32
33def visualize_non_blocking(vis, pcds):
34    for pcd in pcds:
35        vis.update_geometry(pcd)
36    vis.poll_events()
37    vis.update_renderer()
38
39
40pcd_data = o3d.data.PCDPointCloud()
41pcd_orig = o3d.io.read_point_cloud(pcd_data.path)
42flip_transform = [[1, 0, 0, 0], [0, -1, 0, 0], [0, 0, -1, 0], [0, 0, 0, 1]]
43pcd_orig.transform(flip_transform)
44n_pcd = 5
45pcds = []
46for i in range(n_pcd):
47    pcds.append(copy.deepcopy(pcd_orig))
48    trans = np.identity(4)
49    trans[:3, 3] = [3 * i, 0, 0]
50    pcds[i].transform(trans)
51
52vis = o3d.visualization.Visualizer()
53vis.create_window()
54start_time = time.time()
55added = [False] * n_pcd
56
57curr_sec = int(time.time() - start_time)
58prev_sec = curr_sec - 1
59
60while True:
61    curr_sec = int(time.time() - start_time)
62    if curr_sec - prev_sec == 1:
63        prev_sec = curr_sec
64
65        for i in range(n_pcd):
66            if curr_sec % (n_pcd * 2) == i and not added[i]:
67                vis.add_geometry(pcds[i])
68                added[i] = True
69                print("Adding %d" % i)
70            if curr_sec % (n_pcd * 2) == (i + n_pcd) and added[i]:
71                vis.remove_geometry(pcds[i])
72                added[i] = False
73                print("Removing %d" % i)
74
75    visualize_non_blocking(vis, pcds)

render_to_image.py

 1# ----------------------------------------------------------------------------
 2# -                        Open3D: www.open3d.org                            -
 3# ----------------------------------------------------------------------------
 4# The MIT License (MIT)
 5#
 6# Copyright (c) 2018-2021 www.open3d.org
 7#
 8# Permission is hereby granted, free of charge, to any person obtaining a copy
 9# of this software and associated documentation files (the "Software"), to deal
10# in the Software without restriction, including without limitation the rights
11# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12# copies of the Software, and to permit persons to whom the Software is
13# furnished to do so, subject to the following conditions:
14#
15# The above copyright notice and this permission notice shall be included in
16# all copies or substantial portions of the Software.
17#
18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
24# IN THE SOFTWARE.
25# ----------------------------------------------------------------------------
26
27import open3d as o3d
28import open3d.visualization.rendering as rendering
29
30
31def main():
32    render = rendering.OffscreenRenderer(640, 480)
33
34    yellow = rendering.MaterialRecord()
35    yellow.base_color = [1.0, 0.75, 0.0, 1.0]
36    yellow.shader = "defaultLit"
37
38    green = rendering.MaterialRecord()
39    green.base_color = [0.0, 0.5, 0.0, 1.0]
40    green.shader = "defaultLit"
41
42    grey = rendering.MaterialRecord()
43    grey.base_color = [0.7, 0.7, 0.7, 1.0]
44    grey.shader = "defaultLit"
45
46    white = rendering.MaterialRecord()
47    white.base_color = [1.0, 1.0, 1.0, 1.0]
48    white.shader = "defaultLit"
49
50    cyl = o3d.geometry.TriangleMesh.create_cylinder(.05, 3)
51    cyl.compute_vertex_normals()
52    cyl.translate([-2, 0, 1.5])
53    sphere = o3d.geometry.TriangleMesh.create_sphere(.2)
54    sphere.compute_vertex_normals()
55    sphere.translate([-2, 0, 3])
56
57    box = o3d.geometry.TriangleMesh.create_box(2, 2, 1)
58    box.compute_vertex_normals()
59    box.translate([-1, -1, 0])
60    solid = o3d.geometry.TriangleMesh.create_icosahedron(0.5)
61    solid.compute_triangle_normals()
62    solid.compute_vertex_normals()
63    solid.translate([0, 0, 1.75])
64
65    render.scene.add_geometry("cyl", cyl, green)
66    render.scene.add_geometry("sphere", sphere, yellow)
67    render.scene.add_geometry("box", box, grey)
68    render.scene.add_geometry("solid", solid, white)
69    render.setup_camera(60.0, [0, 0, 0], [0, 10, 0], [0, 0, 1])
70    render.scene.scene.set_sun_light([0.707, 0.0, -.707], [1.0, 1.0, 1.0],
71                                     75000)
72    render.scene.scene.enable_sun_light(True)
73    render.scene.show_axes(True)
74
75    img = render.render_to_image()
76    print("Saving image at test.png")
77    o3d.io.write_image("test.png", img, 9)
78
79    render.setup_camera(60.0, [0, 0, 0], [-10, 0, 0], [0, 0, 1])
80    img = render.render_to_image()
81    print("Saving image at test2.png")
82    o3d.io.write_image("test2.png", img, 9)
83
84
85if __name__ == "__main__":
86    main()

tensorboard_pytorch.py

  1# ----------------------------------------------------------------------------
  2# -                        Open3D: www.open3d.org                            -
  3# ----------------------------------------------------------------------------
  4# The MIT License (MIT)
  5#
  6# Copyright (c) 2018-2021 www.open3d.org
  7#
  8# Permission is hereby granted, free of charge, to any person obtaining a copy
  9# of this software and associated documentation files (the "Software"), to deal
 10# in the Software without restriction, including without limitation the rights
 11# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 12# copies of the Software, and to permit persons to whom the Software is
 13# furnished to do so, subject to the following conditions:
 14#
 15# The above copyright notice and this permission notice shall be included in
 16# all copies or substantial portions of the Software.
 17#
 18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 19# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 20# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 21# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 22# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 23# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 24# IN THE SOFTWARE.
 25# ----------------------------------------------------------------------------
 26import copy
 27import os
 28import sys
 29import numpy as np
 30import open3d as o3d
 31# pylint: disable-next=unused-import
 32from open3d.visualization.tensorboard_plugin.util import to_dict_batch
 33from torch.utils.tensorboard import SummaryWriter
 34
 35BASE_LOGDIR = "demo_logs/pytorch/"
 36MODEL_DIR = os.path.join(
 37    os.path.dirname(os.path.dirname(os.path.dirname(
 38        os.path.realpath(__file__)))), "test_data", "monkey")
 39
 40
 41def small_scale(run_name="small_scale"):
 42    """Basic demo with cube and cylinder with normals and colors.
 43    """
 44    logdir = os.path.join(BASE_LOGDIR, run_name)
 45    writer = SummaryWriter(logdir)
 46
 47    cube = o3d.geometry.TriangleMesh.create_box(1, 2, 4, create_uv_map=True)
 48    cube.compute_vertex_normals()
 49    cylinder = o3d.geometry.TriangleMesh.create_cylinder(radius=1.0,
 50                                                         height=2.0,
 51                                                         resolution=20,
 52                                                         split=4,
 53                                                         create_uv_map=True)
 54    cylinder.compute_vertex_normals()
 55    colors = [(1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)]
 56    for step in range(3):
 57        cube.paint_uniform_color(colors[step])
 58        writer.add_3d('cube', to_dict_batch([cube]), step=step)
 59        cylinder.paint_uniform_color(colors[step])
 60        writer.add_3d('cylinder', to_dict_batch([cylinder]), step=step)
 61
 62
 63def property_reference(run_name="property_reference"):
 64    """Produces identical visualization to small_scale, but does not store
 65    repeated properties of ``vertex_positions`` and ``vertex_normals``.
 66    """
 67    logdir = os.path.join(BASE_LOGDIR, run_name)
 68    writer = SummaryWriter(logdir)
 69
 70    cube = o3d.geometry.TriangleMesh.create_box(1, 2, 4, create_uv_map=True)
 71    cube.compute_vertex_normals()
 72    cylinder = o3d.geometry.TriangleMesh.create_cylinder(radius=1.0,
 73                                                         height=2.0,
 74                                                         resolution=20,
 75                                                         split=4,
 76                                                         create_uv_map=True)
 77    cylinder.compute_vertex_normals()
 78    colors = [(1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)]
 79    for step in range(3):
 80        cube.paint_uniform_color(colors[step])
 81        cube_summary = to_dict_batch([cube])
 82        if step > 0:
 83            cube_summary['vertex_positions'] = 0
 84            cube_summary['vertex_normals'] = 0
 85        writer.add_3d('cube', cube_summary, step=step)
 86        cylinder.paint_uniform_color(colors[step])
 87        cylinder_summary = to_dict_batch([cylinder])
 88        if step > 0:
 89            cylinder_summary['vertex_positions'] = 0
 90            cylinder_summary['vertex_normals'] = 0
 91        writer.add_3d('cylinder', cylinder_summary, step=step)
 92
 93
 94def large_scale(n_steps=16,
 95                batch_size=1,
 96                base_resolution=200,
 97                run_name="large_scale"):
 98    """Generate a large scale summary. Geometry resolution increases linearly
 99    with step. Each element in a batch is painted a different color.
100    """
101    logdir = os.path.join(BASE_LOGDIR, run_name)
102    writer = SummaryWriter(logdir)
103    colors = []
104    for k in range(batch_size):
105        t = k * np.pi / batch_size
106        colors.append(((1 + np.sin(t)) / 2, (1 + np.cos(t)) / 2, t / np.pi))
107    for step in range(n_steps):
108        resolution = base_resolution * (step + 1)
109        cylinder_list = []
110        mobius_list = []
111        cylinder = o3d.geometry.TriangleMesh.create_cylinder(
112            radius=1.0, height=2.0, resolution=resolution, split=4)
113        cylinder.compute_vertex_normals()
114        mobius = o3d.geometry.TriangleMesh.create_mobius(
115            length_split=int(3.5 * resolution),
116            width_split=int(0.75 * resolution),
117            twists=1,
118            raidus=1,
119            flatness=1,
120            width=1,
121            scale=1)
122        mobius.compute_vertex_normals()
123        for b in range(batch_size):
124            cylinder_list.append(copy.deepcopy(cylinder))
125            cylinder_list[b].paint_uniform_color(colors[b])
126            mobius_list.append(copy.deepcopy(mobius))
127            mobius_list[b].paint_uniform_color(colors[b])
128        writer.add_3d('cylinder',
129                      to_dict_batch(cylinder_list),
130                      step=step,
131                      max_outputs=batch_size)
132        writer.add_3d('mobius',
133                      to_dict_batch(mobius_list),
134                      step=step,
135                      max_outputs=batch_size)
136
137
138def with_material(model_dir=MODEL_DIR):
139    """Read an obj model from a directory and write as a TensorBoard summary.
140    """
141    model_name = os.path.basename(model_dir)
142    logdir = os.path.join(BASE_LOGDIR, model_name)
143    model_path = os.path.join(model_dir, model_name + ".obj")
144    model = o3d.t.geometry.TriangleMesh.from_legacy(
145        o3d.io.read_triangle_mesh(model_path))
146    summary_3d = {
147        "vertex_positions": model.vertex["positions"],
148        "vertex_normals": model.vertex["normals"],
149        "triangle_texture_uvs": model.triangle["texture_uvs"],
150        "triangle_indices": model.triangle["indices"],
151        "material_name": "defaultLit"
152    }
153    names_to_o3dprop = {"ao": "ambient_occlusion"}
154
155    for texture in ("albedo", "normal", "ao", "metallic", "roughness"):
156        texture_file = os.path.join(model_dir, texture + ".png")
157        if os.path.exists(texture_file):
158            texture = names_to_o3dprop.get(texture, texture)
159            summary_3d.update({
160                ("material_texture_map_" + texture):
161                    o3d.t.io.read_image(texture_file)
162            })
163            if texture == "metallic":
164                summary_3d.update(material_scalar_metallic=1.0)
165
166    writer = SummaryWriter(logdir)
167    writer.add_3d(model_name, summary_3d, step=0)
168
169
170def demo_scene():
171    """Write the demo_scene.py example showing rich PBR materials as a summary
172    """
173    import demo_scene
174    demo_scene.check_for_required_assets()
175    geoms = demo_scene.create_scene()
176    writer = SummaryWriter(os.path.join(BASE_LOGDIR, 'demo_scene'))
177    for geom_data in geoms:
178        geom = geom_data["geometry"]
179        summary_3d = {}
180        for key, tensor in geom.vertex.items():
181            summary_3d["vertex_" + key] = tensor
182        for key, tensor in geom.triangle.items():
183            summary_3d["triangle_" + key] = tensor
184        if geom.has_valid_material():
185            summary_3d["material_name"] = geom.material.material_name
186            for key, value in geom.material.scalar_properties.items():
187                summary_3d["material_scalar_" + key] = value
188            for key, value in geom.material.vector_properties.items():
189                summary_3d["material_vector_" + key] = value
190            for key, value in geom.material.texture_maps.items():
191                summary_3d["material_texture_map_" + key] = value
192        writer.add_3d(geom_data["name"], summary_3d, step=0)
193
194
195if __name__ == "__main__":
196
197    examples = ('small_scale', 'large_scale', 'property_reference',
198                'with_material', 'demo_scene')
199    selected = tuple(eg for eg in sys.argv[1:] if eg in examples)
200    if len(selected) == 0:
201        print(f'Usage: python {__file__} EXAMPLE...')
202        print(f'  where EXAMPLE are from {examples}')
203        selected = ('property_reference', 'with_material')
204
205    for eg in selected:
206        locals()[eg]()
207
208    print(f"Run 'tensorboard --logdir {BASE_LOGDIR}' to visualize the 3D "
209          "summary.")

tensorboard_tensorflow.py

  1# ----------------------------------------------------------------------------
  2# -                        Open3D: www.open3d.org                            -
  3# ----------------------------------------------------------------------------
  4# The MIT License (MIT)
  5#
  6# Copyright (c) 2018-2021 www.open3d.org
  7#
  8# Permission is hereby granted, free of charge, to any person obtaining a copy
  9# of this software and associated documentation files (the "Software"), to deal
 10# in the Software without restriction, including without limitation the rights
 11# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 12# copies of the Software, and to permit persons to whom the Software is
 13# furnished to do so, subject to the following conditions:
 14#
 15# The above copyright notice and this permission notice shall be included in
 16# all copies or substantial portions of the Software.
 17#
 18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 19# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 20# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 21# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 22# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 23# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 24# IN THE SOFTWARE.
 25# ----------------------------------------------------------------------------
 26import copy
 27import os
 28import sys
 29import numpy as np
 30import open3d as o3d
 31from open3d.visualization.tensorboard_plugin import summary
 32from open3d.visualization.tensorboard_plugin.util import to_dict_batch
 33import tensorflow as tf
 34
 35BASE_LOGDIR = "demo_logs/tf/"
 36MODEL_DIR = os.path.join(
 37    os.path.dirname(os.path.dirname(os.path.dirname(
 38        os.path.realpath(__file__)))), "test_data", "monkey")
 39
 40
 41def small_scale(run_name="small_scale"):
 42    """Basic demo with cube and cylinder with normals and colors.
 43    """
 44    logdir = os.path.join(BASE_LOGDIR, run_name)
 45    writer = tf.summary.create_file_writer(logdir)
 46
 47    cube = o3d.geometry.TriangleMesh.create_box(1, 2, 4, create_uv_map=True)
 48    cube.compute_vertex_normals()
 49    cylinder = o3d.geometry.TriangleMesh.create_cylinder(radius=1.0,
 50                                                         height=2.0,
 51                                                         resolution=20,
 52                                                         split=4,
 53                                                         create_uv_map=True)
 54    cylinder.compute_vertex_normals()
 55    colors = [(1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)]
 56    with writer.as_default():
 57        for step in range(3):
 58            cube.paint_uniform_color(colors[step])
 59            summary.add_3d('cube',
 60                           to_dict_batch([cube]),
 61                           step=step,
 62                           logdir=logdir)
 63            cylinder.paint_uniform_color(colors[step])
 64            summary.add_3d('cylinder',
 65                           to_dict_batch([cylinder]),
 66                           step=step,
 67                           logdir=logdir)
 68
 69
 70def property_reference(run_name="property_reference"):
 71    """Produces identical visualization to small_scale, but does not store
 72    repeated properties of ``vertex_positions`` and ``vertex_normals``.
 73    """
 74    logdir = os.path.join(BASE_LOGDIR, run_name)
 75    writer = tf.summary.create_file_writer(logdir)
 76
 77    cube = o3d.geometry.TriangleMesh.create_box(1, 2, 4, create_uv_map=True)
 78    cube.compute_vertex_normals()
 79    cylinder = o3d.geometry.TriangleMesh.create_cylinder(radius=1.0,
 80                                                         height=2.0,
 81                                                         resolution=20,
 82                                                         split=4,
 83                                                         create_uv_map=True)
 84    cylinder.compute_vertex_normals()
 85    colors = [(1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)]
 86    with writer.as_default():
 87        for step in range(3):
 88            cube.paint_uniform_color(colors[step])
 89            cube_summary = to_dict_batch([cube])
 90            if step > 0:
 91                cube_summary['vertex_positions'] = 0
 92                cube_summary['vertex_normals'] = 0
 93            summary.add_3d('cube', cube_summary, step=step, logdir=logdir)
 94            cylinder.paint_uniform_color(colors[step])
 95            cylinder_summary = to_dict_batch([cylinder])
 96            if step > 0:
 97                cylinder_summary['vertex_positions'] = 0
 98                cylinder_summary['vertex_normals'] = 0
 99            summary.add_3d('cylinder',
100                           cylinder_summary,
101                           step=step,
102                           logdir=logdir)
103
104
105def large_scale(n_steps=16,
106                batch_size=1,
107                base_resolution=200,
108                run_name="large_scale"):
109    """Generate a large scale summary. Geometry resolution increases linearly
110    with step. Each element in a batch is painted a different color.
111    """
112    logdir = os.path.join(BASE_LOGDIR, run_name)
113    writer = tf.summary.create_file_writer(logdir)
114    colors = []
115    for k in range(batch_size):
116        t = k * np.pi / batch_size
117        colors.append(((1 + np.sin(t)) / 2, (1 + np.cos(t)) / 2, t / np.pi))
118    with writer.as_default():
119        for step in range(n_steps):
120            resolution = base_resolution * (step + 1)
121            cylinder_list = []
122            mobius_list = []
123            cylinder = o3d.geometry.TriangleMesh.create_cylinder(
124                radius=1.0, height=2.0, resolution=resolution, split=4)
125            cylinder.compute_vertex_normals()
126            mobius = o3d.geometry.TriangleMesh.create_mobius(
127                length_split=int(3.5 * resolution),
128                width_split=int(0.75 * resolution),
129                twists=1,
130                raidus=1,
131                flatness=1,
132                width=1,
133                scale=1)
134            mobius.compute_vertex_normals()
135            for b in range(batch_size):
136                cylinder_list.append(copy.deepcopy(cylinder))
137                cylinder_list[b].paint_uniform_color(colors[b])
138                mobius_list.append(copy.deepcopy(mobius))
139                mobius_list[b].paint_uniform_color(colors[b])
140            summary.add_3d('cylinder',
141                           to_dict_batch(cylinder_list),
142                           step=step,
143                           logdir=logdir,
144                           max_outputs=batch_size)
145            summary.add_3d('mobius',
146                           to_dict_batch(mobius_list),
147                           step=step,
148                           logdir=logdir,
149                           max_outputs=batch_size)
150
151
152def with_material(model_dir=MODEL_DIR):
153    """Read an obj model from a directory and write as a TensorBoard summary.
154    """
155    model_name = os.path.basename(model_dir)
156    logdir = os.path.join(BASE_LOGDIR, model_name)
157    model_path = os.path.join(model_dir, model_name + ".obj")
158    model = o3d.t.geometry.TriangleMesh.from_legacy(
159        o3d.io.read_triangle_mesh(model_path))
160    summary_3d = {
161        "vertex_positions": model.vertex["positions"],
162        "vertex_normals": model.vertex["normals"],
163        "triangle_texture_uvs": model.triangle["texture_uvs"],
164        "triangle_indices": model.triangle["indices"],
165        "material_name": "defaultLit"
166    }
167    names_to_o3dprop = {"ao": "ambient_occlusion"}
168
169    for texture in ("albedo", "normal", "ao", "metallic", "roughness"):
170        texture_file = os.path.join(model_dir, texture + ".png")
171        if os.path.exists(texture_file):
172            texture = names_to_o3dprop.get(texture, texture)
173            summary_3d.update({
174                ("material_texture_map_" + texture):
175                    o3d.t.io.read_image(texture_file)
176            })
177            if texture == "metallic":
178                summary_3d.update(material_scalar_metallic=1.0)
179
180    writer = tf.summary.create_file_writer(logdir)
181    with writer.as_default():
182        summary.add_3d(model_name, summary_3d, step=0, logdir=logdir)
183
184
185def demo_scene():
186    """Write the demo_scene.py example showing rich PBR materials as a summary.
187    """
188    import demo_scene
189    demo_scene.check_for_required_assets()
190    geoms = demo_scene.create_scene()
191    logdir = os.path.join(BASE_LOGDIR, 'demo_scene')
192    writer = tf.summary.create_file_writer(logdir)
193    for geom_data in geoms:
194        geom = geom_data["geometry"]
195        summary_3d = {}
196        for key, tensor in geom.vertex.items():
197            summary_3d["vertex_" + key] = tensor
198        for key, tensor in geom.triangle.items():
199            summary_3d["triangle_" + key] = tensor
200        if geom.has_valid_material():
201            summary_3d["material_name"] = geom.material.material_name
202            for key, value in geom.material.scalar_properties.items():
203                summary_3d["material_scalar_" + key] = value
204            for key, value in geom.material.vector_properties.items():
205                summary_3d["material_vector_" + key] = value
206            for key, value in geom.material.texture_maps.items():
207                summary_3d["material_texture_map_" + key] = value
208        with writer.as_default():
209            summary.add_3d(geom_data["name"], summary_3d, step=0, logdir=logdir)
210
211
212if __name__ == "__main__":
213
214    examples = ('small_scale', 'large_scale', 'property_reference',
215                'with_material', 'demo_scene')
216    selected = tuple(eg for eg in sys.argv[1:] if eg in examples)
217    if len(selected) == 0:
218        print(f'Usage: python {__file__} EXAMPLE...')
219        print(f'  where EXAMPLE are from {examples}')
220        selected = ('property_reference', 'with_material')
221
222    for eg in selected:
223        locals()[eg]()
224
225    print(f"Run 'tensorboard --logdir {BASE_LOGDIR}' to visualize the 3D "
226          "summary.")

text3d.py

 1# ----------------------------------------------------------------------------
 2# -                        Open3D: www.open3d.org                            -
 3# ----------------------------------------------------------------------------
 4# The MIT License (MIT)
 5#
 6# Copyright (c) 2018-2021 www.open3d.org
 7#
 8# Permission is hereby granted, free of charge, to any person obtaining a copy
 9# of this software and associated documentation files (the "Software"), to deal
10# in the Software without restriction, including without limitation the rights
11# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12# copies of the Software, and to permit persons to whom the Software is
13# furnished to do so, subject to the following conditions:
14#
15# The above copyright notice and this permission notice shall be included in
16# all copies or substantial portions of the Software.
17#
18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
24# IN THE SOFTWARE.
25# ----------------------------------------------------------------------------
26
27import numpy as np
28import open3d as o3d
29import open3d.visualization.gui as gui
30import open3d.visualization.rendering as rendering
31
32
33def make_point_cloud(npts, center, radius):
34    pts = np.random.uniform(-radius, radius, size=[npts, 3]) + center
35    cloud = o3d.geometry.PointCloud()
36    cloud.points = o3d.utility.Vector3dVector(pts)
37    colors = np.random.uniform(0.0, 1.0, size=[npts, 3])
38    cloud.colors = o3d.utility.Vector3dVector(colors)
39    return cloud
40
41
42def high_level():
43    app = gui.Application.instance
44    app.initialize()
45
46    points = make_point_cloud(100, (0, 0, 0), 1.0)
47
48    vis = o3d.visualization.O3DVisualizer("Open3D - 3D Text", 1024, 768)
49    vis.show_settings = True
50    vis.add_geometry("Points", points)
51    for idx in range(0, len(points.points)):
52        vis.add_3d_label(points.points[idx], "{}".format(idx))
53    vis.reset_camera_to_default()
54
55    app.add_window(vis)
56    app.run()
57
58
59def low_level():
60    app = gui.Application.instance
61    app.initialize()
62
63    points = make_point_cloud(100, (0, 0, 0), 1.0)
64
65    w = app.create_window("Open3D - 3D Text", 1024, 768)
66    widget3d = gui.SceneWidget()
67    widget3d.scene = rendering.Open3DScene(w.renderer)
68    mat = rendering.MaterialRecord()
69    mat.shader = "defaultUnlit"
70    mat.point_size = 5 * w.scaling
71    widget3d.scene.add_geometry("Points", points, mat)
72    for idx in range(0, len(points.points)):
73        l = widget3d.add_3d_label(points.points[idx], "{}".format(idx))
74        l.color = gui.Color(points.colors[idx][0], points.colors[idx][1],
75                            points.colors[idx][2])
76        l.scale = np.random.uniform(0.5, 3.0)
77    bbox = widget3d.scene.bounding_box
78    widget3d.setup_camera(60.0, bbox, bbox.get_center())
79    w.add_child(widget3d)
80
81    app.run()
82
83
84if __name__ == "__main__":
85    high_level()
86    low_level()

textured_mesh.py

 1# ----------------------------------------------------------------------------
 2# -                        Open3D: www.open3d.org                            -
 3# ----------------------------------------------------------------------------
 4# The MIT License (MIT)
 5#
 6# Copyright (c) 2018-2021 www.open3d.org
 7#
 8# Permission is hereby granted, free of charge, to any person obtaining a copy
 9# of this software and associated documentation files (the "Software"), to deal
10# in the Software without restriction, including without limitation the rights
11# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12# copies of the Software, and to permit persons to whom the Software is
13# furnished to do so, subject to the following conditions:
14#
15# The above copyright notice and this permission notice shall be included in
16# all copies or substantial portions of the Software.
17#
18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
24# IN THE SOFTWARE.
25# ----------------------------------------------------------------------------
26
27import sys
28import os
29import open3d as o3d
30
31
32def main():
33    if len(sys.argv) < 2:
34        print("""Usage: textured-mesh.py [model directory]
35    This example will load [model directory].obj plus any of albedo, normal,
36    ao, metallic and roughness textures present. The textures should be named
37    albedo.png, normal.png, ao.png, metallic.png and roughness.png
38    respectively.""")
39        sys.exit()
40
41    model_dir = os.path.normpath(os.path.realpath(sys.argv[1]))
42    model_name = os.path.join(model_dir, os.path.basename(model_dir) + ".obj")
43    mesh = o3d.t.geometry.TriangleMesh.from_legacy(
44        o3d.io.read_triangle_mesh(model_name))
45    material = mesh.material
46    material.material_name = "defaultLit"
47
48    names_to_o3dprop = {"ao": "ambient_occlusion"}
49    for texture in ("albedo", "normal", "ao", "metallic", "roughness"):
50        texture_file = os.path.join(model_dir, texture + ".png")
51        if os.path.exists(texture_file):
52            texture = names_to_o3dprop.get(texture, texture)
53            material.texture_maps[texture] = o3d.t.io.read_image(texture_file)
54    if "metallic" in material.texture_maps:
55        material.scalar_properties["metallic"] = 1.0
56
57    o3d.visualization.draw(mesh, title=model_name)
58
59
60if __name__ == "__main__":
61    main()

textured_model.py

 1# ----------------------------------------------------------------------------
 2# -                        Open3D: www.open3d.org                            -
 3# ----------------------------------------------------------------------------
 4# The MIT License (MIT)
 5#
 6# Copyright (c) 2018-2021 www.open3d.org
 7#
 8# Permission is hereby granted, free of charge, to any person obtaining a copy
 9# of this software and associated documentation files (the "Software"), to deal
10# in the Software without restriction, including without limitation the rights
11# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12# copies of the Software, and to permit persons to whom the Software is
13# furnished to do so, subject to the following conditions:
14#
15# The above copyright notice and this permission notice shall be included in
16# all copies or substantial portions of the Software.
17#
18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
24# IN THE SOFTWARE.
25# ----------------------------------------------------------------------------
26
27import sys
28import os
29import open3d as o3d
30
31
32def main():
33    if len(sys.argv) < 2:
34        print("""Usage: texture-model.py [model directory]
35    This example will load [model directory].obj plus any of albedo, normal,
36    ao, metallic and roughness textures present.""")
37        sys.exit()
38
39    model_dir = sys.argv[1]
40    model_name = os.path.join(model_dir, os.path.basename(model_dir) + ".obj")
41    model = o3d.io.read_triangle_mesh(model_name)
42    material = o3d.visualization.rendering.MaterialRecord()
43    material.shader = "defaultLit"
44
45    albedo_name = os.path.join(model_dir, "albedo.png")
46    normal_name = os.path.join(model_dir, "normal.png")
47    ao_name = os.path.join(model_dir, "ao.png")
48    metallic_name = os.path.join(model_dir, "metallic.png")
49    roughness_name = os.path.join(model_dir, "roughness.png")
50    if os.path.exists(albedo_name):
51        material.albedo_img = o3d.io.read_image(albedo_name)
52    if os.path.exists(normal_name):
53        material.normal_img = o3d.io.read_image(normal_name)
54    if os.path.exists(ao_name):
55        material.ao_img = o3d.io.read_image(ao_name)
56    if os.path.exists(metallic_name):
57        material.base_metallic = 1.0
58        material.metallic_img = o3d.io.read_image(metallic_name)
59    if os.path.exists(roughness_name):
60        material.roughness_img = o3d.io.read_image(roughness_name)
61
62    o3d.visualization.draw([{
63        "name": "cube",
64        "geometry": model,
65        "material": material
66    }])
67
68
69if __name__ == "__main__":
70    main()

video.py

  1# ----------------------------------------------------------------------------
  2# -                        Open3D: www.open3d.org                            -
  3# ----------------------------------------------------------------------------
  4# The MIT License (MIT)
  5#
  6# Copyright (c) 2018-2021 www.open3d.org
  7#
  8# Permission is hereby granted, free of charge, to any person obtaining a copy
  9# of this software and associated documentation files (the "Software"), to deal
 10# in the Software without restriction, including without limitation the rights
 11# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 12# copies of the Software, and to permit persons to whom the Software is
 13# furnished to do so, subject to the following conditions:
 14#
 15# The above copyright notice and this permission notice shall be included in
 16# all copies or substantial portions of the Software.
 17#
 18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 19# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 20# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 21# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 22# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 23# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 24# IN THE SOFTWARE.
 25# ----------------------------------------------------------------------------
 26
 27import numpy as np
 28import open3d as o3d
 29import open3d.visualization.gui as gui
 30import open3d.visualization.rendering as rendering
 31import time
 32import threading
 33
 34
 35def rescale_greyscale(img):
 36    data = np.asarray(img)
 37    assert (len(data.shape) == 2)  # requires 1 channel image
 38    dataFloat = data.astype(np.float64)
 39    max_val = dataFloat.max()
 40    # We don't currently support 16-bit images, so convert to 8-bit
 41    dataFloat *= 255.0 / max_val
 42    data8 = dataFloat.astype(np.uint8)
 43    return o3d.geometry.Image(data8)
 44
 45
 46class VideoWindow:
 47
 48    def __init__(self):
 49        self.rgb_images = []
 50        rgbd_data = o3d.data.SampleRedwoodRGBDImages()
 51        for path in rgbd_data.color_paths:
 52            img = o3d.io.read_image(path)
 53            self.rgb_images.append(img)
 54        self.depth_images = []
 55        for path in rgbd_data.depth_paths:
 56            img = o3d.io.read_image(path)
 57            # The images are pretty dark, so rescale them so that it is
 58            # obvious that this is a depth image, for the sake of the example
 59            img = rescale_greyscale(img)
 60            self.depth_images.append(img)
 61        assert (len(self.rgb_images) == len(self.depth_images))
 62
 63        self.window = gui.Application.instance.create_window(
 64            "Open3D - Video Example", 1000, 500)
 65        self.window.set_on_layout(self._on_layout)
 66        self.window.set_on_close(self._on_close)
 67
 68        self.widget3d = gui.SceneWidget()
 69        self.widget3d.scene = rendering.Open3DScene(self.window.renderer)
 70        self.window.add_child(self.widget3d)
 71
 72        lit = rendering.MaterialRecord()
 73        lit.shader = "defaultLit"
 74        tet = o3d.geometry.TriangleMesh.create_tetrahedron()
 75        tet.compute_vertex_normals()
 76        tet.paint_uniform_color([0.5, 0.75, 1.0])
 77        self.widget3d.scene.add_geometry("tetrahedron", tet, lit)
 78        bounds = self.widget3d.scene.bounding_box
 79        self.widget3d.setup_camera(60.0, bounds, bounds.get_center())
 80        self.widget3d.scene.show_axes(True)
 81
 82        em = self.window.theme.font_size
 83        margin = 0.5 * em
 84        self.panel = gui.Vert(0.5 * em, gui.Margins(margin))
 85        self.panel.add_child(gui.Label("Color image"))
 86        self.rgb_widget = gui.ImageWidget(self.rgb_images[0])
 87        self.panel.add_child(self.rgb_widget)
 88        self.panel.add_child(gui.Label("Depth image (normalized)"))
 89        self.depth_widget = gui.ImageWidget(self.depth_images[0])
 90        self.panel.add_child(self.depth_widget)
 91        self.window.add_child(self.panel)
 92
 93        self.is_done = False
 94        threading.Thread(target=self._update_thread).start()
 95
 96    def _on_layout(self, layout_context):
 97        contentRect = self.window.content_rect
 98        panel_width = 15 * layout_context.theme.font_size  # 15 ems wide
 99        self.widget3d.frame = gui.Rect(contentRect.x, contentRect.y,
100                                       contentRect.width - panel_width,
101                                       contentRect.height)
102        self.panel.frame = gui.Rect(self.widget3d.frame.get_right(),
103                                    contentRect.y, panel_width,
104                                    contentRect.height)
105
106    def _on_close(self):
107        self.is_done = True
108        return True  # False would cancel the close
109
110    def _update_thread(self):
111        # This is NOT the UI thread, need to call post_to_main_thread() to update
112        # the scene or any part of the UI.
113        idx = 0
114        while not self.is_done:
115            time.sleep(0.100)
116
117            # Get the next frame, for instance, reading a frame from the camera.
118            rgb_frame = self.rgb_images[idx]
119            depth_frame = self.depth_images[idx]
120            idx += 1
121            if idx >= len(self.rgb_images):
122                idx = 0
123
124            # Update the images. This must be done on the UI thread.
125            def update():
126                self.rgb_widget.update_image(rgb_frame)
127                self.depth_widget.update_image(depth_frame)
128                self.widget3d.scene.set_background([1, 1, 1, 1], rgb_frame)
129
130            if not self.is_done:
131                gui.Application.instance.post_to_main_thread(
132                    self.window, update)
133
134
135def main():
136    app = o3d.visualization.gui.Application.instance
137    app.initialize()
138
139    win = VideoWindow()
140
141    app.run()
142
143
144if __name__ == "__main__":
145    main()

vis_gui.py

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