Visualization
add_geometry.py
27import open3d as o3d
28import open3d.visualization.gui as gui
29import open3d.visualization.rendering as rendering
30import platform
31import random
32import threading
33import time
34
35isMacOS = (platform.system() == "Darwin")
36
37
38# This example shows two methods of adding geometry to an existing scene.
39# 1) add via a UI callback (in this case a menu, but a button would be similar,
40# you would call `button.set_on_clicked(self.on_menu_sphere_)` when
41# configuring the button. See `on_menu_sphere()`.
42# 2) add asynchronously by polling from another thread. GUI functions must be
43# called from the UI thread, so use Application.post_to_main_thread().
44# See `on_menu_random()`.
45# Running the example will show a simple window with a Debug menu item with the
46# two different options. The second method will add random spheres for
47# 20 seconds, during which time you can be interacting with the scene, rotating,
48# etc.
49class SpheresApp:
50 MENU_SPHERE = 1
51 MENU_RANDOM = 2
52 MENU_QUIT = 3
53
54 def __init__(self):
55 self._id = 0
56 self.window = gui.Application.instance.create_window(
57 "Add Spheres Example", 1024, 768)
58 self.scene = gui.SceneWidget()
59 self.scene.scene = rendering.Open3DScene(self.window.renderer)
60 self.scene.scene.set_background([1, 1, 1, 1])
61 self.scene.scene.scene.set_sun_light(
62 [-1, -1, -1], # direction
63 [1, 1, 1], # color
64 100000) # intensity
65 self.scene.scene.scene.enable_sun_light(True)
66 bbox = o3d.geometry.AxisAlignedBoundingBox([-10, -10, -10],
67 [10, 10, 10])
68 self.scene.setup_camera(60, bbox, [0, 0, 0])
69
70 self.window.add_child(self.scene)
71
72 # The menu is global (because the macOS menu is global), so only create
73 # it once, no matter how many windows are created
74 if gui.Application.instance.menubar is None:
75 if isMacOS:
76 app_menu = gui.Menu()
77 app_menu.add_item("Quit", SpheresApp.MENU_QUIT)
78 debug_menu = gui.Menu()
79 debug_menu.add_item("Add Sphere", SpheresApp.MENU_SPHERE)
80 debug_menu.add_item("Add Random Spheres", SpheresApp.MENU_RANDOM)
81 if not isMacOS:
82 debug_menu.add_separator()
83 debug_menu.add_item("Quit", SpheresApp.MENU_QUIT)
84
85 menu = gui.Menu()
86 if isMacOS:
87 # macOS will name the first menu item for the running application
88 # (in our case, probably "Python"), regardless of what we call
89 # it. This is the application menu, and it is where the
90 # About..., Preferences..., and Quit menu items typically go.
91 menu.add_menu("Example", app_menu)
92 menu.add_menu("Debug", debug_menu)
93 else:
94 menu.add_menu("Debug", debug_menu)
95 gui.Application.instance.menubar = menu
96
97 # The menubar is global, but we need to connect the menu items to the
98 # window, so that the window can call the appropriate function when the
99 # menu item is activated.
100 self.window.set_on_menu_item_activated(SpheresApp.MENU_SPHERE,
101 self._on_menu_sphere)
102 self.window.set_on_menu_item_activated(SpheresApp.MENU_RANDOM,
103 self._on_menu_random)
104 self.window.set_on_menu_item_activated(SpheresApp.MENU_QUIT,
105 self._on_menu_quit)
106
107 def add_sphere(self):
108 self._id += 1
109 mat = rendering.MaterialRecord()
110 mat.base_color = [
111 random.random(),
112 random.random(),
113 random.random(), 1.0
114 ]
115 mat.shader = "defaultLit"
116 sphere = o3d.geometry.TriangleMesh.create_sphere(0.5)
117 sphere.compute_vertex_normals()
118 sphere.translate([
119 10.0 * random.uniform(-1.0, 1.0), 10.0 * random.uniform(-1.0, 1.0),
120 10.0 * random.uniform(-1.0, 1.0)
121 ])
122 self.scene.scene.add_geometry("sphere" + str(self._id), sphere, mat)
123
124 def _on_menu_sphere(self):
125 # GUI callbacks happen on the main thread, so we can do everything
126 # normally here.
127 self.add_sphere()
128
129 def _on_menu_random(self):
130 # This adds spheres asynchronously. This pattern is useful if you have
131 # data coming in from another source than user interaction.
132 def thread_main():
133 for _ in range(0, 20):
134 # We can only modify GUI objects on the main thread, so we
135 # need to post the function to call to the main thread.
136 gui.Application.instance.post_to_main_thread(
137 self.window, self.add_sphere)
138 time.sleep(1)
139
140 threading.Thread(target=thread_main).start()
141
142 def _on_menu_quit(self):
143 gui.Application.instance.quit()
144
145
146def main():
147 gui.Application.instance.initialize()
148 SpheresApp()
149 gui.Application.instance.run()
150
151
152if __name__ == "__main__":
153 main()
all_widgets.py
27import open3d.visualization.gui as gui
28import os.path
29
30basedir = os.path.dirname(os.path.realpath(__file__))
31
32
33class ExampleWindow:
34 MENU_CHECKABLE = 1
35 MENU_DISABLED = 2
36 MENU_QUIT = 3
37
38 def __init__(self):
39 self.window = gui.Application.instance.create_window("Test", 400, 768)
40 # self.window = gui.Application.instance.create_window("Test", 400, 768,
41 # x=50, y=100)
42 w = self.window # for more concise code
43
44 # Rather than specifying sizes in pixels, which may vary in size based
45 # on the monitor, especially on macOS which has 220 dpi monitors, use
46 # the em-size. This way sizings will be proportional to the font size,
47 # which will create a more visually consistent size across platforms.
48 em = w.theme.font_size
49
50 # Widgets are laid out in layouts: gui.Horiz, gui.Vert,
51 # gui.CollapsableVert, and gui.VGrid. By nesting the layouts we can
52 # achieve complex designs. Usually we use a vertical layout as the
53 # topmost widget, since widgets tend to be organized from top to bottom.
54 # Within that, we usually have a series of horizontal layouts for each
55 # row.
56 layout = gui.Vert(0, gui.Margins(0.5 * em, 0.5 * em, 0.5 * em,
57 0.5 * em))
58
59 # Create the menu. The menu is global (because the macOS menu is global),
60 # so only create it once.
61 if gui.Application.instance.menubar is None:
62 menubar = gui.Menu()
63 test_menu = gui.Menu()
64 test_menu.add_item("An option", ExampleWindow.MENU_CHECKABLE)
65 test_menu.set_checked(ExampleWindow.MENU_CHECKABLE, True)
66 test_menu.add_item("Unavailable feature",
67 ExampleWindow.MENU_DISABLED)
68 test_menu.set_enabled(ExampleWindow.MENU_DISABLED, False)
69 test_menu.add_separator()
70 test_menu.add_item("Quit", ExampleWindow.MENU_QUIT)
71 # On macOS the first menu item is the application menu item and will
72 # always be the name of the application (probably "Python"),
73 # regardless of what you pass in here. The application menu is
74 # typically where About..., Preferences..., and Quit go.
75 menubar.add_menu("Test", test_menu)
76 gui.Application.instance.menubar = menubar
77
78 # Each window needs to know what to do with the menu items, so we need
79 # to tell the window how to handle menu items.
80 w.set_on_menu_item_activated(ExampleWindow.MENU_CHECKABLE,
81 self._on_menu_checkable)
82 w.set_on_menu_item_activated(ExampleWindow.MENU_QUIT,
83 self._on_menu_quit)
84
85 # Create a file-chooser widget. One part will be a text edit widget for
86 # the filename and clicking on the button will let the user choose using
87 # the file dialog.
88 self._fileedit = gui.TextEdit()
89 filedlgbutton = gui.Button("...")
90 filedlgbutton.horizontal_padding_em = 0.5
91 filedlgbutton.vertical_padding_em = 0
92 filedlgbutton.set_on_clicked(self._on_filedlg_button)
93
94 # (Create the horizontal widget for the row. This will make sure the
95 # text editor takes up as much space as it can.)
96 fileedit_layout = gui.Horiz()
97 fileedit_layout.add_child(gui.Label("Model file"))
98 fileedit_layout.add_child(self._fileedit)
99 fileedit_layout.add_fixed(0.25 * em)
100 fileedit_layout.add_child(filedlgbutton)
101 # add to the top-level (vertical) layout
102 layout.add_child(fileedit_layout)
103
104 # Create a collapsible vertical widget, which takes up enough vertical
105 # space for all its children when open, but only enough for text when
106 # closed. This is useful for property pages, so the user can hide sets
107 # of properties they rarely use. All layouts take a spacing parameter,
108 # which is the spacinging between items in the widget, and a margins
109 # parameter, which specifies the spacing of the left, top, right,
110 # bottom margins. (This acts like the 'padding' property in CSS.)
111 collapse = gui.CollapsableVert("Widgets", 0.33 * em,
112 gui.Margins(em, 0, 0, 0))
113 self._label = gui.Label("Lorem ipsum dolor")
114 self._label.text_color = gui.Color(1.0, 0.5, 0.0)
115 collapse.add_child(self._label)
116
117 # Create a checkbox. Checking or unchecking would usually be used to set
118 # a binary property, but in this case it will show a simple message box,
119 # which illustrates how to create simple dialogs.
120 cb = gui.Checkbox("Enable some really cool effect")
121 cb.set_on_checked(self._on_cb) # set the callback function
122 collapse.add_child(cb)
123
124 # Create a color editor. We will change the color of the orange label
125 # above when the color changes.
126 color = gui.ColorEdit()
127 color.color_value = self._label.text_color
128 color.set_on_value_changed(self._on_color)
129 collapse.add_child(color)
130
131 # This is a combobox, nothing fancy here, just set a simple function to
132 # handle the user selecting an item.
133 combo = gui.Combobox()
134 combo.add_item("Show point labels")
135 combo.add_item("Show point velocity")
136 combo.add_item("Show bounding boxes")
137 combo.set_on_selection_changed(self._on_combo)
138 collapse.add_child(combo)
139
140 # This is a toggle switch, which is similar to a checkbox. To my way of
141 # thinking the difference is subtle: a checkbox toggles properties
142 # (for example, purely visual changes like enabling lighting) while a
143 # toggle switch is better for changing the behavior of the app (for
144 # example, turning on processing from the camera).
145 switch = gui.ToggleSwitch("Continuously update from camera")
146 switch.set_on_clicked(self._on_switch)
147 collapse.add_child(switch)
148
149 self.logo_idx = 0
150 proxy = gui.WidgetProxy()
151
152 def switch_proxy():
153 self.logo_idx += 1
154 if self.logo_idx % 3 == 0:
155 proxy.set_widget(None)
156 elif self.logo_idx % 3 == 1:
157 # Add a simple image
158 logo = gui.ImageWidget(basedir + "/icon-32.png")
159 proxy.set_widget(logo)
160 else:
161 label = gui.Label(
162 'Open3D: A Modern Library for 3D Data Processing')
163 proxy.set_widget(label)
164 w.set_needs_layout()
165
166 logo_btn = gui.Button('Switch Logo By WidgetProxy')
167 logo_btn.vertical_padding_em = 0
168 logo_btn.background_color = gui.Color(r=0, b=0.5, g=0)
169 logo_btn.set_on_clicked(switch_proxy)
170 collapse.add_child(logo_btn)
171 collapse.add_child(proxy)
172
173 # Widget stack demo
174 self._widget_idx = 0
175 hz = gui.Horiz(spacing=5)
176 push_widget_btn = gui.Button('Push widget')
177 push_widget_btn.vertical_padding_em = 0
178 pop_widget_btn = gui.Button('Pop widget')
179 pop_widget_btn.vertical_padding_em = 0
180 stack = gui.WidgetStack()
181 stack.set_on_top(lambda w: print(f'New widget is: {w.text}'))
182 hz.add_child(gui.Label('WidgetStack '))
183 hz.add_child(push_widget_btn)
184 hz.add_child(pop_widget_btn)
185 hz.add_child(stack)
186 collapse.add_child(hz)
187
188 def push_widget():
189 self._widget_idx += 1
190 stack.push_widget(gui.Label(f'Widget {self._widget_idx}'))
191
192 push_widget_btn.set_on_clicked(push_widget)
193 pop_widget_btn.set_on_clicked(stack.pop_widget)
194
195 # Add a list of items
196 lv = gui.ListView()
197 lv.set_items(["Ground", "Trees", "Buildings", "Cars", "People", "Cats"])
198 lv.selected_index = lv.selected_index + 2 # initially is -1, so now 1
199 lv.set_max_visible_items(4)
200 lv.set_on_selection_changed(self._on_list)
201 collapse.add_child(lv)
202
203 # Add a tree view
204 tree = gui.TreeView()
205 tree.add_text_item(tree.get_root_item(), "Camera")
206 geo_id = tree.add_text_item(tree.get_root_item(), "Geometries")
207 mesh_id = tree.add_text_item(geo_id, "Mesh")
208 tree.add_text_item(mesh_id, "Triangles")
209 tree.add_text_item(mesh_id, "Albedo texture")
210 tree.add_text_item(mesh_id, "Normal map")
211 points_id = tree.add_text_item(geo_id, "Points")
212 tree.can_select_items_with_children = True
213 tree.set_on_selection_changed(self._on_tree)
214 # does not call on_selection_changed: user did not change selection
215 tree.selected_item = points_id
216 collapse.add_child(tree)
217
218 # Add two number editors, one for integers and one for floating point
219 # Number editor can clamp numbers to a range, although this is more
220 # useful for integers than for floating point.
221 intedit = gui.NumberEdit(gui.NumberEdit.INT)
222 intedit.int_value = 0
223 intedit.set_limits(1, 19) # value coerced to 1
224 intedit.int_value = intedit.int_value + 2 # value should be 3
225 doubleedit = gui.NumberEdit(gui.NumberEdit.DOUBLE)
226 numlayout = gui.Horiz()
227 numlayout.add_child(gui.Label("int"))
228 numlayout.add_child(intedit)
229 numlayout.add_fixed(em) # manual spacing (could set it in Horiz() ctor)
230 numlayout.add_child(gui.Label("double"))
231 numlayout.add_child(doubleedit)
232 collapse.add_child(numlayout)
233
234 # Create a progress bar. It ranges from 0.0 to 1.0.
235 self._progress = gui.ProgressBar()
236 self._progress.value = 0.25 # 25% complete
237 self._progress.value = self._progress.value + 0.08 # 0.25 + 0.08 = 33%
238 prog_layout = gui.Horiz(em)
239 prog_layout.add_child(gui.Label("Progress..."))
240 prog_layout.add_child(self._progress)
241 collapse.add_child(prog_layout)
242
243 # Create a slider. It acts very similar to NumberEdit except that the
244 # user moves a slider and cannot type the number.
245 slider = gui.Slider(gui.Slider.INT)
246 slider.set_limits(5, 13)
247 slider.set_on_value_changed(self._on_slider)
248 collapse.add_child(slider)
249
250 # Create a text editor. The placeholder text (if not empty) will be
251 # displayed when there is no text, as concise help, or visible tooltip.
252 tedit = gui.TextEdit()
253 tedit.placeholder_text = "Edit me some text here"
254
255 # on_text_changed fires whenever the user changes the text (but not if
256 # the text_value property is assigned to).
257 tedit.set_on_text_changed(self._on_text_changed)
258
259 # on_value_changed fires whenever the user signals that they are finished
260 # editing the text, either by pressing return or by clicking outside of
261 # the text editor, thus losing text focus.
262 tedit.set_on_value_changed(self._on_value_changed)
263 collapse.add_child(tedit)
264
265 # Create a widget for showing/editing a 3D vector
266 vedit = gui.VectorEdit()
267 vedit.vector_value = [1, 2, 3]
268 vedit.set_on_value_changed(self._on_vedit)
269 collapse.add_child(vedit)
270
271 # Create a VGrid layout. This layout specifies the number of columns
272 # (two, in this case), and will place the first child in the first
273 # column, the second in the second, the third in the first, the fourth
274 # in the second, etc.
275 # So:
276 # 2 cols 3 cols 4 cols
277 # | 1 | 2 | | 1 | 2 | 3 | | 1 | 2 | 3 | 4 |
278 # | 3 | 4 | | 4 | 5 | 6 | | 5 | 6 | 7 | 8 |
279 # | 5 | 6 | | 7 | 8 | 9 | | 9 | 10 | 11 | 12 |
280 # | ... | | ... | | ... |
281 vgrid = gui.VGrid(2)
282 vgrid.add_child(gui.Label("Trees"))
283 vgrid.add_child(gui.Label("12 items"))
284 vgrid.add_child(gui.Label("People"))
285 vgrid.add_child(gui.Label("2 (93% certainty)"))
286 vgrid.add_child(gui.Label("Cars"))
287 vgrid.add_child(gui.Label("5 (87% certainty)"))
288 collapse.add_child(vgrid)
289
290 # Create a tab control. This is really a set of N layouts on top of each
291 # other, but with only one selected.
292 tabs = gui.TabControl()
293 tab1 = gui.Vert()
294 tab1.add_child(gui.Checkbox("Enable option 1"))
295 tab1.add_child(gui.Checkbox("Enable option 2"))
296 tab1.add_child(gui.Checkbox("Enable option 3"))
297 tabs.add_tab("Options", tab1)
298 tab2 = gui.Vert()
299 tab2.add_child(gui.Label("No plugins detected"))
300 tab2.add_stretch()
301 tabs.add_tab("Plugins", tab2)
302 tab3 = gui.RadioButton(gui.RadioButton.VERT)
303 tab3.set_items(["Apple", "Orange"])
304
305 def vt_changed(idx):
306 print(f"current cargo: {tab3.selected_value}")
307
308 tab3.set_on_selection_changed(vt_changed)
309 tabs.add_tab("Cargo", tab3)
310 tab4 = gui.RadioButton(gui.RadioButton.HORIZ)
311 tab4.set_items(["Air plane", "Train", "Bus"])
312
313 def hz_changed(idx):
314 print(f"current traffic plan: {tab4.selected_value}")
315
316 tab4.set_on_selection_changed(hz_changed)
317 tabs.add_tab("Traffic", tab4)
318 collapse.add_child(tabs)
319
320 # Quit button. (Typically this is a menu item)
321 button_layout = gui.Horiz()
322 ok_button = gui.Button("Ok")
323 ok_button.set_on_clicked(self._on_ok)
324 button_layout.add_stretch()
325 button_layout.add_child(ok_button)
326
327 layout.add_child(collapse)
328 layout.add_child(button_layout)
329
330 # We're done, set the window's layout
331 w.add_child(layout)
332
333 def _on_filedlg_button(self):
334 filedlg = gui.FileDialog(gui.FileDialog.OPEN, "Select file",
335 self.window.theme)
336 filedlg.add_filter(".obj .ply .stl", "Triangle mesh (.obj, .ply, .stl)")
337 filedlg.add_filter("", "All files")
338 filedlg.set_on_cancel(self._on_filedlg_cancel)
339 filedlg.set_on_done(self._on_filedlg_done)
340 self.window.show_dialog(filedlg)
341
342 def _on_filedlg_cancel(self):
343 self.window.close_dialog()
344
345 def _on_filedlg_done(self, path):
346 self._fileedit.text_value = path
347 self.window.close_dialog()
348
349 def _on_cb(self, is_checked):
350 if is_checked:
351 text = "Sorry, effects are unimplemented"
352 else:
353 text = "Good choice"
354
355 self.show_message_dialog("There might be a problem...", text)
356
357 def _on_switch(self, is_on):
358 if is_on:
359 print("Camera would now be running")
360 else:
361 print("Camera would now be off")
362
363 # This function is essentially the same as window.show_message_box(),
364 # so for something this simple just use that, but it illustrates making a
365 # dialog.
366 def show_message_dialog(self, title, message):
367 # A Dialog is just a widget, so you make its child a layout just like
368 # a Window.
369 dlg = gui.Dialog(title)
370
371 # Add the message text
372 em = self.window.theme.font_size
373 dlg_layout = gui.Vert(em, gui.Margins(em, em, em, em))
374 dlg_layout.add_child(gui.Label(message))
375
376 # Add the Ok button. We need to define a callback function to handle
377 # the click.
378 ok_button = gui.Button("Ok")
379 ok_button.set_on_clicked(self._on_dialog_ok)
380
381 # We want the Ok button to be an the right side, so we need to add
382 # a stretch item to the layout, otherwise the button will be the size
383 # of the entire row. A stretch item takes up as much space as it can,
384 # which forces the button to be its minimum size.
385 button_layout = gui.Horiz()
386 button_layout.add_stretch()
387 button_layout.add_child(ok_button)
388
389 # Add the button layout,
390 dlg_layout.add_child(button_layout)
391 # ... then add the layout as the child of the Dialog
392 dlg.add_child(dlg_layout)
393 # ... and now we can show the dialog
394 self.window.show_dialog(dlg)
395
396 def _on_dialog_ok(self):
397 self.window.close_dialog()
398
399 def _on_color(self, new_color):
400 self._label.text_color = new_color
401
402 def _on_combo(self, new_val, new_idx):
403 print(new_idx, new_val)
404
405 def _on_list(self, new_val, is_dbl_click):
406 print(new_val)
407
408 def _on_tree(self, new_item_id):
409 print(new_item_id)
410
411 def _on_slider(self, new_val):
412 self._progress.value = new_val / 20.0
413
414 def _on_text_changed(self, new_text):
415 print("edit:", new_text)
416
417 def _on_value_changed(self, new_text):
418 print("value:", new_text)
419
420 def _on_vedit(self, new_val):
421 print(new_val)
422
423 def _on_ok(self):
424 gui.Application.instance.quit()
425
426 def _on_menu_checkable(self):
427 gui.Application.instance.menubar.set_checked(
428 ExampleWindow.MENU_CHECKABLE,
429 not gui.Application.instance.menubar.is_checked(
430 ExampleWindow.MENU_CHECKABLE))
431
432 def _on_menu_quit(self):
433 gui.Application.instance.quit()
434
435
436# This class is essentially the same as window.show_message_box(),
437# so for something this simple just use that, but it illustrates making a
438# dialog.
439class MessageBox:
440
441 def __init__(self, title, message):
442 self._window = None
443
444 # A Dialog is just a widget, so you make its child a layout just like
445 # a Window.
446 dlg = gui.Dialog(title)
447
448 # Add the message text
449 em = self.window.theme.font_size
450 dlg_layout = gui.Vert(em, gui.Margins(em, em, em, em))
451 dlg_layout.add_child(gui.Label(message))
452
453 # Add the Ok button. We need to define a callback function to handle
454 # the click.
455 ok_button = gui.Button("Ok")
456 ok_button.set_on_clicked(self._on_ok)
457
458 # We want the Ok button to be an the right side, so we need to add
459 # a stretch item to the layout, otherwise the button will be the size
460 # of the entire row. A stretch item takes up as much space as it can,
461 # which forces the button to be its minimum size.
462 button_layout = gui.Horiz()
463 button_layout.add_stretch()
464 button_layout.add_child(ok_button)
465
466 # Add the button layout,
467 dlg_layout.add_child(button_layout)
468 # ... then add the layout as the child of the Dialog
469 dlg.add_child(dlg_layout)
470
471 def show(self, window):
472 self._window = window
473
474 def _on_ok(self):
475 self._window.close_dialog()
476
477
478def main():
479 # We need to initialize the application, which finds the necessary shaders for
480 # rendering and prepares the cross-platform window abstraction.
481 gui.Application.instance.initialize()
482
483 w = ExampleWindow()
484
485 # Run the event loop. This will not return until the last window is closed.
486 gui.Application.instance.run()
487
488
489if __name__ == "__main__":
490 main()
customized_visualization.py
27import os
28import open3d as o3d
29import numpy as np
30import matplotlib.pyplot as plt
31
32pyexample_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
33test_data_path = os.path.join(os.path.dirname(pyexample_path), 'test_data')
34
35
36def custom_draw_geometry(pcd):
37 # The following code achieves the same effect as:
38 # o3d.visualization.draw_geometries([pcd])
39 vis = o3d.visualization.Visualizer()
40 vis.create_window()
41 vis.add_geometry(pcd)
42 vis.run()
43 vis.destroy_window()
44
45
46def custom_draw_geometry_with_custom_fov(pcd, fov_step):
47 vis = o3d.visualization.Visualizer()
48 vis.create_window()
49 vis.add_geometry(pcd)
50 ctr = vis.get_view_control()
51 print("Field of view (before changing) %.2f" % ctr.get_field_of_view())
52 ctr.change_field_of_view(step=fov_step)
53 print("Field of view (after changing) %.2f" % ctr.get_field_of_view())
54 vis.run()
55 vis.destroy_window()
56
57
58def custom_draw_geometry_with_rotation(pcd):
59
60 def rotate_view(vis):
61 ctr = vis.get_view_control()
62 ctr.rotate(10.0, 0.0)
63 return False
64
65 o3d.visualization.draw_geometries_with_animation_callback([pcd],
66 rotate_view)
67
68
69def custom_draw_geometry_load_option(pcd, render_option_path):
70 vis = o3d.visualization.Visualizer()
71 vis.create_window()
72 vis.add_geometry(pcd)
73 vis.get_render_option().load_from_json(render_option_path)
74 vis.run()
75 vis.destroy_window()
76
77
78def custom_draw_geometry_with_key_callback(pcd, render_option_path):
79
80 def change_background_to_black(vis):
81 opt = vis.get_render_option()
82 opt.background_color = np.asarray([0, 0, 0])
83 return False
84
85 def load_render_option(vis):
86 vis.get_render_option().load_from_json(render_option_path)
87 return False
88
89 def capture_depth(vis):
90 depth = vis.capture_depth_float_buffer()
91 plt.imshow(np.asarray(depth))
92 plt.show()
93 return False
94
95 def capture_image(vis):
96 image = vis.capture_screen_float_buffer()
97 plt.imshow(np.asarray(image))
98 plt.show()
99 return False
100
101 key_to_callback = {}
102 key_to_callback[ord("K")] = change_background_to_black
103 key_to_callback[ord("R")] = load_render_option
104 key_to_callback[ord(",")] = capture_depth
105 key_to_callback[ord(".")] = capture_image
106 o3d.visualization.draw_geometries_with_key_callbacks([pcd], key_to_callback)
107
108
109def custom_draw_geometry_with_camera_trajectory(pcd, render_option_path,
110 camera_trajectory_path):
111 custom_draw_geometry_with_camera_trajectory.index = -1
112 custom_draw_geometry_with_camera_trajectory.trajectory =\
113 o3d.io.read_pinhole_camera_trajectory(camera_trajectory_path)
114 custom_draw_geometry_with_camera_trajectory.vis = o3d.visualization.Visualizer(
115 )
116 image_path = os.path.join(test_data_path, 'image')
117 if not os.path.exists(image_path):
118 os.makedirs(image_path)
119 depth_path = os.path.join(test_data_path, 'depth')
120 if not os.path.exists(depth_path):
121 os.makedirs(depth_path)
122
123 def move_forward(vis):
124 # This function is called within the o3d.visualization.Visualizer::run() loop
125 # The run loop calls the function, then re-render
126 # So the sequence in this function is to:
127 # 1. Capture frame
128 # 2. index++, check ending criteria
129 # 3. Set camera
130 # 4. (Re-render)
131 ctr = vis.get_view_control()
132 glb = custom_draw_geometry_with_camera_trajectory
133 if glb.index >= 0:
134 print("Capture image {:05d}".format(glb.index))
135 depth = vis.capture_depth_float_buffer(False)
136 image = vis.capture_screen_float_buffer(False)
137 plt.imsave(os.path.join(depth_path, '{:05d}.png'.format(glb.index)),
138 np.asarray(depth),
139 dpi=1)
140 plt.imsave(os.path.join(image_path, '{:05d}.png'.format(glb.index)),
141 np.asarray(image),
142 dpi=1)
143 # vis.capture_depth_image("depth/{:05d}.png".format(glb.index), False)
144 # vis.capture_screen_image("image/{:05d}.png".format(glb.index), False)
145 glb.index = glb.index + 1
146 if glb.index < len(glb.trajectory.parameters):
147 ctr.convert_from_pinhole_camera_parameters(
148 glb.trajectory.parameters[glb.index], allow_arbitrary=True)
149 else:
150 custom_draw_geometry_with_camera_trajectory.vis.\
151 register_animation_callback(None)
152 return False
153
154 vis = custom_draw_geometry_with_camera_trajectory.vis
155 vis.create_window()
156 vis.add_geometry(pcd)
157 vis.get_render_option().load_from_json(render_option_path)
158 vis.register_animation_callback(move_forward)
159 vis.run()
160 vis.destroy_window()
161
162
163if __name__ == "__main__":
164 sample_data = o3d.data.DemoCustomVisualization()
165 pcd_flipped = o3d.io.read_point_cloud(sample_data.point_cloud_path)
166 # Flip it, otherwise the pointcloud will be upside down
167 pcd_flipped.transform([[1, 0, 0, 0], [0, -1, 0, 0], [0, 0, -1, 0],
168 [0, 0, 0, 1]])
169
170 print("1. Customized visualization to mimic DrawGeometry")
171 custom_draw_geometry(pcd_flipped)
172
173 print("2. Changing field of view")
174 custom_draw_geometry_with_custom_fov(pcd_flipped, 90.0)
175 custom_draw_geometry_with_custom_fov(pcd_flipped, -90.0)
176
177 print("3. Customized visualization with a rotating view")
178 custom_draw_geometry_with_rotation(pcd_flipped)
179
180 print("4. Customized visualization showing normal rendering")
181 custom_draw_geometry_load_option(pcd_flipped,
182 sample_data.render_option_path)
183
184 print("5. Customized visualization with key press callbacks")
185 print(" Press 'K' to change background color to black")
186 print(" Press 'R' to load a customized render option, showing normals")
187 print(" Press ',' to capture the depth buffer and show it")
188 print(" Press '.' to capture the screen and show it")
189 custom_draw_geometry_with_key_callback(pcd_flipped,
190 sample_data.render_option_path)
191
192 pcd = o3d.io.read_point_cloud(sample_data.point_cloud_path)
193 print("6. Customized visualization playing a camera trajectory")
194 custom_draw_geometry_with_camera_trajectory(
195 pcd, sample_data.render_option_path, sample_data.camera_trajectory_path)
customized_visualization_key_action.py
27import open3d as o3d
28
29
30def custom_key_action_without_kb_repeat_delay(pcd):
31 rotating = False
32
33 vis = o3d.visualization.VisualizerWithKeyCallback()
34
35 def key_action_callback(vis, action, mods):
36 nonlocal rotating
37 print(action)
38 if action == 1: # key down
39 rotating = True
40 elif action == 0: # key up
41 rotating = False
42 elif action == 2: # key repeat
43 pass
44 return True
45
46 def animation_callback(vis):
47 nonlocal rotating
48 if rotating:
49 ctr = vis.get_view_control()
50 ctr.rotate(10.0, 0.0)
51
52 # key_action_callback will be triggered when there's a keyboard press, release or repeat event
53 vis.register_key_action_callback(32, key_action_callback) # space
54
55 # animation_callback is always repeatedly called by the visualizer
56 vis.register_animation_callback(animation_callback)
57
58 vis.create_window()
59 vis.add_geometry(pcd)
60 vis.run()
61
62
63if __name__ == "__main__":
64 ply_data = o3d.data.PLYPointCloud()
65 pcd = o3d.io.read_point_cloud(ply_data.path)
66
67 print(
68 "Customized visualization with smooth key action (without keyboard repeat delay)"
69 )
70 custom_key_action_without_kb_repeat_delay(pcd)
demo_scene.py
27
28import math
29import numpy as np
30import os
31import open3d as o3d
32import open3d.visualization as vis
33
34
35def convert_material_record(mat_record):
36 mat = vis.Material('defaultLit')
37 # Convert scalar parameters
38 mat.vector_properties['base_color'] = mat_record.base_color
39 mat.scalar_properties['metallic'] = mat_record.base_metallic
40 mat.scalar_properties['roughness'] = mat_record.base_roughness
41 mat.scalar_properties['reflectance'] = mat_record.base_reflectance
42 mat.texture_maps['albedo'] = o3d.t.geometry.Image.from_legacy(
43 mat_record.albedo_img)
44 mat.texture_maps['normal'] = o3d.t.geometry.Image.from_legacy(
45 mat_record.normal_img)
46 mat.texture_maps['ao_rough_metal'] = o3d.t.geometry.Image.from_legacy(
47 mat_record.ao_rough_metal_img)
48 return mat
49
50
51def create_scene():
52 '''
53 Creates the geometry and materials for the demo scene and returns a dictionary suitable for draw call
54 '''
55 # Create some shapes for our scene
56 a_cube = o3d.geometry.TriangleMesh.create_box(2,
57 4,
58 4,
59 create_uv_map=True,
60 map_texture_to_each_face=True)
61 a_cube.compute_triangle_normals()
62 a_cube.translate((-5, 0, -2))
63 a_cube = o3d.t.geometry.TriangleMesh.from_legacy(a_cube)
64
65 a_sphere = o3d.geometry.TriangleMesh.create_sphere(2.5,
66 resolution=40,
67 create_uv_map=True)
68 a_sphere.compute_vertex_normals()
69 rotate_90 = o3d.geometry.get_rotation_matrix_from_xyz((-math.pi / 2, 0, 0))
70 a_sphere.rotate(rotate_90)
71 a_sphere.translate((5, 2.4, 0))
72 a_sphere = o3d.t.geometry.TriangleMesh.from_legacy(a_sphere)
73
74 a_cylinder = o3d.geometry.TriangleMesh.create_cylinder(
75 1.0, 4.0, 30, 4, True)
76 a_cylinder.compute_triangle_normals()
77 a_cylinder.rotate(rotate_90)
78 a_cylinder.translate((10, 2, 0))
79 a_cylinder = o3d.t.geometry.TriangleMesh.from_legacy(a_cylinder)
80
81 a_ico = o3d.geometry.TriangleMesh.create_icosahedron(1.25,
82 create_uv_map=True)
83 a_ico.compute_triangle_normals()
84 a_ico.translate((-10, 2, 0))
85 a_ico = o3d.t.geometry.TriangleMesh.from_legacy(a_ico)
86
87 # Load an OBJ model for our scene
88 helmet_data = o3d.data.FlightHelmetModel()
89 helmet = o3d.io.read_triangle_model(helmet_data.path)
90 helmet_parts = []
91 for m in helmet.meshes:
92 # m.mesh.paint_uniform_color((1.0, 0.75, 0.3))
93 m.mesh.scale(10.0, (0.0, 0.0, 0.0))
94 helmet_parts.append(m)
95
96 # Create a ground plane
97 ground_plane = o3d.geometry.TriangleMesh.create_box(
98 50.0, 0.1, 50.0, create_uv_map=True, map_texture_to_each_face=True)
99 ground_plane.compute_triangle_normals()
100 rotate_180 = o3d.geometry.get_rotation_matrix_from_xyz((-math.pi, 0, 0))
101 ground_plane.rotate(rotate_180)
102 ground_plane.translate((-25.0, -0.1, -25.0))
103 ground_plane.paint_uniform_color((1, 1, 1))
104 ground_plane = o3d.t.geometry.TriangleMesh.from_legacy(ground_plane)
105
106 # Material to make ground plane more interesting - a rough piece of glass
107 ground_plane.material = vis.Material("defaultLitSSR")
108 ground_plane.material.scalar_properties['roughness'] = 0.15
109 ground_plane.material.scalar_properties['reflectance'] = 0.72
110 ground_plane.material.scalar_properties['transmission'] = 0.6
111 ground_plane.material.scalar_properties['thickness'] = 0.3
112 ground_plane.material.scalar_properties['absorption_distance'] = 0.1
113 ground_plane.material.vector_properties['absorption_color'] = np.array(
114 [0.82, 0.98, 0.972, 1.0])
115 painted_plaster_texture_data = o3d.data.PaintedPlasterTexture()
116 ground_plane.material.texture_maps['albedo'] = o3d.t.io.read_image(
117 painted_plaster_texture_data.albedo_texture_path)
118 ground_plane.material.texture_maps['normal'] = o3d.t.io.read_image(
119 painted_plaster_texture_data.normal_texture_path)
120 ground_plane.material.texture_maps['roughness'] = o3d.t.io.read_image(
121 painted_plaster_texture_data.roughness_texture_path)
122
123 # Load textures and create materials for each of our demo items
124 wood_floor_texture_data = o3d.data.WoodFloorTexture()
125 a_cube.material = vis.Material('defaultLit')
126 a_cube.material.texture_maps['albedo'] = o3d.t.io.read_image(
127 wood_floor_texture_data.albedo_texture_path)
128 a_cube.material.texture_maps['normal'] = o3d.t.io.read_image(
129 wood_floor_texture_data.normal_texture_path)
130 a_cube.material.texture_maps['roughness'] = o3d.t.io.read_image(
131 wood_floor_texture_data.roughness_texture_path)
132
133 tiles_texture_data = o3d.data.TilesTexture()
134 a_sphere.material = vis.Material('defaultLit')
135 a_sphere.material.texture_maps['albedo'] = o3d.t.io.read_image(
136 tiles_texture_data.albedo_texture_path)
137 a_sphere.material.texture_maps['normal'] = o3d.t.io.read_image(
138 tiles_texture_data.normal_texture_path)
139 a_sphere.material.texture_maps['roughness'] = o3d.t.io.read_image(
140 tiles_texture_data.roughness_texture_path)
141
142 terrazzo_texture_data = o3d.data.TerrazzoTexture()
143 a_ico.material = vis.Material('defaultLit')
144 a_ico.material.texture_maps['albedo'] = o3d.t.io.read_image(
145 terrazzo_texture_data.albedo_texture_path)
146 a_ico.material.texture_maps['normal'] = o3d.t.io.read_image(
147 terrazzo_texture_data.normal_texture_path)
148 a_ico.material.texture_maps['roughness'] = o3d.t.io.read_image(
149 terrazzo_texture_data.roughness_texture_path)
150
151 metal_texture_data = o3d.data.MetalTexture()
152 a_cylinder.material = vis.Material('defaultLit')
153 a_cylinder.material.texture_maps['albedo'] = o3d.t.io.read_image(
154 metal_texture_data.albedo_texture_path)
155 a_cylinder.material.texture_maps['normal'] = o3d.t.io.read_image(
156 metal_texture_data.normal_texture_path)
157 a_cylinder.material.texture_maps['roughness'] = o3d.t.io.read_image(
158 metal_texture_data.roughness_texture_path)
159 a_cylinder.material.texture_maps['metallic'] = o3d.t.io.read_image(
160 metal_texture_data.metallic_texture_path)
161
162 geoms = [{
163 "name": "plane",
164 "geometry": ground_plane
165 }, {
166 "name": "cube",
167 "geometry": a_cube
168 }, {
169 "name": "cylinder",
170 "geometry": a_cylinder
171 }, {
172 "name": "ico",
173 "geometry": a_ico
174 }, {
175 "name": "sphere",
176 "geometry": a_sphere
177 }]
178 # Load the helmet
179 for part in helmet_parts:
180 name = part.mesh_name
181 tgeom = o3d.t.geometry.TriangleMesh.from_legacy(part.mesh)
182 tgeom.material = convert_material_record(
183 helmet.materials[part.material_idx])
184 geoms.append({"name": name, "geometry": tgeom})
185 return geoms
186
187
188if __name__ == "__main__":
189 geoms = create_scene()
190 vis.draw(geoms,
191 bg_color=(0.8, 0.9, 0.9, 1.0),
192 show_ui=True,
193 width=1920,
194 height=1080)
draw.py
27import math
28import numpy as np
29import open3d as o3d
30import open3d.visualization as vis
31import os
32import random
33
34pyexample_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
35test_data_path = os.path.join(os.path.dirname(pyexample_path), 'test_data')
36
37
38def normalize(v):
39 a = 1.0 / math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2])
40 return (a * v[0], a * v[1], a * v[2])
41
42
43def make_point_cloud(npts, center, radius, colorize):
44 pts = np.random.uniform(-radius, radius, size=[npts, 3]) + center
45 cloud = o3d.geometry.PointCloud()
46 cloud.points = o3d.utility.Vector3dVector(pts)
47 if colorize:
48 colors = np.random.uniform(0.0, 1.0, size=[npts, 3])
49 cloud.colors = o3d.utility.Vector3dVector(colors)
50 return cloud
51
52
53def single_object():
54 # No colors, no normals, should appear unlit black
55 cube = o3d.geometry.TriangleMesh.create_box(1, 2, 4)
56 vis.draw(cube)
57
58
59def multi_objects():
60 pc_rad = 1.0
61 pc_nocolor = make_point_cloud(100, (0, -2, 0), pc_rad, False)
62 pc_color = make_point_cloud(100, (3, -2, 0), pc_rad, True)
63 r = 0.4
64 sphere_unlit = o3d.geometry.TriangleMesh.create_sphere(r)
65 sphere_unlit.translate((0, 1, 0))
66 sphere_colored_unlit = o3d.geometry.TriangleMesh.create_sphere(r)
67 sphere_colored_unlit.paint_uniform_color((1.0, 0.0, 0.0))
68 sphere_colored_unlit.translate((2, 1, 0))
69 sphere_lit = o3d.geometry.TriangleMesh.create_sphere(r)
70 sphere_lit.compute_vertex_normals()
71 sphere_lit.translate((4, 1, 0))
72 sphere_colored_lit = o3d.geometry.TriangleMesh.create_sphere(r)
73 sphere_colored_lit.compute_vertex_normals()
74 sphere_colored_lit.paint_uniform_color((0.0, 1.0, 0.0))
75 sphere_colored_lit.translate((6, 1, 0))
76 big_bbox = o3d.geometry.AxisAlignedBoundingBox((-pc_rad, -3, -pc_rad),
77 (6.0 + r, 1.0 + r, pc_rad))
78 big_bbox.color = (0.0, 0.0, 0.0)
79 sphere_bbox = sphere_unlit.get_axis_aligned_bounding_box()
80 sphere_bbox.color = (1.0, 0.5, 0.0)
81 lines = o3d.geometry.LineSet.create_from_axis_aligned_bounding_box(
82 sphere_lit.get_axis_aligned_bounding_box())
83 lines.paint_uniform_color((0.0, 1.0, 0.0))
84 lines_colored = o3d.geometry.LineSet.create_from_axis_aligned_bounding_box(
85 sphere_colored_lit.get_axis_aligned_bounding_box())
86 lines_colored.paint_uniform_color((0.0, 0.0, 1.0))
87
88 vis.draw([
89 pc_nocolor, pc_color, sphere_unlit, sphere_colored_unlit, sphere_lit,
90 sphere_colored_lit, big_bbox, sphere_bbox, lines, lines_colored
91 ])
92
93
94def actions():
95 SOURCE_NAME = "Source"
96 RESULT_NAME = "Result (Poisson reconstruction)"
97 TRUTH_NAME = "Ground truth"
98
99 bunny = o3d.data.BunnyMesh()
100 bunny_mesh = o3d.io.read_triangle_mesh(bunny.path)
101 bunny_mesh.compute_vertex_normals()
102
103 bunny_mesh.paint_uniform_color((1, 0.75, 0))
104 bunny_mesh.compute_vertex_normals()
105 cloud = o3d.geometry.PointCloud()
106 cloud.points = bunny_mesh.vertices
107 cloud.normals = bunny_mesh.vertex_normals
108
109 def make_mesh(o3dvis):
110 # TODO: call o3dvis.get_geometry instead of using bunny_mesh
111 mesh, _ = o3d.geometry.TriangleMesh.create_from_point_cloud_poisson(
112 cloud)
113 mesh.paint_uniform_color((1, 1, 1))
114 mesh.compute_vertex_normals()
115 o3dvis.add_geometry({"name": RESULT_NAME, "geometry": mesh})
116 o3dvis.show_geometry(SOURCE_NAME, False)
117
118 def toggle_result(o3dvis):
119 truth_vis = o3dvis.get_geometry(TRUTH_NAME).is_visible
120 o3dvis.show_geometry(TRUTH_NAME, not truth_vis)
121 o3dvis.show_geometry(RESULT_NAME, truth_vis)
122
123 vis.draw([{
124 "name": SOURCE_NAME,
125 "geometry": cloud
126 }, {
127 "name": TRUTH_NAME,
128 "geometry": bunny_mesh,
129 "is_visible": False
130 }],
131 actions=[("Create Mesh", make_mesh),
132 ("Toggle truth/result", toggle_result)])
133
134
135def get_icp_transform(source, target, source_indices, target_indices):
136 corr = np.zeros((len(source_indices), 2))
137 corr[:, 0] = source_indices
138 corr[:, 1] = target_indices
139
140 # Estimate rough transformation using correspondences
141 p2p = o3d.pipelines.registration.TransformationEstimationPointToPoint()
142 trans_init = p2p.compute_transformation(source, target,
143 o3d.utility.Vector2iVector(corr))
144
145 # Point-to-point ICP for refinement
146 threshold = 0.03 # 3cm distance threshold
147 reg_p2p = o3d.pipelines.registration.registration_icp(
148 source, target, threshold, trans_init,
149 o3d.pipelines.registration.TransformationEstimationPointToPoint())
150
151 return reg_p2p.transformation
152
153
154def selections():
155 pcd_fragments_data = o3d.data.DemoICPPointClouds()
156 source = o3d.io.read_point_cloud(pcd_fragments_data.paths[0])
157 target = o3d.io.read_point_cloud(pcd_fragments_data.paths[1])
158 source.paint_uniform_color([1, 0.706, 0])
159 target.paint_uniform_color([0, 0.651, 0.929])
160
161 source_name = "Source (yellow)"
162 target_name = "Target (blue)"
163
164 def do_icp_one_set(o3dvis):
165 # sets: [name: [{ "index": int, "order": int, "point": (x, y, z)}, ...],
166 # ...]
167 sets = o3dvis.get_selection_sets()
168 source_picked = sorted(list(sets[0][source_name]),
169 key=lambda x: x.order)
170 target_picked = sorted(list(sets[0][target_name]),
171 key=lambda x: x.order)
172 source_indices = [idx.index for idx in source_picked]
173 target_indices = [idx.index for idx in target_picked]
174
175 t = get_icp_transform(source, target, source_indices, target_indices)
176 source.transform(t)
177
178 # Update the source geometry
179 o3dvis.remove_geometry(source_name)
180 o3dvis.add_geometry({"name": source_name, "geometry": source})
181
182 def do_icp_two_sets(o3dvis):
183 sets = o3dvis.get_selection_sets()
184 source_set = sets[0][source_name]
185 target_set = sets[1][target_name]
186 source_picked = sorted(list(source_set), key=lambda x: x.order)
187 target_picked = sorted(list(target_set), key=lambda x: x.order)
188 source_indices = [idx.index for idx in source_picked]
189 target_indices = [idx.index for idx in target_picked]
190
191 t = get_icp_transform(source, target, source_indices, target_indices)
192 source.transform(t)
193
194 # Update the source geometry
195 o3dvis.remove_geometry(source_name)
196 o3dvis.add_geometry({"name": source_name, "geometry": source})
197
198 vis.draw([{
199 "name": source_name,
200 "geometry": source
201 }, {
202 "name": target_name,
203 "geometry": target
204 }],
205 actions=[("ICP Registration (one set)", do_icp_one_set),
206 ("ICP Registration (two sets)", do_icp_two_sets)],
207 show_ui=True)
208
209
210def time_animation():
211 orig = make_point_cloud(200, (0, 0, 0), 1.0, True)
212 clouds = [{"name": "t=0", "geometry": orig, "time": 0}]
213 drift_dir = (1.0, 0.0, 0.0)
214 expand = 1.0
215 n = 20
216 for i in range(1, n):
217 amount = float(i) / float(n - 1)
218 cloud = o3d.geometry.PointCloud()
219 pts = np.asarray(orig.points)
220 pts = pts * (1.0 + amount * expand) + [amount * v for v in drift_dir]
221 cloud.points = o3d.utility.Vector3dVector(pts)
222 cloud.colors = orig.colors
223 clouds.append({
224 "name": "points at t=" + str(i),
225 "geometry": cloud,
226 "time": i
227 })
228
229 vis.draw(clouds)
230
231
232def groups():
233 building_mat = vis.rendering.MaterialRecord()
234 building_mat.shader = "defaultLit"
235 building_mat.base_color = (1.0, .90, .75, 1.0)
236 building_mat.base_reflectance = 0.1
237 midrise_mat = vis.rendering.MaterialRecord()
238 midrise_mat.shader = "defaultLit"
239 midrise_mat.base_color = (.475, .450, .425, 1.0)
240 midrise_mat.base_reflectance = 0.1
241 skyscraper_mat = vis.rendering.MaterialRecord()
242 skyscraper_mat.shader = "defaultLit"
243 skyscraper_mat.base_color = (.05, .20, .55, 1.0)
244 skyscraper_mat.base_reflectance = 0.9
245 skyscraper_mat.base_roughness = 0.01
246
247 buildings = []
248 size = 10.0
249 half = size / 2.0
250 min_height = 1.0
251 max_height = 20.0
252 for z in range(0, 10):
253 for x in range(0, 10):
254 max_h = max_height * (1.0 - abs(half - x) / half) * (
255 1.0 - abs(half - z) / half)
256 h = random.uniform(min_height, max(max_h, min_height + 1.0))
257 box = o3d.geometry.TriangleMesh.create_box(0.9, h, 0.9)
258 box.compute_triangle_normals()
259 box.translate((x + 0.05, 0.0, z + 0.05))
260 if h > 0.333 * max_height:
261 mat = skyscraper_mat
262 elif h > 0.1 * max_height:
263 mat = midrise_mat
264 else:
265 mat = building_mat
266 buildings.append({
267 "name": "building_" + str(x) + "_" + str(z),
268 "geometry": box,
269 "material": mat,
270 "group": "buildings"
271 })
272
273 haze = make_point_cloud(5000, (half, 0.333 * max_height, half),
274 1.414 * half, False)
275 haze.paint_uniform_color((0.8, 0.8, 0.8))
276
277 smog = make_point_cloud(10000, (half, 0.25 * max_height, half), 1.2 * half,
278 False)
279 smog.paint_uniform_color((0.95, 0.85, 0.75))
280
281 vis.draw(buildings + [{
282 "name": "haze",
283 "geometry": haze,
284 "group": "haze"
285 }, {
286 "name": "smog",
287 "geometry": smog,
288 "group": "smog"
289 }])
290
291
292def remove():
293
294 def make_sphere(name, center, color, group, time):
295 sphere = o3d.geometry.TriangleMesh.create_sphere(0.5)
296 sphere.compute_vertex_normals()
297 sphere.translate(center)
298
299 mat = vis.rendering.Material()
300 mat.shader = "defaultLit"
301 mat.base_color = color
302
303 return {
304 "name": name,
305 "geometry": sphere,
306 "material": mat,
307 "group": group,
308 "time": time
309 }
310
311 red = make_sphere("red", (0, 0, 0), (1.0, 0.0, 0.0, 1.0), "spheres", 0)
312 green = make_sphere("green", (2, 0, 0), (0.0, 1.0, 0.0, 1.0), "spheres", 0)
313 blue = make_sphere("blue", (4, 0, 0), (0.0, 0.0, 1.0, 1.0), "spheres", 0)
314 yellow = make_sphere("yellow", (0, 0, 0), (1.0, 1.0, 0.0, 1.0), "spheres",
315 1)
316 bbox = {
317 "name": "bbox",
318 "geometry": red["geometry"].get_axis_aligned_bounding_box()
319 }
320
321 def remove_green(visdraw):
322 visdraw.remove_geometry("green")
323
324 def remove_yellow(visdraw):
325 visdraw.remove_geometry("yellow")
326
327 def remove_bbox(visdraw):
328 visdraw.remove_geometry("bbox")
329
330 vis.draw([red, green, blue, yellow, bbox],
331 actions=[("Remove Green", remove_green),
332 ("Remove Yellow", remove_yellow),
333 ("Remove Bounds", remove_bbox)])
334
335
336def main():
337 single_object()
338 multi_objects()
339 actions()
340 selections()
341
342
343if __name__ == "__main__":
344 main()
draw_webrtc.py
27import open3d as o3d
28
29if __name__ == "__main__":
30 o3d.visualization.webrtc_server.enable_webrtc()
31 cube_red = o3d.geometry.TriangleMesh.create_box(1, 2, 4)
32 cube_red.compute_vertex_normals()
33 cube_red.paint_uniform_color((1.0, 0.0, 0.0))
34 o3d.visualization.draw(cube_red)
headless_rendering.py
27import os
28import open3d as o3d
29import numpy as np
30import matplotlib.pyplot as plt
31
32
33def custom_draw_geometry_with_camera_trajectory(pcd, camera_trajectory_path,
34 render_option_path,
35 output_path):
36 custom_draw_geometry_with_camera_trajectory.index = -1
37 custom_draw_geometry_with_camera_trajectory.trajectory =\
38 o3d.io.read_pinhole_camera_trajectory(camera_trajectory_path)
39 custom_draw_geometry_with_camera_trajectory.vis = o3d.visualization.Visualizer(
40 )
41 image_path = os.path.join(output_path, 'image')
42 if not os.path.exists(image_path):
43 os.makedirs(image_path)
44 depth_path = os.path.join(output_path, 'depth')
45 if not os.path.exists(depth_path):
46 os.makedirs(depth_path)
47
48 print("Saving color images in " + image_path)
49 print("Saving depth images in " + depth_path)
50
51 def move_forward(vis):
52 # This function is called within the o3d.visualization.Visualizer::run() loop
53 # The run loop calls the function, then re-render
54 # So the sequence in this function is to:
55 # 1. Capture frame
56 # 2. index++, check ending criteria
57 # 3. Set camera
58 # 4. (Re-render)
59 ctr = vis.get_view_control()
60 glb = custom_draw_geometry_with_camera_trajectory
61 if glb.index >= 0:
62 print("Capture image {:05d}".format(glb.index))
63 # Capture and save image using Open3D.
64 vis.capture_depth_image(
65 os.path.join(depth_path, "{:05d}.png".format(glb.index)), False)
66 vis.capture_screen_image(
67 os.path.join(image_path, "{:05d}.png".format(glb.index)), False)
68
69 # Example to save image using matplotlib.
70 '''
71 depth = vis.capture_depth_float_buffer()
72 image = vis.capture_screen_float_buffer()
73 plt.imsave(os.path.join(depth_path, "{:05d}.png".format(glb.index)),
74 np.asarray(depth),
75 dpi=1)
76 plt.imsave(os.path.join(image_path, "{:05d}.png".format(glb.index)),
77 np.asarray(image),
78 dpi=1)
79 '''
80
81 glb.index = glb.index + 1
82 if glb.index < len(glb.trajectory.parameters):
83 ctr.convert_from_pinhole_camera_parameters(
84 glb.trajectory.parameters[glb.index])
85 else:
86 custom_draw_geometry_with_camera_trajectory.vis.destroy_window()
87
88 # Return false as we don't need to call UpdateGeometry()
89 return False
90
91 vis = custom_draw_geometry_with_camera_trajectory.vis
92 vis.create_window()
93 vis.add_geometry(pcd)
94 vis.get_render_option().load_from_json(render_option_path)
95 vis.register_animation_callback(move_forward)
96 vis.run()
97
98
99if __name__ == "__main__":
100 if not o3d._build_config['ENABLE_HEADLESS_RENDERING']:
101 print("Headless rendering is not enabled. "
102 "Please rebuild Open3D with ENABLE_HEADLESS_RENDERING=ON")
103 exit(1)
104
105 sample_data = o3d.data.DemoCustomVisualization()
106 pcd = o3d.io.read_point_cloud(sample_data.point_cloud_path)
107 print("Customized visualization playing a camera trajectory. "
108 "Press ctrl+z to terminate.")
109 custom_draw_geometry_with_camera_trajectory(
110 pcd, sample_data.camera_trajectory_path, sample_data.render_option_path,
111 'HeadlessRenderingOutput')
interactive_visualization.py
27# examples/python/visualization/interactive_visualization.py
28
29import numpy as np
30import copy
31import open3d as o3d
32
33
34def demo_crop_geometry():
35 print("Demo for manual geometry cropping")
36 print(
37 "1) Press 'Y' twice to align geometry with negative direction of y-axis"
38 )
39 print("2) Press 'K' to lock screen and to switch to selection mode")
40 print("3) Drag for rectangle selection,")
41 print(" or use ctrl + left click for polygon selection")
42 print("4) Press 'C' to get a selected geometry")
43 print("5) Press 'S' to save the selected geometry")
44 print("6) Press 'F' to switch to freeview mode")
45 pcd_data = o3d.data.DemoICPPointClouds()
46 pcd = o3d.io.read_point_cloud(pcd_data.paths[0])
47 o3d.visualization.draw_geometries_with_editing([pcd])
48
49
50def draw_registration_result(source, target, transformation):
51 source_temp = copy.deepcopy(source)
52 target_temp = copy.deepcopy(target)
53 source_temp.paint_uniform_color([1, 0.706, 0])
54 target_temp.paint_uniform_color([0, 0.651, 0.929])
55 source_temp.transform(transformation)
56 o3d.visualization.draw_geometries([source_temp, target_temp])
57
58
59def pick_points(pcd):
60 print("")
61 print(
62 "1) Please pick at least three correspondences using [shift + left click]"
63 )
64 print(" Press [shift + right click] to undo point picking")
65 print("2) After picking points, press 'Q' to close the window")
66 vis = o3d.visualization.VisualizerWithEditing()
67 vis.create_window()
68 vis.add_geometry(pcd)
69 vis.run() # user picks points
70 vis.destroy_window()
71 print("")
72 return vis.get_picked_points()
73
74
75def demo_manual_registration():
76 print("Demo for manual ICP")
77 pcd_data = o3d.data.DemoICPPointClouds()
78 source = o3d.io.read_point_cloud(pcd_data.paths[0])
79 target = o3d.io.read_point_cloud(pcd_data.paths[2])
80 print("Visualization of two point clouds before manual alignment")
81 draw_registration_result(source, target, np.identity(4))
82
83 # pick points from two point clouds and builds correspondences
84 picked_id_source = pick_points(source)
85 picked_id_target = pick_points(target)
86 assert (len(picked_id_source) >= 3 and len(picked_id_target) >= 3)
87 assert (len(picked_id_source) == len(picked_id_target))
88 corr = np.zeros((len(picked_id_source), 2))
89 corr[:, 0] = picked_id_source
90 corr[:, 1] = picked_id_target
91
92 # estimate rough transformation using correspondences
93 print("Compute a rough transform using the correspondences given by user")
94 p2p = o3d.pipelines.registration.TransformationEstimationPointToPoint()
95 trans_init = p2p.compute_transformation(source, target,
96 o3d.utility.Vector2iVector(corr))
97
98 # point-to-point ICP for refinement
99 print("Perform point-to-point ICP refinement")
100 threshold = 0.03 # 3cm distance threshold
101 reg_p2p = o3d.pipelines.registration.registration_icp(
102 source, target, threshold, trans_init,
103 o3d.pipelines.registration.TransformationEstimationPointToPoint())
104 draw_registration_result(source, target, reg_p2p.transformation)
105 print("")
106
107
108if __name__ == "__main__":
109 demo_crop_geometry()
110 demo_manual_registration()
line_width.py
27import open3d as o3d
28import random
29
30NUM_LINES = 10
31
32
33def random_point():
34 return [5 * random.random(), 5 * random.random(), 5 * random.random()]
35
36
37def main():
38 pts = [random_point() for _ in range(0, 2 * NUM_LINES)]
39 line_indices = [[2 * i, 2 * i + 1] for i in range(0, NUM_LINES)]
40 colors = [[0.0, 0.0, 0.0] for _ in range(0, NUM_LINES)]
41
42 lines = o3d.geometry.LineSet()
43 lines.points = o3d.utility.Vector3dVector(pts)
44 lines.lines = o3d.utility.Vector2iVector(line_indices)
45 # The default color of the lines is white, which will be invisible on the
46 # default white background. So we either need to set the color of the lines
47 # or the base_color of the material.
48 lines.colors = o3d.utility.Vector3dVector(colors)
49
50 # Some platforms do not require OpenGL implementations to support wide lines,
51 # so the renderer requires a custom shader to implement this: "unlitLine".
52 # The line_width field is only used by this shader; all other shaders ignore
53 # it.
54 mat = o3d.visualization.rendering.MaterialRecord()
55 mat.shader = "unlitLine"
56 mat.line_width = 10 # note that this is scaled with respect to pixels,
57 # so will give different results depending on the
58 # scaling values of your system
59 o3d.visualization.draw({
60 "name": "lines",
61 "geometry": lines,
62 "material": mat
63 })
64
65
66if __name__ == "__main__":
67 main()
load_save_viewpoint.py
27import open3d as o3d
28
29
30def save_view_point(pcd, filename):
31 vis = o3d.visualization.Visualizer()
32 vis.create_window()
33 vis.add_geometry(pcd)
34 vis.run() # user changes the view and press "q" to terminate
35 param = vis.get_view_control().convert_to_pinhole_camera_parameters()
36 o3d.io.write_pinhole_camera_parameters(filename, param)
37 vis.destroy_window()
38
39
40def load_view_point(pcd, filename):
41 vis = o3d.visualization.Visualizer()
42 vis.create_window()
43 ctr = vis.get_view_control()
44 param = o3d.io.read_pinhole_camera_parameters(filename)
45 vis.add_geometry(pcd)
46 ctr.convert_from_pinhole_camera_parameters(param)
47 vis.run()
48 vis.destroy_window()
49
50
51if __name__ == "__main__":
52 pcd_data = o3d.data.PCDPointCloud()
53 pcd = o3d.io.read_point_cloud(pcd_data.path)
54 save_view_point(pcd, "viewpoint.json")
55 load_view_point(pcd, "viewpoint.json")
mouse_and_point_coord.py
27import numpy as np
28import open3d as o3d
29import open3d.visualization.gui as gui
30import open3d.visualization.rendering as rendering
31
32
33# This example displays a point cloud and if you Ctrl-click on a point
34# (Cmd-click on macOS) it will show the coordinates of the point.
35# This example illustrates:
36# - custom mouse handling on SceneWidget
37# - getting a the depth value of a point (OpenGL depth)
38# - converting from a window point + OpenGL depth to world coordinate
39class ExampleApp:
40
41 def __init__(self, cloud):
42 # We will create a SceneWidget that fills the entire window, and then
43 # a label in the lower left on top of the SceneWidget to display the
44 # coordinate.
45 app = gui.Application.instance
46 self.window = app.create_window("Open3D - GetCoord Example", 1024, 768)
47 # Since we want the label on top of the scene, we cannot use a layout,
48 # so we need to manually layout the window's children.
49 self.window.set_on_layout(self._on_layout)
50 self.widget3d = gui.SceneWidget()
51 self.window.add_child(self.widget3d)
52 self.info = gui.Label("")
53 self.info.visible = False
54 self.window.add_child(self.info)
55
56 self.widget3d.scene = rendering.Open3DScene(self.window.renderer)
57
58 mat = rendering.MaterialRecord()
59 mat.shader = "defaultUnlit"
60 # Point size is in native pixels, but "pixel" means different things to
61 # different platforms (macOS, in particular), so multiply by Window scale
62 # factor.
63 mat.point_size = 3 * self.window.scaling
64 self.widget3d.scene.add_geometry("Point Cloud", cloud, mat)
65
66 bounds = self.widget3d.scene.bounding_box
67 center = bounds.get_center()
68 self.widget3d.setup_camera(60, bounds, center)
69 self.widget3d.look_at(center, center - [0, 0, 3], [0, -1, 0])
70
71 self.widget3d.set_on_mouse(self._on_mouse_widget3d)
72
73 def _on_layout(self, layout_context):
74 r = self.window.content_rect
75 self.widget3d.frame = r
76 pref = self.info.calc_preferred_size(layout_context,
77 gui.Widget.Constraints())
78 self.info.frame = gui.Rect(r.x,
79 r.get_bottom() - pref.height, pref.width,
80 pref.height)
81
82 def _on_mouse_widget3d(self, event):
83 # We could override BUTTON_DOWN without a modifier, but that would
84 # interfere with manipulating the scene.
85 if event.type == gui.MouseEvent.Type.BUTTON_DOWN and event.is_modifier_down(
86 gui.KeyModifier.CTRL):
87
88 def depth_callback(depth_image):
89 # Coordinates are expressed in absolute coordinates of the
90 # window, but to dereference the image correctly we need them
91 # relative to the origin of the widget. Note that even if the
92 # scene widget is the only thing in the window, if a menubar
93 # exists it also takes up space in the window (except on macOS).
94 x = event.x - self.widget3d.frame.x
95 y = event.y - self.widget3d.frame.y
96 # Note that np.asarray() reverses the axes.
97 depth = np.asarray(depth_image)[y, x]
98
99 if depth == 1.0: # clicked on nothing (i.e. the far plane)
100 text = ""
101 else:
102 world = self.widget3d.scene.camera.unproject(
103 event.x, event.y, depth, self.widget3d.frame.width,
104 self.widget3d.frame.height)
105 text = "({:.3f}, {:.3f}, {:.3f})".format(
106 world[0], world[1], world[2])
107
108 # This is not called on the main thread, so we need to
109 # post to the main thread to safely access UI items.
110 def update_label():
111 self.info.text = text
112 self.info.visible = (text != "")
113 # We are sizing the info label to be exactly the right size,
114 # so since the text likely changed width, we need to
115 # re-layout to set the new frame.
116 self.window.set_needs_layout()
117
118 gui.Application.instance.post_to_main_thread(
119 self.window, update_label)
120
121 self.widget3d.scene.scene.render_to_depth_image(depth_callback)
122 return gui.Widget.EventCallbackResult.HANDLED
123 return gui.Widget.EventCallbackResult.IGNORED
124
125
126def main():
127 app = gui.Application.instance
128 app.initialize()
129
130 # This example will also work with a triangle mesh, or any 3D object.
131 # If you use a triangle mesh you will probably want to set the material
132 # shader to "defaultLit" instead of "defaultUnlit".
133 pcd_data = o3d.data.DemoICPPointClouds()
134 cloud = o3d.io.read_point_cloud(pcd_data.paths[0])
135 ex = ExampleApp(cloud)
136
137 app.run()
138
139
140if __name__ == "__main__":
141 main()
multiple_windows.py
27import numpy as np
28import open3d as o3d
29import threading
30import time
31
32CLOUD_NAME = "points"
33
34
35def main():
36 MultiWinApp().run()
37
38
39class MultiWinApp:
40
41 def __init__(self):
42 self.is_done = False
43 self.n_snapshots = 0
44 self.cloud = None
45 self.main_vis = None
46 self.snapshot_pos = None
47
48 def run(self):
49 app = o3d.visualization.gui.Application.instance
50 app.initialize()
51
52 self.main_vis = o3d.visualization.O3DVisualizer(
53 "Open3D - Multi-Window Demo")
54 self.main_vis.add_action("Take snapshot in new window",
55 self.on_snapshot)
56 self.main_vis.set_on_close(self.on_main_window_closing)
57
58 app.add_window(self.main_vis)
59 self.snapshot_pos = (self.main_vis.os_frame.x, self.main_vis.os_frame.y)
60
61 threading.Thread(target=self.update_thread).start()
62
63 app.run()
64
65 def on_snapshot(self, vis):
66 self.n_snapshots += 1
67 self.snapshot_pos = (self.snapshot_pos[0] + 50,
68 self.snapshot_pos[1] + 50)
69 title = "Open3D - Multi-Window Demo (Snapshot #" + str(
70 self.n_snapshots) + ")"
71 new_vis = o3d.visualization.O3DVisualizer(title)
72 mat = o3d.visualization.rendering.MaterialRecord()
73 mat.shader = "defaultUnlit"
74 new_vis.add_geometry(CLOUD_NAME + " #" + str(self.n_snapshots),
75 self.cloud, mat)
76 new_vis.reset_camera_to_default()
77 bounds = self.cloud.get_axis_aligned_bounding_box()
78 extent = bounds.get_extent()
79 new_vis.setup_camera(60, bounds.get_center(),
80 bounds.get_center() + [0, 0, -3], [0, -1, 0])
81 o3d.visualization.gui.Application.instance.add_window(new_vis)
82 new_vis.os_frame = o3d.visualization.gui.Rect(self.snapshot_pos[0],
83 self.snapshot_pos[1],
84 new_vis.os_frame.width,
85 new_vis.os_frame.height)
86
87 def on_main_window_closing(self):
88 self.is_done = True
89 return True # False would cancel the close
90
91 def update_thread(self):
92 # This is NOT the UI thread, need to call post_to_main_thread() to update
93 # the scene or any part of the UI.
94 pcd_data = o3d.data.DemoICPPointClouds()
95 self.cloud = o3d.io.read_point_cloud(pcd_data.paths[0])
96 bounds = self.cloud.get_axis_aligned_bounding_box()
97 extent = bounds.get_extent()
98
99 def add_first_cloud():
100 mat = o3d.visualization.rendering.MaterialRecord()
101 mat.shader = "defaultUnlit"
102 self.main_vis.add_geometry(CLOUD_NAME, self.cloud, mat)
103 self.main_vis.reset_camera_to_default()
104 self.main_vis.setup_camera(60, bounds.get_center(),
105 bounds.get_center() + [0, 0, -3],
106 [0, -1, 0])
107
108 o3d.visualization.gui.Application.instance.post_to_main_thread(
109 self.main_vis, add_first_cloud)
110
111 while not self.is_done:
112 time.sleep(0.1)
113
114 # Perturb the cloud with a random walk to simulate an actual read
115 pts = np.asarray(self.cloud.points)
116 magnitude = 0.005 * extent
117 displacement = magnitude * (np.random.random_sample(pts.shape) -
118 0.5)
119 new_pts = pts + displacement
120 self.cloud.points = o3d.utility.Vector3dVector(new_pts)
121
122 def update_cloud():
123 # Note: if the number of points is less than or equal to the
124 # number of points in the original object that was added,
125 # using self.scene.update_geometry() will be faster.
126 # Requires that the point cloud be a t.PointCloud.
127 self.main_vis.remove_geometry(CLOUD_NAME)
128 mat = o3d.visualization.rendering.MaterialRecord()
129 mat.shader = "defaultUnlit"
130 self.main_vis.add_geometry(CLOUD_NAME, self.cloud, mat)
131
132 if self.is_done: # might have changed while sleeping
133 break
134 o3d.visualization.gui.Application.instance.post_to_main_thread(
135 self.main_vis, update_cloud)
136
137
138if __name__ == "__main__":
139 main()
non_blocking_visualization.py
27# examples/python/visualization/non_blocking_visualization.py
28
29import open3d as o3d
30import numpy as np
31
32if __name__ == "__main__":
33 o3d.utility.set_verbosity_level(o3d.utility.VerbosityLevel.Debug)
34 pcd_data = o3d.data.DemoICPPointClouds()
35 source_raw = o3d.io.read_point_cloud(pcd_data.paths[0])
36 target_raw = o3d.io.read_point_cloud(pcd_data.paths[1])
37
38 source = source_raw.voxel_down_sample(voxel_size=0.02)
39 target = target_raw.voxel_down_sample(voxel_size=0.02)
40 trans = [[0.862, 0.011, -0.507, 0.0], [-0.139, 0.967, -0.215, 0.7],
41 [0.487, 0.255, 0.835, -1.4], [0.0, 0.0, 0.0, 1.0]]
42 source.transform(trans)
43
44 flip_transform = [[1, 0, 0, 0], [0, -1, 0, 0], [0, 0, -1, 0], [0, 0, 0, 1]]
45 source.transform(flip_transform)
46 target.transform(flip_transform)
47
48 vis = o3d.visualization.Visualizer()
49 vis.create_window()
50 vis.add_geometry(source)
51 vis.add_geometry(target)
52 threshold = 0.05
53 icp_iteration = 100
54 save_image = False
55
56 for i in range(icp_iteration):
57 reg_p2l = o3d.pipelines.registration.registration_icp(
58 source, target, threshold, np.identity(4),
59 o3d.pipelines.registration.TransformationEstimationPointToPlane(),
60 o3d.pipelines.registration.ICPConvergenceCriteria(max_iteration=1))
61 source.transform(reg_p2l.transformation)
62 vis.update_geometry(source)
63 vis.poll_events()
64 vis.update_renderer()
65 if save_image:
66 vis.capture_screen_image("temp_%04d.jpg" % i)
67 vis.destroy_window()
68 o3d.utility.set_verbosity_level(o3d.utility.VerbosityLevel.Info)
non_english.py
27import open3d.visualization.gui as gui
28import os.path
29import platform
30
31basedir = os.path.dirname(os.path.realpath(__file__))
32
33# This is all-widgets.py with some modifications for non-English languages.
34# Please see all-widgets.py for usage of the GUI widgets
35
36MODE_SERIF = "serif"
37MODE_COMMON_HANYU = "common"
38MODE_SERIF_AND_COMMON_HANYU = "serif+common"
39MODE_COMMON_HANYU_EN = "hanyu_en+common"
40MODE_ALL_HANYU = "all"
41MODE_CUSTOM_CHARS = "custom"
42
43#mode = MODE_SERIF
44#mode = MODE_COMMON_HANYU
45mode = MODE_SERIF_AND_COMMON_HANYU
46#mode = MODE_ALL_HANYU
47#mode = MODE_CUSTOM_CHARS
48
49# Fonts can be names or paths
50if platform.system() == "Darwin":
51 serif = "Times New Roman"
52 hanzi = "STHeiti Light"
53 chess = "/System/Library/Fonts/Apple Symbols.ttf"
54elif platform.system() == "Windows":
55 # it is necessary to specify paths on Windows since it stores its fonts
56 # with a cryptic name, so font name searches do not work on Windows
57 serif = "c:/windows/fonts/times.ttf" # Times New Roman
58 hanzi = "c:/windows/fonts/msyh.ttc" # YaHei UI
59 chess = "c:/windows/fonts/seguisym.ttf" # Segoe UI Symbol
60else:
61 # Assumes Ubuntu 18.04
62 serif = "DejaVuSerif"
63 hanzi = "NotoSansCJK"
64 chess = "/usr/share/fonts/truetype/freefont/FreeSerif.ttf"
65
66
67def main():
68 gui.Application.instance.initialize()
69
70 # Font changes must be done after initialization but before creating
71 # a window.
72
73 # MODE_SERIF changes the English font; Chinese will not be displayed
74 font = None
75 if mode == MODE_SERIF:
76 font = gui.FontDescription(serif)
77 # MODE_COMMON_HANYU uses the default English font and adds common Chinese
78 elif mode == MODE_COMMON_HANYU:
79 font = gui.FontDescription()
80 font.add_typeface_for_language(hanzi, "zh")
81 # MODE_SERIF_AND_COMMON_HANYU uses a serif English font and adds common
82 # Chinese characters
83 elif mode == MODE_SERIF_AND_COMMON_HANYU:
84 font = gui.FontDescription(serif)
85 font.add_typeface_for_language(hanzi, "zh")
86 # MODE_COMMON_HANYU_EN the Chinese font for both English and the common
87 # characters
88 elif mode == MODE_COMMON_HANYU_EN:
89 font = gui.FontDescription(hanzi)
90 font.add_typeface_for_language(hanzi, "zh")
91 # MODE_ALL_HANYU uses the default English font but includes all the Chinese
92 # characters (which uses a substantial amount of memory)
93 elif mode == MODE_ALL_HANYU:
94 font = gui.FontDescription()
95 font.add_typeface_for_language(hanzi, "zh_all")
96 elif mode == MODE_CUSTOM_CHARS:
97 range = [0x2654, 0x2655, 0x2656, 0x2657, 0x2658, 0x2659]
98 font = gui.FontDescription()
99 font.add_typeface_for_code_points(chess, range)
100
101 if font is not None:
102 gui.Application.instance.set_font(gui.Application.DEFAULT_FONT_ID, font)
103
104 w = ExampleWindow()
105 gui.Application.instance.run()
106
107
108class ExampleWindow:
109 MENU_CHECKABLE = 1
110 MENU_DISABLED = 2
111 MENU_QUIT = 3
112
113 def __init__(self):
114 self.window = gui.Application.instance.create_window("Test", 400, 768)
115 # self.window = gui.Application.instance.create_window("Test", 400, 768,
116 # x=50, y=100)
117 w = self.window # for more concise code
118
119 # Rather than specifying sizes in pixels, which may vary in size based
120 # on the monitor, especially on macOS which has 220 dpi monitors, use
121 # the em-size. This way sizings will be proportional to the font size,
122 # which will create a more visually consistent size across platforms.
123 em = w.theme.font_size
124
125 # Widgets are laid out in layouts: gui.Horiz, gui.Vert,
126 # gui.CollapsableVert, and gui.VGrid. By nesting the layouts we can
127 # achieve complex designs. Usually we use a vertical layout as the
128 # topmost widget, since widgets tend to be organized from top to bottom.
129 # Within that, we usually have a series of horizontal layouts for each
130 # row.
131 layout = gui.Vert(0, gui.Margins(0.5 * em, 0.5 * em, 0.5 * em,
132 0.5 * em))
133
134 # Create the menu. The menu is global (because the macOS menu is global),
135 # so only create it once.
136 if gui.Application.instance.menubar is None:
137 menubar = gui.Menu()
138 test_menu = gui.Menu()
139 test_menu.add_item("An option", ExampleWindow.MENU_CHECKABLE)
140 test_menu.set_checked(ExampleWindow.MENU_CHECKABLE, True)
141 test_menu.add_item("Unavailable feature",
142 ExampleWindow.MENU_DISABLED)
143 test_menu.set_enabled(ExampleWindow.MENU_DISABLED, False)
144 test_menu.add_separator()
145 test_menu.add_item("Quit", ExampleWindow.MENU_QUIT)
146 # On macOS the first menu item is the application menu item and will
147 # always be the name of the application (probably "Python"),
148 # regardless of what you pass in here. The application menu is
149 # typically where About..., Preferences..., and Quit go.
150 menubar.add_menu("Test", test_menu)
151 gui.Application.instance.menubar = menubar
152
153 # Each window needs to know what to do with the menu items, so we need
154 # to tell the window how to handle menu items.
155 w.set_on_menu_item_activated(ExampleWindow.MENU_CHECKABLE,
156 self._on_menu_checkable)
157 w.set_on_menu_item_activated(ExampleWindow.MENU_QUIT,
158 self._on_menu_quit)
159
160 # Create a file-chooser widget. One part will be a text edit widget for
161 # the filename and clicking on the button will let the user choose using
162 # the file dialog.
163 self._fileedit = gui.TextEdit()
164 filedlgbutton = gui.Button("...")
165 filedlgbutton.horizontal_padding_em = 0.5
166 filedlgbutton.vertical_padding_em = 0
167 filedlgbutton.set_on_clicked(self._on_filedlg_button)
168
169 # (Create the horizontal widget for the row. This will make sure the
170 # text editor takes up as much space as it can.)
171 fileedit_layout = gui.Horiz()
172 fileedit_layout.add_child(gui.Label("Model file"))
173 fileedit_layout.add_child(self._fileedit)
174 fileedit_layout.add_fixed(0.25 * em)
175 fileedit_layout.add_child(filedlgbutton)
176 # add to the top-level (vertical) layout
177 layout.add_child(fileedit_layout)
178
179 # Create a collapsible vertical widget, which takes up enough vertical
180 # space for all its children when open, but only enough for text when
181 # closed. This is useful for property pages, so the user can hide sets
182 # of properties they rarely use. All layouts take a spacing parameter,
183 # which is the spacinging between items in the widget, and a margins
184 # parameter, which specifies the spacing of the left, top, right,
185 # bottom margins. (This acts like the 'padding' property in CSS.)
186 collapse = gui.CollapsableVert("Widgets", 0.33 * em,
187 gui.Margins(em, 0, 0, 0))
188 if mode == MODE_CUSTOM_CHARS:
189 self._label = gui.Label("♔♕♖♗♘♙")
190 elif mode == MODE_ALL_HANYU:
191 self._label = gui.Label("天地玄黃,宇宙洪荒。日月盈昃,辰宿列張。")
192 else:
193 self._label = gui.Label("锄禾日当午,汗滴禾下土。谁知盘中餐,粒粒皆辛苦。")
194 self._label.text_color = gui.Color(1.0, 0.5, 0.0)
195 collapse.add_child(self._label)
196
197 # Create a checkbox. Checking or unchecking would usually be used to set
198 # a binary property, but in this case it will show a simple message box,
199 # which illustrates how to create simple dialogs.
200 cb = gui.Checkbox("Enable some really cool effect")
201 cb.set_on_checked(self._on_cb) # set the callback function
202 collapse.add_child(cb)
203
204 # Create a color editor. We will change the color of the orange label
205 # above when the color changes.
206 color = gui.ColorEdit()
207 color.color_value = self._label.text_color
208 color.set_on_value_changed(self._on_color)
209 collapse.add_child(color)
210
211 # This is a combobox, nothing fancy here, just set a simple function to
212 # handle the user selecting an item.
213 combo = gui.Combobox()
214 combo.add_item("Show point labels")
215 combo.add_item("Show point velocity")
216 combo.add_item("Show bounding boxes")
217 combo.set_on_selection_changed(self._on_combo)
218 collapse.add_child(combo)
219
220 # Add a simple image
221 logo = gui.ImageWidget(basedir + "/icon-32.png")
222 collapse.add_child(logo)
223
224 # Add a list of items
225 lv = gui.ListView()
226 lv.set_items(["Ground", "Trees", "Buildings" "Cars", "People"])
227 lv.selected_index = lv.selected_index + 2 # initially is -1, so now 1
228 lv.set_on_selection_changed(self._on_list)
229 collapse.add_child(lv)
230
231 # Add a tree view
232 tree = gui.TreeView()
233 tree.add_text_item(tree.get_root_item(), "Camera")
234 geo_id = tree.add_text_item(tree.get_root_item(), "Geometries")
235 mesh_id = tree.add_text_item(geo_id, "Mesh")
236 tree.add_text_item(mesh_id, "Triangles")
237 tree.add_text_item(mesh_id, "Albedo texture")
238 tree.add_text_item(mesh_id, "Normal map")
239 points_id = tree.add_text_item(geo_id, "Points")
240 tree.can_select_items_with_children = True
241 tree.set_on_selection_changed(self._on_tree)
242 # does not call on_selection_changed: user did not change selection
243 tree.selected_item = points_id
244 collapse.add_child(tree)
245
246 # Add two number editors, one for integers and one for floating point
247 # Number editor can clamp numbers to a range, although this is more
248 # useful for integers than for floating point.
249 intedit = gui.NumberEdit(gui.NumberEdit.INT)
250 intedit.int_value = 0
251 intedit.set_limits(1, 19) # value coerced to 1
252 intedit.int_value = intedit.int_value + 2 # value should be 3
253 doubleedit = gui.NumberEdit(gui.NumberEdit.DOUBLE)
254 numlayout = gui.Horiz()
255 numlayout.add_child(gui.Label("int"))
256 numlayout.add_child(intedit)
257 numlayout.add_fixed(em) # manual spacing (could set it in Horiz() ctor)
258 numlayout.add_child(gui.Label("double"))
259 numlayout.add_child(doubleedit)
260 collapse.add_child(numlayout)
261
262 # Create a progress bar. It ranges from 0.0 to 1.0.
263 self._progress = gui.ProgressBar()
264 self._progress.value = 0.25 # 25% complete
265 self._progress.value = self._progress.value + 0.08 # 0.25 + 0.08 = 33%
266 prog_layout = gui.Horiz(em)
267 prog_layout.add_child(gui.Label("Progress..."))
268 prog_layout.add_child(self._progress)
269 collapse.add_child(prog_layout)
270
271 # Create a slider. It acts very similar to NumberEdit except that the
272 # user moves a slider and cannot type the number.
273 slider = gui.Slider(gui.Slider.INT)
274 slider.set_limits(5, 13)
275 slider.set_on_value_changed(self._on_slider)
276 collapse.add_child(slider)
277
278 # Create a text editor. The placeholder text (if not empty) will be
279 # displayed when there is no text, as concise help, or visible tooltip.
280 tedit = gui.TextEdit()
281 tedit.placeholder_text = "Edit me some text here"
282
283 # on_text_changed fires whenever the user changes the text (but not if
284 # the text_value property is assigned to).
285 tedit.set_on_text_changed(self._on_text_changed)
286
287 # on_value_changed fires whenever the user signals that they are finished
288 # editing the text, either by pressing return or by clicking outside of
289 # the text editor, thus losing text focus.
290 tedit.set_on_value_changed(self._on_value_changed)
291 collapse.add_child(tedit)
292
293 # Create a widget for showing/editing a 3D vector
294 vedit = gui.VectorEdit()
295 vedit.vector_value = [1, 2, 3]
296 vedit.set_on_value_changed(self._on_vedit)
297 collapse.add_child(vedit)
298
299 # Create a VGrid layout. This layout specifies the number of columns
300 # (two, in this case), and will place the first child in the first
301 # column, the second in the second, the third in the first, the fourth
302 # in the second, etc.
303 # So:
304 # 2 cols 3 cols 4 cols
305 # | 1 | 2 | | 1 | 2 | 3 | | 1 | 2 | 3 | 4 |
306 # | 3 | 4 | | 4 | 5 | 6 | | 5 | 6 | 7 | 8 |
307 # | 5 | 6 | | 7 | 8 | 9 | | 9 | 10 | 11 | 12 |
308 # | ... | | ... | | ... |
309 vgrid = gui.VGrid(2)
310 vgrid.add_child(gui.Label("Trees"))
311 vgrid.add_child(gui.Label("12 items"))
312 vgrid.add_child(gui.Label("People"))
313 vgrid.add_child(gui.Label("2 (93% certainty)"))
314 vgrid.add_child(gui.Label("Cars"))
315 vgrid.add_child(gui.Label("5 (87% certainty)"))
316 collapse.add_child(vgrid)
317
318 # Create a tab control. This is really a set of N layouts on top of each
319 # other, but with only one selected.
320 tabs = gui.TabControl()
321 tab1 = gui.Vert()
322 tab1.add_child(gui.Checkbox("Enable option 1"))
323 tab1.add_child(gui.Checkbox("Enable option 2"))
324 tab1.add_child(gui.Checkbox("Enable option 3"))
325 tabs.add_tab("Options", tab1)
326 tab2 = gui.Vert()
327 tab2.add_child(gui.Label("No plugins detected"))
328 tab2.add_stretch()
329 tabs.add_tab("Plugins", tab2)
330 collapse.add_child(tabs)
331
332 # Quit button. (Typically this is a menu item)
333 button_layout = gui.Horiz()
334 ok_button = gui.Button("Ok")
335 ok_button.set_on_clicked(self._on_ok)
336 button_layout.add_stretch()
337 button_layout.add_child(ok_button)
338
339 layout.add_child(collapse)
340 layout.add_child(button_layout)
341
342 # We're done, set the window's layout
343 w.add_child(layout)
344
345 def _on_filedlg_button(self):
346 filedlg = gui.FileDialog(gui.FileDialog.OPEN, "Select file",
347 self.window.theme)
348 filedlg.add_filter(".obj .ply .stl", "Triangle mesh (.obj, .ply, .stl)")
349 filedlg.add_filter("", "All files")
350 filedlg.set_on_cancel(self._on_filedlg_cancel)
351 filedlg.set_on_done(self._on_filedlg_done)
352 self.window.show_dialog(filedlg)
353
354 def _on_filedlg_cancel(self):
355 self.window.close_dialog()
356
357 def _on_filedlg_done(self, path):
358 self._fileedit.text_value = path
359 self.window.close_dialog()
360
361 def _on_cb(self, is_checked):
362 if is_checked:
363 text = "Sorry, effects are unimplemented"
364 else:
365 text = "Good choice"
366
367 self.show_message_dialog("There might be a problem...", text)
368
369 # This function is essentially the same as window.show_message_box(),
370 # so for something this simple just use that, but it illustrates making a
371 # dialog.
372 def show_message_dialog(self, title, message):
373 # A Dialog is just a widget, so you make its child a layout just like
374 # a Window.
375 dlg = gui.Dialog(title)
376
377 # Add the message text
378 em = self.window.theme.font_size
379 dlg_layout = gui.Vert(em, gui.Margins(em, em, em, em))
380 dlg_layout.add_child(gui.Label(message))
381
382 # Add the Ok button. We need to define a callback function to handle
383 # the click.
384 ok_button = gui.Button("Ok")
385 ok_button.set_on_clicked(self._on_dialog_ok)
386
387 # We want the Ok button to be an the right side, so we need to add
388 # a stretch item to the layout, otherwise the button will be the size
389 # of the entire row. A stretch item takes up as much space as it can,
390 # which forces the button to be its minimum size.
391 button_layout = gui.Horiz()
392 button_layout.add_stretch()
393 button_layout.add_child(ok_button)
394
395 # Add the button layout,
396 dlg_layout.add_child(button_layout)
397 # ... then add the layout as the child of the Dialog
398 dlg.add_child(dlg_layout)
399 # ... and now we can show the dialog
400 self.window.show_dialog(dlg)
401
402 def _on_dialog_ok(self):
403 self.window.close_dialog()
404
405 def _on_color(self, new_color):
406 self._label.text_color = new_color
407
408 def _on_combo(self, new_val, new_idx):
409 print(new_idx, new_val)
410
411 def _on_list(self, new_val, is_dbl_click):
412 print(new_val)
413
414 def _on_tree(self, new_item_id):
415 print(new_item_id)
416
417 def _on_slider(self, new_val):
418 self._progress.value = new_val / 20.0
419
420 def _on_text_changed(self, new_text):
421 print("edit:", new_text)
422
423 def _on_value_changed(self, new_text):
424 print("value:", new_text)
425
426 def _on_vedit(self, new_val):
427 print(new_val)
428
429 def _on_ok(self):
430 gui.Application.instance.quit()
431
432 def _on_menu_checkable(self):
433 gui.Application.instance.menubar.set_checked(
434 ExampleWindow.MENU_CHECKABLE,
435 not gui.Application.instance.menubar.is_checked(
436 ExampleWindow.MENU_CHECKABLE))
437
438 def _on_menu_quit(self):
439 gui.Application.instance.quit()
440
441
442# This class is essentially the same as window.show_message_box(),
443# so for something this simple just use that, but it illustrates making a
444# dialog.
445class MessageBox:
446
447 def __init__(self, title, message):
448 self._window = None
449
450 # A Dialog is just a widget, so you make its child a layout just like
451 # a Window.
452 dlg = gui.Dialog(title)
453
454 # Add the message text
455 em = self.window.theme.font_size
456 dlg_layout = gui.Vert(em, gui.Margins(em, em, em, em))
457 dlg_layout.add_child(gui.Label(message))
458
459 # Add the Ok button. We need to define a callback function to handle
460 # the click.
461 ok_button = gui.Button("Ok")
462 ok_button.set_on_clicked(self._on_ok)
463
464 # We want the Ok button to be an the right side, so we need to add
465 # a stretch item to the layout, otherwise the button will be the size
466 # of the entire row. A stretch item takes up as much space as it can,
467 # which forces the button to be its minimum size.
468 button_layout = gui.Horiz()
469 button_layout.add_stretch()
470 button_layout.add_child(ok_button)
471
472 # Add the button layout,
473 dlg_layout.add_child(button_layout)
474 # ... then add the layout as the child of the Dialog
475 dlg.add_child(dlg_layout)
476
477 def show(self, window):
478 self._window = window
479
480 def _on_ok(self):
481 self._window.close_dialog()
482
483
484if __name__ == "__main__":
485 main()
online_processing.py
27
28- Connects to a RGBD camera or RGBD video file (currently
29 RealSense camera and bag file format are supported).
30- Captures / reads color and depth frames. Allow recording from camera.
31- Convert frames to point cloud, optionally with normals.
32- Visualize point cloud video and results.
33- Save point clouds and RGBD images for selected frames.
34
35For this example, Open3D must be built with -DBUILD_LIBREALSENSE=ON
36"""
37
38import os
39import json
40import time
41import logging as log
42import argparse
43import threading
44from datetime import datetime
45from concurrent.futures import ThreadPoolExecutor
46import numpy as np
47import open3d as o3d
48import open3d.visualization.gui as gui
49import open3d.visualization.rendering as rendering
50
51
52# Camera and processing
53class PipelineModel:
54 """Controls IO (camera, video file, recording, saving frames). Methods run
55 in worker threads."""
56
57 def __init__(self,
58 update_view,
59 camera_config_file=None,
60 rgbd_video=None,
61 device=None):
62 """Initialize.
63
64 Args:
65 update_view (callback): Callback to update display elements for a
66 frame.
67 camera_config_file (str): Camera configuration json file.
68 rgbd_video (str): RS bag file containing the RGBD video. If this is
69 provided, connected cameras are ignored.
70 device (str): Compute device (e.g.: 'cpu:0' or 'cuda:0').
71 """
72 self.update_view = update_view
73 if device:
74 self.device = device.lower()
75 else:
76 self.device = 'cuda:0' if o3d.core.cuda.is_available() else 'cpu:0'
77 self.o3d_device = o3d.core.Device(self.device)
78
79 self.video = None
80 self.camera = None
81 self.flag_capture = False
82 self.cv_capture = threading.Condition() # condition variable
83 self.recording = False # Are we currently recording
84 self.flag_record = False # Request to start/stop recording
85 if rgbd_video: # Video file
86 self.video = o3d.t.io.RGBDVideoReader.create(rgbd_video)
87 self.rgbd_metadata = self.video.metadata
88 self.status_message = f"Video {rgbd_video} opened."
89
90 else: # RGBD camera
91 now = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
92 filename = f"{now}.bag"
93 self.camera = o3d.t.io.RealSenseSensor()
94 if camera_config_file:
95 with open(camera_config_file) as ccf:
96 self.camera.init_sensor(o3d.t.io.RealSenseSensorConfig(
97 json.load(ccf)),
98 filename=filename)
99 else:
100 self.camera.init_sensor(filename=filename)
101 self.camera.start_capture(start_record=False)
102 self.rgbd_metadata = self.camera.get_metadata()
103 self.status_message = f"Camera {self.rgbd_metadata.serial_number} opened."
104
105 log.info(self.rgbd_metadata)
106
107 # RGBD -> PCD
108 self.extrinsics = o3d.core.Tensor.eye(4,
109 dtype=o3d.core.Dtype.Float32,
110 device=self.o3d_device)
111 self.intrinsic_matrix = o3d.core.Tensor(
112 self.rgbd_metadata.intrinsics.intrinsic_matrix,
113 dtype=o3d.core.Dtype.Float32,
114 device=self.o3d_device)
115 self.depth_max = 3.0 # m
116 self.pcd_stride = 2 # downsample point cloud, may increase frame rate
117 self.flag_normals = False
118 self.flag_save_rgbd = False
119 self.flag_save_pcd = False
120
121 self.pcd_frame = None
122 self.rgbd_frame = None
123 self.executor = ThreadPoolExecutor(max_workers=3,
124 thread_name_prefix='Capture-Save')
125 self.flag_exit = False
126
127 @property
128 def max_points(self):
129 """Max points in one frame for the camera or RGBD video resolution."""
130 return self.rgbd_metadata.width * self.rgbd_metadata.height
131
132 @property
133 def vfov(self):
134 """Camera or RGBD video vertical field of view."""
135 return np.rad2deg(2 * np.arctan(self.intrinsic_matrix[1, 2].item() /
136 self.intrinsic_matrix[1, 1].item()))
137
138 def run(self):
139 """Run pipeline."""
140 n_pts = 0
141 frame_id = 0
142 t1 = time.perf_counter()
143 if self.video:
144 self.rgbd_frame = self.video.next_frame()
145 else:
146 self.rgbd_frame = self.camera.capture_frame(
147 wait=True, align_depth_to_color=True)
148
149 pcd_errors = 0
150 while (not self.flag_exit and
151 (self.video is None or # Camera
152 (self.video and not self.video.is_eof()))): # Video
153 if self.video:
154 future_rgbd_frame = self.executor.submit(self.video.next_frame)
155 else:
156 future_rgbd_frame = self.executor.submit(
157 self.camera.capture_frame,
158 wait=True,
159 align_depth_to_color=True)
160
161 if self.flag_save_pcd:
162 self.save_pcd()
163 self.flag_save_pcd = False
164 try:
165 self.rgbd_frame = self.rgbd_frame.to(self.o3d_device)
166 self.pcd_frame = o3d.t.geometry.PointCloud.create_from_rgbd_image(
167 self.rgbd_frame, self.intrinsic_matrix, self.extrinsics,
168 self.rgbd_metadata.depth_scale, self.depth_max,
169 self.pcd_stride, self.flag_normals)
170 depth_in_color = self.rgbd_frame.depth.colorize_depth(
171 self.rgbd_metadata.depth_scale, 0, self.depth_max)
172 except RuntimeError:
173 pcd_errors += 1
174
175 if self.pcd_frame.is_empty():
176 log.warning(f"No valid depth data in frame {frame_id})")
177 continue
178
179 n_pts += self.pcd_frame.point.positions.shape[0]
180 if frame_id % 60 == 0 and frame_id > 0:
181 t0, t1 = t1, time.perf_counter()
182 log.debug(f"\nframe_id = {frame_id}, \t {(t1-t0)*1000./60:0.2f}"
183 f"ms/frame \t {(t1-t0)*1e9/n_pts} ms/Mp\t")
184 n_pts = 0
185 frame_elements = {
186 'color': self.rgbd_frame.color.cpu(),
187 'depth': depth_in_color.cpu(),
188 'pcd': self.pcd_frame.cpu(),
189 'status_message': self.status_message
190 }
191 self.update_view(frame_elements)
192
193 if self.flag_save_rgbd:
194 self.save_rgbd()
195 self.flag_save_rgbd = False
196 self.rgbd_frame = future_rgbd_frame.result()
197 with self.cv_capture: # Wait for capture to be enabled
198 self.cv_capture.wait_for(
199 predicate=lambda: self.flag_capture or self.flag_exit)
200 self.toggle_record()
201 frame_id += 1
202
203 if self.camera:
204 self.camera.stop_capture()
205 else:
206 self.video.close()
207 self.executor.shutdown()
208 log.debug(f"create_from_depth_image() errors = {pcd_errors}")
209
210 def toggle_record(self):
211 if self.camera is not None:
212 if self.flag_record and not self.recording:
213 self.camera.resume_record()
214 self.recording = True
215 elif not self.flag_record and self.recording:
216 self.camera.pause_record()
217 self.recording = False
218
219 def save_pcd(self):
220 """Save current point cloud."""
221 now = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
222 filename = f"{self.rgbd_metadata.serial_number}_pcd_{now}.ply"
223 # Convert colors to uint8 for compatibility
224 self.pcd_frame.point.colors = (self.pcd_frame.point.colors * 255).to(
225 o3d.core.Dtype.UInt8)
226 self.executor.submit(o3d.t.io.write_point_cloud,
227 filename,
228 self.pcd_frame,
229 write_ascii=False,
230 compressed=True,
231 print_progress=False)
232 self.status_message = f"Saving point cloud to {filename}."
233
234 def save_rgbd(self):
235 """Save current RGBD image pair."""
236 now = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
237 filename = f"{self.rgbd_metadata.serial_number}_color_{now}.jpg"
238 self.executor.submit(o3d.t.io.write_image, filename,
239 self.rgbd_frame.color)
240 filename = f"{self.rgbd_metadata.serial_number}_depth_{now}.png"
241 self.executor.submit(o3d.t.io.write_image, filename,
242 self.rgbd_frame.depth)
243 self.status_message = (
244 f"Saving RGBD images to {filename[:-3]}.{{jpg,png}}.")
245
246
247class PipelineView:
248 """Controls display and user interface. All methods must run in the main thread."""
249
250 def __init__(self, vfov=60, max_pcd_vertices=1 << 20, **callbacks):
251 """Initialize.
252
253 Args:
254 vfov (float): Vertical field of view for the 3D scene.
255 max_pcd_vertices (int): Maximum point clud verties for which memory
256 is allocated.
257 callbacks (dict of kwargs): Callbacks provided by the controller
258 for various operations.
259 """
260
261 self.vfov = vfov
262 self.max_pcd_vertices = max_pcd_vertices
263
264 gui.Application.instance.initialize()
265 self.window = gui.Application.instance.create_window(
266 "Open3D || Online RGBD Video Processing", 1280, 960)
267 # Called on window layout (eg: resize)
268 self.window.set_on_layout(self.on_layout)
269 self.window.set_on_close(callbacks['on_window_close'])
270
271 self.pcd_material = o3d.visualization.rendering.MaterialRecord()
272 self.pcd_material.shader = "defaultLit"
273 # Set n_pixels displayed for each 3D point, accounting for HiDPI scaling
274 self.pcd_material.point_size = int(4 * self.window.scaling)
275
276 # 3D scene
277 self.pcdview = gui.SceneWidget()
278 self.window.add_child(self.pcdview)
279 self.pcdview.enable_scene_caching(
280 True) # makes UI _much_ more responsive
281 self.pcdview.scene = rendering.Open3DScene(self.window.renderer)
282 self.pcdview.scene.set_background([1, 1, 1, 1]) # White background
283 self.pcdview.scene.set_lighting(
284 rendering.Open3DScene.LightingProfile.SOFT_SHADOWS, [0, -6, 0])
285 # Point cloud bounds, depends on the sensor range
286 self.pcd_bounds = o3d.geometry.AxisAlignedBoundingBox([-3, -3, 0],
287 [3, 3, 6])
288 self.camera_view() # Initially look from the camera
289 em = self.window.theme.font_size
290
291 # Options panel
292 self.panel = gui.Vert(em, gui.Margins(em, em, em, em))
293 self.panel.preferred_width = int(360 * self.window.scaling)
294 self.window.add_child(self.panel)
295 toggles = gui.Horiz(em)
296 self.panel.add_child(toggles)
297
298 toggle_capture = gui.ToggleSwitch("Capture / Play")
299 toggle_capture.is_on = False
300 toggle_capture.set_on_clicked(
301 callbacks['on_toggle_capture']) # callback
302 toggles.add_child(toggle_capture)
303
304 self.flag_normals = False
305 self.toggle_normals = gui.ToggleSwitch("Colors / Normals")
306 self.toggle_normals.is_on = False
307 self.toggle_normals.set_on_clicked(
308 callbacks['on_toggle_normals']) # callback
309 toggles.add_child(self.toggle_normals)
310
311 view_buttons = gui.Horiz(em)
312 self.panel.add_child(view_buttons)
313 view_buttons.add_stretch() # for centering
314 camera_view = gui.Button("Camera view")
315 camera_view.set_on_clicked(self.camera_view) # callback
316 view_buttons.add_child(camera_view)
317 birds_eye_view = gui.Button("Bird's eye view")
318 birds_eye_view.set_on_clicked(self.birds_eye_view) # callback
319 view_buttons.add_child(birds_eye_view)
320 view_buttons.add_stretch() # for centering
321
322 save_toggle = gui.Horiz(em)
323 self.panel.add_child(save_toggle)
324 save_toggle.add_child(gui.Label("Record / Save"))
325 self.toggle_record = None
326 if callbacks['on_toggle_record'] is not None:
327 save_toggle.add_fixed(1.5 * em)
328 self.toggle_record = gui.ToggleSwitch("Video")
329 self.toggle_record.is_on = False
330 self.toggle_record.set_on_clicked(callbacks['on_toggle_record'])
331 save_toggle.add_child(self.toggle_record)
332
333 save_buttons = gui.Horiz(em)
334 self.panel.add_child(save_buttons)
335 save_buttons.add_stretch() # for centering
336 save_pcd = gui.Button("Save Point cloud")
337 save_pcd.set_on_clicked(callbacks['on_save_pcd'])
338 save_buttons.add_child(save_pcd)
339 save_rgbd = gui.Button("Save RGBD frame")
340 save_rgbd.set_on_clicked(callbacks['on_save_rgbd'])
341 save_buttons.add_child(save_rgbd)
342 save_buttons.add_stretch() # for centering
343
344 self.video_size = (int(240 * self.window.scaling),
345 int(320 * self.window.scaling), 3)
346 self.show_color = gui.CollapsableVert("Color image")
347 self.show_color.set_is_open(False)
348 self.panel.add_child(self.show_color)
349 self.color_video = gui.ImageWidget(
350 o3d.geometry.Image(np.zeros(self.video_size, dtype=np.uint8)))
351 self.show_color.add_child(self.color_video)
352 self.show_depth = gui.CollapsableVert("Depth image")
353 self.show_depth.set_is_open(False)
354 self.panel.add_child(self.show_depth)
355 self.depth_video = gui.ImageWidget(
356 o3d.geometry.Image(np.zeros(self.video_size, dtype=np.uint8)))
357 self.show_depth.add_child(self.depth_video)
358
359 self.status_message = gui.Label("")
360 self.panel.add_child(self.status_message)
361
362 self.flag_exit = False
363 self.flag_gui_init = False
364
365 def update(self, frame_elements):
366 """Update visualization with point cloud and images. Must run in main
367 thread since this makes GUI calls.
368
369 Args:
370 frame_elements: dict {element_type: geometry element}.
371 Dictionary of element types to geometry elements to be updated
372 in the GUI:
373 'pcd': point cloud,
374 'color': rgb image (3 channel, uint8),
375 'depth': depth image (uint8),
376 'status_message': message
377 """
378 if not self.flag_gui_init:
379 # Set dummy point cloud to allocate graphics memory
380 dummy_pcd = o3d.t.geometry.PointCloud({
381 'positions':
382 o3d.core.Tensor.zeros((self.max_pcd_vertices, 3),
383 o3d.core.Dtype.Float32),
384 'colors':
385 o3d.core.Tensor.zeros((self.max_pcd_vertices, 3),
386 o3d.core.Dtype.Float32),
387 'normals':
388 o3d.core.Tensor.zeros((self.max_pcd_vertices, 3),
389 o3d.core.Dtype.Float32)
390 })
391 if self.pcdview.scene.has_geometry('pcd'):
392 self.pcdview.scene.remove_geometry('pcd')
393
394 self.pcd_material.shader = "normals" if self.flag_normals else "defaultLit"
395 self.pcdview.scene.add_geometry('pcd', dummy_pcd, self.pcd_material)
396 self.flag_gui_init = True
397
398 # TODO(ssheorey) Switch to update_geometry() after #3452 is fixed
399 if os.name == 'nt':
400 self.pcdview.scene.remove_geometry('pcd')
401 self.pcdview.scene.add_geometry('pcd', frame_elements['pcd'],
402 self.pcd_material)
403 else:
404 update_flags = (rendering.Scene.UPDATE_POINTS_FLAG |
405 rendering.Scene.UPDATE_COLORS_FLAG |
406 (rendering.Scene.UPDATE_NORMALS_FLAG
407 if self.flag_normals else 0))
408 self.pcdview.scene.scene.update_geometry('pcd',
409 frame_elements['pcd'],
410 update_flags)
411
412 # Update color and depth images
413 # TODO(ssheorey) Remove CPU transfer after we have CUDA -> OpenGL bridge
414 if self.show_color.get_is_open() and 'color' in frame_elements:
415 sampling_ratio = self.video_size[1] / frame_elements['color'].columns
416 self.color_video.update_image(
417 frame_elements['color'].resize(sampling_ratio).cpu())
418 if self.show_depth.get_is_open() and 'depth' in frame_elements:
419 sampling_ratio = self.video_size[1] / frame_elements['depth'].columns
420 self.depth_video.update_image(
421 frame_elements['depth'].resize(sampling_ratio).cpu())
422
423 if 'status_message' in frame_elements:
424 self.status_message.text = frame_elements["status_message"]
425
426 self.pcdview.force_redraw()
427
428 def camera_view(self):
429 """Callback to reset point cloud view to the camera"""
430 self.pcdview.setup_camera(self.vfov, self.pcd_bounds, [0, 0, 0])
431 # Look at [0, 0, 1] from camera placed at [0, 0, 0] with Y axis
432 # pointing at [0, -1, 0]
433 self.pcdview.scene.camera.look_at([0, 0, 1], [0, 0, 0], [0, -1, 0])
434
435 def birds_eye_view(self):
436 """Callback to reset point cloud view to birds eye (overhead) view"""
437 self.pcdview.setup_camera(self.vfov, self.pcd_bounds, [0, 0, 0])
438 self.pcdview.scene.camera.look_at([0, 0, 1.5], [0, 3, 1.5], [0, -1, 0])
439
440 def on_layout(self, layout_context):
441 # The on_layout callback should set the frame (position + size) of every
442 # child correctly. After the callback is done the window will layout
443 # the grandchildren.
444 """Callback on window initialize / resize"""
445 frame = self.window.content_rect
446 self.pcdview.frame = frame
447 panel_size = self.panel.calc_preferred_size(layout_context,
448 self.panel.Constraints())
449 self.panel.frame = gui.Rect(frame.get_right() - panel_size.width,
450 frame.y, panel_size.width,
451 panel_size.height)
452
453
454class PipelineController:
455 """Entry point for the app. Controls the PipelineModel object for IO and
456 processing and the PipelineView object for display and UI. All methods
457 operate on the main thread.
458 """
459
460 def __init__(self, camera_config_file=None, rgbd_video=None, device=None):
461 """Initialize.
462
463 Args:
464 camera_config_file (str): Camera configuration json file.
465 rgbd_video (str): RS bag file containing the RGBD video. If this is
466 provided, connected cameras are ignored.
467 device (str): Compute device (e.g.: 'cpu:0' or 'cuda:0').
468 """
469 self.pipeline_model = PipelineModel(self.update_view,
470 camera_config_file, rgbd_video,
471 device)
472
473 self.pipeline_view = PipelineView(
474 1.25 * self.pipeline_model.vfov,
475 self.pipeline_model.max_points,
476 on_window_close=self.on_window_close,
477 on_toggle_capture=self.on_toggle_capture,
478 on_save_pcd=self.on_save_pcd,
479 on_save_rgbd=self.on_save_rgbd,
480 on_toggle_record=self.on_toggle_record
481 if rgbd_video is None else None,
482 on_toggle_normals=self.on_toggle_normals)
483
484 threading.Thread(name='PipelineModel',
485 target=self.pipeline_model.run).start()
486 gui.Application.instance.run()
487
488 def update_view(self, frame_elements):
489 """Updates view with new data. May be called from any thread.
490
491 Args:
492 frame_elements (dict): Display elements (point cloud and images)
493 from the new frame to be shown.
494 """
495 gui.Application.instance.post_to_main_thread(
496 self.pipeline_view.window,
497 lambda: self.pipeline_view.update(frame_elements))
498
499 def on_toggle_capture(self, is_enabled):
500 """Callback to toggle capture."""
501 self.pipeline_model.flag_capture = is_enabled
502 if not is_enabled:
503 self.on_toggle_record(False)
504 if self.pipeline_view.toggle_record is not None:
505 self.pipeline_view.toggle_record.is_on = False
506 else:
507 with self.pipeline_model.cv_capture:
508 self.pipeline_model.cv_capture.notify()
509
510 def on_toggle_record(self, is_enabled):
511 """Callback to toggle recording RGBD video."""
512 self.pipeline_model.flag_record = is_enabled
513
514 def on_toggle_normals(self, is_enabled):
515 """Callback to toggle display of normals"""
516 self.pipeline_model.flag_normals = is_enabled
517 self.pipeline_view.flag_normals = is_enabled
518 self.pipeline_view.flag_gui_init = False
519
520 def on_window_close(self):
521 """Callback when the user closes the application window."""
522 self.pipeline_model.flag_exit = True
523 with self.pipeline_model.cv_capture:
524 self.pipeline_model.cv_capture.notify_all()
525 return True # OK to close window
526
527 def on_save_pcd(self):
528 """Callback to save current point cloud."""
529 self.pipeline_model.flag_save_pcd = True
530
531 def on_save_rgbd(self):
532 """Callback to save current RGBD image pair."""
533 self.pipeline_model.flag_save_rgbd = True
534
535
536if __name__ == "__main__":
537
538 log.basicConfig(level=log.INFO)
539 parser = argparse.ArgumentParser(
540 description=__doc__,
541 formatter_class=argparse.RawDescriptionHelpFormatter)
542 parser.add_argument('--camera-config',
543 help='RGBD camera configuration JSON file')
544 parser.add_argument('--rgbd-video', help='RGBD video file (RealSense bag)')
545 parser.add_argument('--device',
546 help='Device to run computations. e.g. cpu:0 or cuda:0 '
547 'Default is CUDA GPU if available, else CPU.')
548
549 args = parser.parse_args()
550 if args.camera_config and args.rgbd_video:
551 log.critical(
552 "Please provide only one of --camera-config and --rgbd-video arguments"
553 )
554 else:
555 PipelineController(args.camera_config, args.rgbd_video, args.device)
remove_geometry.py
27import open3d as o3d
28import numpy as np
29import time
30import copy
31
32
33def visualize_non_blocking(vis, pcds):
34 for pcd in pcds:
35 vis.update_geometry(pcd)
36 vis.poll_events()
37 vis.update_renderer()
38
39
40pcd_data = o3d.data.PCDPointCloud()
41pcd_orig = o3d.io.read_point_cloud(pcd_data.path)
42flip_transform = [[1, 0, 0, 0], [0, -1, 0, 0], [0, 0, -1, 0], [0, 0, 0, 1]]
43pcd_orig.transform(flip_transform)
44n_pcd = 5
45pcds = []
46for i in range(n_pcd):
47 pcds.append(copy.deepcopy(pcd_orig))
48 trans = np.identity(4)
49 trans[:3, 3] = [3 * i, 0, 0]
50 pcds[i].transform(trans)
51
52vis = o3d.visualization.Visualizer()
53vis.create_window()
54start_time = time.time()
55added = [False] * n_pcd
56
57curr_sec = int(time.time() - start_time)
58prev_sec = curr_sec - 1
59
60while True:
61 curr_sec = int(time.time() - start_time)
62 if curr_sec - prev_sec == 1:
63 prev_sec = curr_sec
64
65 for i in range(n_pcd):
66 if curr_sec % (n_pcd * 2) == i and not added[i]:
67 vis.add_geometry(pcds[i])
68 added[i] = True
69 print("Adding %d" % i)
70 if curr_sec % (n_pcd * 2) == (i + n_pcd) and added[i]:
71 vis.remove_geometry(pcds[i])
72 added[i] = False
73 print("Removing %d" % i)
74
75 visualize_non_blocking(vis, pcds)
render_to_image.py
27import open3d as o3d
28import open3d.visualization.rendering as rendering
29
30
31def main():
32 render = rendering.OffscreenRenderer(640, 480)
33
34 yellow = rendering.MaterialRecord()
35 yellow.base_color = [1.0, 0.75, 0.0, 1.0]
36 yellow.shader = "defaultLit"
37
38 green = rendering.MaterialRecord()
39 green.base_color = [0.0, 0.5, 0.0, 1.0]
40 green.shader = "defaultLit"
41
42 grey = rendering.MaterialRecord()
43 grey.base_color = [0.7, 0.7, 0.7, 1.0]
44 grey.shader = "defaultLit"
45
46 white = rendering.MaterialRecord()
47 white.base_color = [1.0, 1.0, 1.0, 1.0]
48 white.shader = "defaultLit"
49
50 cyl = o3d.geometry.TriangleMesh.create_cylinder(.05, 3)
51 cyl.compute_vertex_normals()
52 cyl.translate([-2, 0, 1.5])
53 sphere = o3d.geometry.TriangleMesh.create_sphere(.2)
54 sphere.compute_vertex_normals()
55 sphere.translate([-2, 0, 3])
56
57 box = o3d.geometry.TriangleMesh.create_box(2, 2, 1)
58 box.compute_vertex_normals()
59 box.translate([-1, -1, 0])
60 solid = o3d.geometry.TriangleMesh.create_icosahedron(0.5)
61 solid.compute_triangle_normals()
62 solid.compute_vertex_normals()
63 solid.translate([0, 0, 1.75])
64
65 render.scene.add_geometry("cyl", cyl, green)
66 render.scene.add_geometry("sphere", sphere, yellow)
67 render.scene.add_geometry("box", box, grey)
68 render.scene.add_geometry("solid", solid, white)
69 render.setup_camera(60.0, [0, 0, 0], [0, 10, 0], [0, 0, 1])
70 render.scene.scene.set_sun_light([0.707, 0.0, -.707], [1.0, 1.0, 1.0],
71 75000)
72 render.scene.scene.enable_sun_light(True)
73 render.scene.show_axes(True)
74
75 img = render.render_to_image()
76 print("Saving image at test.png")
77 o3d.io.write_image("test.png", img, 9)
78
79 render.setup_camera(60.0, [0, 0, 0], [-10, 0, 0], [0, 0, 1])
80 img = render.render_to_image()
81 print("Saving image at test2.png")
82 o3d.io.write_image("test2.png", img, 9)
83
84
85if __name__ == "__main__":
86 main()
tensorboard_pytorch.py
27import os
28import sys
29import numpy as np
30import open3d as o3d
31# pylint: disable-next=unused-import
32from open3d.visualization.tensorboard_plugin.util import to_dict_batch
33from torch.utils.tensorboard import SummaryWriter
34
35BASE_LOGDIR = "demo_logs/pytorch/"
36MODEL_DIR = os.path.join(
37 os.path.dirname(os.path.dirname(os.path.dirname(
38 os.path.realpath(__file__)))), "test_data", "monkey")
39
40
41def small_scale(run_name="small_scale"):
42 """Basic demo with cube and cylinder with normals and colors.
43 """
44 logdir = os.path.join(BASE_LOGDIR, run_name)
45 writer = SummaryWriter(logdir)
46
47 cube = o3d.geometry.TriangleMesh.create_box(1, 2, 4, create_uv_map=True)
48 cube.compute_vertex_normals()
49 cylinder = o3d.geometry.TriangleMesh.create_cylinder(radius=1.0,
50 height=2.0,
51 resolution=20,
52 split=4,
53 create_uv_map=True)
54 cylinder.compute_vertex_normals()
55 colors = [(1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)]
56 for step in range(3):
57 cube.paint_uniform_color(colors[step])
58 writer.add_3d('cube', to_dict_batch([cube]), step=step)
59 cylinder.paint_uniform_color(colors[step])
60 writer.add_3d('cylinder', to_dict_batch([cylinder]), step=step)
61
62
63def property_reference(run_name="property_reference"):
64 """Produces identical visualization to small_scale, but does not store
65 repeated properties of ``vertex_positions`` and ``vertex_normals``.
66 """
67 logdir = os.path.join(BASE_LOGDIR, run_name)
68 writer = SummaryWriter(logdir)
69
70 cube = o3d.geometry.TriangleMesh.create_box(1, 2, 4, create_uv_map=True)
71 cube.compute_vertex_normals()
72 cylinder = o3d.geometry.TriangleMesh.create_cylinder(radius=1.0,
73 height=2.0,
74 resolution=20,
75 split=4,
76 create_uv_map=True)
77 cylinder.compute_vertex_normals()
78 colors = [(1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)]
79 for step in range(3):
80 cube.paint_uniform_color(colors[step])
81 cube_summary = to_dict_batch([cube])
82 if step > 0:
83 cube_summary['vertex_positions'] = 0
84 cube_summary['vertex_normals'] = 0
85 writer.add_3d('cube', cube_summary, step=step)
86 cylinder.paint_uniform_color(colors[step])
87 cylinder_summary = to_dict_batch([cylinder])
88 if step > 0:
89 cylinder_summary['vertex_positions'] = 0
90 cylinder_summary['vertex_normals'] = 0
91 writer.add_3d('cylinder', cylinder_summary, step=step)
92
93
94def large_scale(n_steps=16,
95 batch_size=1,
96 base_resolution=200,
97 run_name="large_scale"):
98 """Generate a large scale summary. Geometry resolution increases linearly
99 with step. Each element in a batch is painted a different color.
100 """
101 logdir = os.path.join(BASE_LOGDIR, run_name)
102 writer = SummaryWriter(logdir)
103 colors = []
104 for k in range(batch_size):
105 t = k * np.pi / batch_size
106 colors.append(((1 + np.sin(t)) / 2, (1 + np.cos(t)) / 2, t / np.pi))
107 for step in range(n_steps):
108 resolution = base_resolution * (step + 1)
109 cylinder_list = []
110 mobius_list = []
111 cylinder = o3d.geometry.TriangleMesh.create_cylinder(
112 radius=1.0, height=2.0, resolution=resolution, split=4)
113 cylinder.compute_vertex_normals()
114 mobius = o3d.geometry.TriangleMesh.create_mobius(
115 length_split=int(3.5 * resolution),
116 width_split=int(0.75 * resolution),
117 twists=1,
118 raidus=1,
119 flatness=1,
120 width=1,
121 scale=1)
122 mobius.compute_vertex_normals()
123 for b in range(batch_size):
124 cylinder_list.append(copy.deepcopy(cylinder))
125 cylinder_list[b].paint_uniform_color(colors[b])
126 mobius_list.append(copy.deepcopy(mobius))
127 mobius_list[b].paint_uniform_color(colors[b])
128 writer.add_3d('cylinder',
129 to_dict_batch(cylinder_list),
130 step=step,
131 max_outputs=batch_size)
132 writer.add_3d('mobius',
133 to_dict_batch(mobius_list),
134 step=step,
135 max_outputs=batch_size)
136
137
138def with_material(model_dir=MODEL_DIR):
139 """Read an obj model from a directory and write as a TensorBoard summary.
140 """
141 model_name = os.path.basename(model_dir)
142 logdir = os.path.join(BASE_LOGDIR, model_name)
143 model_path = os.path.join(model_dir, model_name + ".obj")
144 model = o3d.t.geometry.TriangleMesh.from_legacy(
145 o3d.io.read_triangle_mesh(model_path))
146 summary_3d = {
147 "vertex_positions": model.vertex.positions,
148 "vertex_normals": model.vertex.normals,
149 "triangle_texture_uvs": model.triangle["texture_uvs"],
150 "triangle_indices": model.triangle.indices,
151 "material_name": "defaultLit"
152 }
153 names_to_o3dprop = {"ao": "ambient_occlusion"}
154
155 for texture in ("albedo", "normal", "ao", "metallic", "roughness"):
156 texture_file = os.path.join(model_dir, texture + ".png")
157 if os.path.exists(texture_file):
158 texture = names_to_o3dprop.get(texture, texture)
159 summary_3d.update({
160 ("material_texture_map_" + texture):
161 o3d.t.io.read_image(texture_file)
162 })
163 if texture == "metallic":
164 summary_3d.update(material_scalar_metallic=1.0)
165
166 writer = SummaryWriter(logdir)
167 writer.add_3d(model_name, summary_3d, step=0)
168
169
170def demo_scene():
171 """Write the demo_scene.py example showing rich PBR materials as a summary
172 """
173 import demo_scene
174 geoms = demo_scene.create_scene()
175 writer = SummaryWriter(os.path.join(BASE_LOGDIR, 'demo_scene'))
176 for geom_data in geoms:
177 geom = geom_data["geometry"]
178 summary_3d = {}
179 for key, tensor in geom.vertex.items():
180 summary_3d["vertex_" + key] = tensor
181 for key, tensor in geom.triangle.items():
182 summary_3d["triangle_" + key] = tensor
183 if geom.has_valid_material():
184 summary_3d["material_name"] = geom.material.material_name
185 for key, value in geom.material.scalar_properties.items():
186 summary_3d["material_scalar_" + key] = value
187 for key, value in geom.material.vector_properties.items():
188 summary_3d["material_vector_" + key] = value
189 for key, value in geom.material.texture_maps.items():
190 summary_3d["material_texture_map_" + key] = value
191 writer.add_3d(geom_data["name"], summary_3d, step=0)
192
193
194if __name__ == "__main__":
195
196 examples = ('small_scale', 'large_scale', 'property_reference',
197 'with_material', 'demo_scene')
198 selected = tuple(eg for eg in sys.argv[1:] if eg in examples)
199 if len(selected) == 0:
200 print(f'Usage: python {__file__} EXAMPLE...')
201 print(f' where EXAMPLE are from {examples}')
202 selected = ('property_reference', 'with_material')
203
204 for eg in selected:
205 locals()[eg]()
206
207 print(f"Run 'tensorboard --logdir {BASE_LOGDIR}' to visualize the 3D "
208 "summary.")
tensorboard_tensorflow.py
27import os
28import sys
29import numpy as np
30import open3d as o3d
31from open3d.visualization.tensorboard_plugin import summary
32from open3d.visualization.tensorboard_plugin.util import to_dict_batch
33import tensorflow as tf
34
35BASE_LOGDIR = "demo_logs/tf/"
36MODEL_DIR = os.path.join(
37 os.path.dirname(os.path.dirname(os.path.dirname(
38 os.path.realpath(__file__)))), "test_data", "monkey")
39
40
41def small_scale(run_name="small_scale"):
42 """Basic demo with cube and cylinder with normals and colors.
43 """
44 logdir = os.path.join(BASE_LOGDIR, run_name)
45 writer = tf.summary.create_file_writer(logdir)
46
47 cube = o3d.geometry.TriangleMesh.create_box(1, 2, 4, create_uv_map=True)
48 cube.compute_vertex_normals()
49 cylinder = o3d.geometry.TriangleMesh.create_cylinder(radius=1.0,
50 height=2.0,
51 resolution=20,
52 split=4,
53 create_uv_map=True)
54 cylinder.compute_vertex_normals()
55 colors = [(1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)]
56 with writer.as_default():
57 for step in range(3):
58 cube.paint_uniform_color(colors[step])
59 summary.add_3d('cube',
60 to_dict_batch([cube]),
61 step=step,
62 logdir=logdir)
63 cylinder.paint_uniform_color(colors[step])
64 summary.add_3d('cylinder',
65 to_dict_batch([cylinder]),
66 step=step,
67 logdir=logdir)
68
69
70def property_reference(run_name="property_reference"):
71 """Produces identical visualization to small_scale, but does not store
72 repeated properties of ``vertex_positions`` and ``vertex_normals``.
73 """
74 logdir = os.path.join(BASE_LOGDIR, run_name)
75 writer = tf.summary.create_file_writer(logdir)
76
77 cube = o3d.geometry.TriangleMesh.create_box(1, 2, 4, create_uv_map=True)
78 cube.compute_vertex_normals()
79 cylinder = o3d.geometry.TriangleMesh.create_cylinder(radius=1.0,
80 height=2.0,
81 resolution=20,
82 split=4,
83 create_uv_map=True)
84 cylinder.compute_vertex_normals()
85 colors = [(1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)]
86 with writer.as_default():
87 for step in range(3):
88 cube.paint_uniform_color(colors[step])
89 cube_summary = to_dict_batch([cube])
90 if step > 0:
91 cube_summary['vertex_positions'] = 0
92 cube_summary['vertex_normals'] = 0
93 summary.add_3d('cube', cube_summary, step=step, logdir=logdir)
94 cylinder.paint_uniform_color(colors[step])
95 cylinder_summary = to_dict_batch([cylinder])
96 if step > 0:
97 cylinder_summary['vertex_positions'] = 0
98 cylinder_summary['vertex_normals'] = 0
99 summary.add_3d('cylinder',
100 cylinder_summary,
101 step=step,
102 logdir=logdir)
103
104
105def large_scale(n_steps=16,
106 batch_size=1,
107 base_resolution=200,
108 run_name="large_scale"):
109 """Generate a large scale summary. Geometry resolution increases linearly
110 with step. Each element in a batch is painted a different color.
111 """
112 logdir = os.path.join(BASE_LOGDIR, run_name)
113 writer = tf.summary.create_file_writer(logdir)
114 colors = []
115 for k in range(batch_size):
116 t = k * np.pi / batch_size
117 colors.append(((1 + np.sin(t)) / 2, (1 + np.cos(t)) / 2, t / np.pi))
118 with writer.as_default():
119 for step in range(n_steps):
120 resolution = base_resolution * (step + 1)
121 cylinder_list = []
122 mobius_list = []
123 cylinder = o3d.geometry.TriangleMesh.create_cylinder(
124 radius=1.0, height=2.0, resolution=resolution, split=4)
125 cylinder.compute_vertex_normals()
126 mobius = o3d.geometry.TriangleMesh.create_mobius(
127 length_split=int(3.5 * resolution),
128 width_split=int(0.75 * resolution),
129 twists=1,
130 raidus=1,
131 flatness=1,
132 width=1,
133 scale=1)
134 mobius.compute_vertex_normals()
135 for b in range(batch_size):
136 cylinder_list.append(copy.deepcopy(cylinder))
137 cylinder_list[b].paint_uniform_color(colors[b])
138 mobius_list.append(copy.deepcopy(mobius))
139 mobius_list[b].paint_uniform_color(colors[b])
140 summary.add_3d('cylinder',
141 to_dict_batch(cylinder_list),
142 step=step,
143 logdir=logdir,
144 max_outputs=batch_size)
145 summary.add_3d('mobius',
146 to_dict_batch(mobius_list),
147 step=step,
148 logdir=logdir,
149 max_outputs=batch_size)
150
151
152def with_material(model_dir=MODEL_DIR):
153 """Read an obj model from a directory and write as a TensorBoard summary.
154 """
155 model_name = os.path.basename(model_dir)
156 logdir = os.path.join(BASE_LOGDIR, model_name)
157 model_path = os.path.join(model_dir, model_name + ".obj")
158 model = o3d.t.geometry.TriangleMesh.from_legacy(
159 o3d.io.read_triangle_mesh(model_path))
160 summary_3d = {
161 "vertex_positions": model.vertex.positions,
162 "vertex_normals": model.vertex.normals,
163 "triangle_texture_uvs": model.triangle["texture_uvs"],
164 "triangle_indices": model.triangle.indices,
165 "material_name": "defaultLit"
166 }
167 names_to_o3dprop = {"ao": "ambient_occlusion"}
168
169 for texture in ("albedo", "normal", "ao", "metallic", "roughness"):
170 texture_file = os.path.join(model_dir, texture + ".png")
171 if os.path.exists(texture_file):
172 texture = names_to_o3dprop.get(texture, texture)
173 summary_3d.update({
174 ("material_texture_map_" + texture):
175 o3d.t.io.read_image(texture_file)
176 })
177 if texture == "metallic":
178 summary_3d.update(material_scalar_metallic=1.0)
179
180 writer = tf.summary.create_file_writer(logdir)
181 with writer.as_default():
182 summary.add_3d(model_name, summary_3d, step=0, logdir=logdir)
183
184
185def demo_scene():
186 """Write the demo_scene.py example showing rich PBR materials as a summary.
187 """
188 import demo_scene
189 geoms = demo_scene.create_scene()
190 logdir = os.path.join(BASE_LOGDIR, 'demo_scene')
191 writer = tf.summary.create_file_writer(logdir)
192 for geom_data in geoms:
193 geom = geom_data["geometry"]
194 summary_3d = {}
195 for key, tensor in geom.vertex.items():
196 summary_3d["vertex_" + key] = tensor
197 for key, tensor in geom.triangle.items():
198 summary_3d["triangle_" + key] = tensor
199 if geom.has_valid_material():
200 summary_3d["material_name"] = geom.material.material_name
201 for key, value in geom.material.scalar_properties.items():
202 summary_3d["material_scalar_" + key] = value
203 for key, value in geom.material.vector_properties.items():
204 summary_3d["material_vector_" + key] = value
205 for key, value in geom.material.texture_maps.items():
206 summary_3d["material_texture_map_" + key] = value
207 with writer.as_default():
208 summary.add_3d(geom_data["name"], summary_3d, step=0, logdir=logdir)
209
210
211if __name__ == "__main__":
212
213 examples = ('small_scale', 'large_scale', 'property_reference',
214 'with_material', 'demo_scene')
215 selected = tuple(eg for eg in sys.argv[1:] if eg in examples)
216 if len(selected) == 0:
217 print(f'Usage: python {__file__} EXAMPLE...')
218 print(f' where EXAMPLE are from {examples}')
219 selected = ('property_reference', 'with_material')
220
221 for eg in selected:
222 locals()[eg]()
223
224 print(f"Run 'tensorboard --logdir {BASE_LOGDIR}' to visualize the 3D "
225 "summary.")
text3d.py
27import numpy as np
28import open3d as o3d
29import open3d.visualization.gui as gui
30import open3d.visualization.rendering as rendering
31
32
33def make_point_cloud(npts, center, radius):
34 pts = np.random.uniform(-radius, radius, size=[npts, 3]) + center
35 cloud = o3d.geometry.PointCloud()
36 cloud.points = o3d.utility.Vector3dVector(pts)
37 colors = np.random.uniform(0.0, 1.0, size=[npts, 3])
38 cloud.colors = o3d.utility.Vector3dVector(colors)
39 return cloud
40
41
42def high_level():
43 app = gui.Application.instance
44 app.initialize()
45
46 points = make_point_cloud(100, (0, 0, 0), 1.0)
47
48 vis = o3d.visualization.O3DVisualizer("Open3D - 3D Text", 1024, 768)
49 vis.show_settings = True
50 vis.add_geometry("Points", points)
51 for idx in range(0, len(points.points)):
52 vis.add_3d_label(points.points[idx], "{}".format(idx))
53 vis.reset_camera_to_default()
54
55 app.add_window(vis)
56 app.run()
57
58
59def low_level():
60 app = gui.Application.instance
61 app.initialize()
62
63 points = make_point_cloud(100, (0, 0, 0), 1.0)
64
65 w = app.create_window("Open3D - 3D Text", 1024, 768)
66 widget3d = gui.SceneWidget()
67 widget3d.scene = rendering.Open3DScene(w.renderer)
68 mat = rendering.MaterialRecord()
69 mat.shader = "defaultUnlit"
70 mat.point_size = 5 * w.scaling
71 widget3d.scene.add_geometry("Points", points, mat)
72 for idx in range(0, len(points.points)):
73 l = widget3d.add_3d_label(points.points[idx], "{}".format(idx))
74 l.color = gui.Color(points.colors[idx][0], points.colors[idx][1],
75 points.colors[idx][2])
76 l.scale = np.random.uniform(0.5, 3.0)
77 bbox = widget3d.scene.bounding_box
78 widget3d.setup_camera(60.0, bbox, bbox.get_center())
79 w.add_child(widget3d)
80
81 app.run()
82
83
84if __name__ == "__main__":
85 high_level()
86 low_level()
textured_mesh.py
27import sys
28import os
29import open3d as o3d
30
31
32def main():
33 if len(sys.argv) < 2:
34 print("""Usage: textured-mesh.py [model directory]
35 This example will load [model directory].obj plus any of albedo, normal,
36 ao, metallic and roughness textures present. The textures should be named
37 albedo.png, normal.png, ao.png, metallic.png and roughness.png
38 respectively.""")
39 sys.exit()
40
41 model_dir = os.path.normpath(os.path.realpath(sys.argv[1]))
42 model_name = os.path.join(model_dir, os.path.basename(model_dir) + ".obj")
43 mesh = o3d.t.geometry.TriangleMesh.from_legacy(
44 o3d.io.read_triangle_mesh(model_name))
45 material = mesh.material
46 material.material_name = "defaultLit"
47
48 names_to_o3dprop = {"ao": "ambient_occlusion"}
49 for texture in ("albedo", "normal", "ao", "metallic", "roughness"):
50 texture_file = os.path.join(model_dir, texture + ".png")
51 if os.path.exists(texture_file):
52 texture = names_to_o3dprop.get(texture, texture)
53 material.texture_maps[texture] = o3d.t.io.read_image(texture_file)
54 if "metallic" in material.texture_maps:
55 material.scalar_properties["metallic"] = 1.0
56
57 o3d.visualization.draw(mesh, title=model_name)
58
59
60if __name__ == "__main__":
61 main()
textured_model.py
27import sys
28import os
29import open3d as o3d
30
31
32def main():
33 if len(sys.argv) < 2:
34 print("""Usage: texture-model.py [model directory]
35 This example will load [model directory].obj plus any of albedo, normal,
36 ao, metallic and roughness textures present.""")
37 sys.exit()
38
39 model_dir = sys.argv[1]
40 model_name = os.path.join(model_dir, os.path.basename(model_dir) + ".obj")
41 model = o3d.io.read_triangle_mesh(model_name)
42 material = o3d.visualization.rendering.MaterialRecord()
43 material.shader = "defaultLit"
44
45 albedo_name = os.path.join(model_dir, "albedo.png")
46 normal_name = os.path.join(model_dir, "normal.png")
47 ao_name = os.path.join(model_dir, "ao.png")
48 metallic_name = os.path.join(model_dir, "metallic.png")
49 roughness_name = os.path.join(model_dir, "roughness.png")
50 if os.path.exists(albedo_name):
51 material.albedo_img = o3d.io.read_image(albedo_name)
52 if os.path.exists(normal_name):
53 material.normal_img = o3d.io.read_image(normal_name)
54 if os.path.exists(ao_name):
55 material.ao_img = o3d.io.read_image(ao_name)
56 if os.path.exists(metallic_name):
57 material.base_metallic = 1.0
58 material.metallic_img = o3d.io.read_image(metallic_name)
59 if os.path.exists(roughness_name):
60 material.roughness_img = o3d.io.read_image(roughness_name)
61
62 o3d.visualization.draw([{
63 "name": "cube",
64 "geometry": model,
65 "material": material
66 }])
67
68
69if __name__ == "__main__":
70 main()
video.py
27import numpy as np
28import open3d as o3d
29import open3d.visualization.gui as gui
30import open3d.visualization.rendering as rendering
31import time
32import threading
33
34
35def rescale_greyscale(img):
36 data = np.asarray(img)
37 assert (len(data.shape) == 2) # requires 1 channel image
38 dataFloat = data.astype(np.float64)
39 max_val = dataFloat.max()
40 # We don't currently support 16-bit images, so convert to 8-bit
41 dataFloat *= 255.0 / max_val
42 data8 = dataFloat.astype(np.uint8)
43 return o3d.geometry.Image(data8)
44
45
46class VideoWindow:
47
48 def __init__(self):
49 self.rgb_images = []
50 rgbd_data = o3d.data.SampleRedwoodRGBDImages()
51 for path in rgbd_data.color_paths:
52 img = o3d.io.read_image(path)
53 self.rgb_images.append(img)
54 self.depth_images = []
55 for path in rgbd_data.depth_paths:
56 img = o3d.io.read_image(path)
57 # The images are pretty dark, so rescale them so that it is
58 # obvious that this is a depth image, for the sake of the example
59 img = rescale_greyscale(img)
60 self.depth_images.append(img)
61 assert (len(self.rgb_images) == len(self.depth_images))
62
63 self.window = gui.Application.instance.create_window(
64 "Open3D - Video Example", 1000, 500)
65 self.window.set_on_layout(self._on_layout)
66 self.window.set_on_close(self._on_close)
67
68 self.widget3d = gui.SceneWidget()
69 self.widget3d.scene = rendering.Open3DScene(self.window.renderer)
70 self.window.add_child(self.widget3d)
71
72 lit = rendering.MaterialRecord()
73 lit.shader = "defaultLit"
74 tet = o3d.geometry.TriangleMesh.create_tetrahedron()
75 tet.compute_vertex_normals()
76 tet.paint_uniform_color([0.5, 0.75, 1.0])
77 self.widget3d.scene.add_geometry("tetrahedron", tet, lit)
78 bounds = self.widget3d.scene.bounding_box
79 self.widget3d.setup_camera(60.0, bounds, bounds.get_center())
80 self.widget3d.scene.show_axes(True)
81
82 em = self.window.theme.font_size
83 margin = 0.5 * em
84 self.panel = gui.Vert(0.5 * em, gui.Margins(margin))
85 self.panel.add_child(gui.Label("Color image"))
86 self.rgb_widget = gui.ImageWidget(self.rgb_images[0])
87 self.panel.add_child(self.rgb_widget)
88 self.panel.add_child(gui.Label("Depth image (normalized)"))
89 self.depth_widget = gui.ImageWidget(self.depth_images[0])
90 self.panel.add_child(self.depth_widget)
91 self.window.add_child(self.panel)
92
93 self.is_done = False
94 threading.Thread(target=self._update_thread).start()
95
96 def _on_layout(self, layout_context):
97 contentRect = self.window.content_rect
98 panel_width = 15 * layout_context.theme.font_size # 15 ems wide
99 self.widget3d.frame = gui.Rect(contentRect.x, contentRect.y,
100 contentRect.width - panel_width,
101 contentRect.height)
102 self.panel.frame = gui.Rect(self.widget3d.frame.get_right(),
103 contentRect.y, panel_width,
104 contentRect.height)
105
106 def _on_close(self):
107 self.is_done = True
108 return True # False would cancel the close
109
110 def _update_thread(self):
111 # This is NOT the UI thread, need to call post_to_main_thread() to update
112 # the scene or any part of the UI.
113 idx = 0
114 while not self.is_done:
115 time.sleep(0.100)
116
117 # Get the next frame, for instance, reading a frame from the camera.
118 rgb_frame = self.rgb_images[idx]
119 depth_frame = self.depth_images[idx]
120 idx += 1
121 if idx >= len(self.rgb_images):
122 idx = 0
123
124 # Update the images. This must be done on the UI thread.
125 def update():
126 self.rgb_widget.update_image(rgb_frame)
127 self.depth_widget.update_image(depth_frame)
128 self.widget3d.scene.set_background([1, 1, 1, 1], rgb_frame)
129
130 if not self.is_done:
131 gui.Application.instance.post_to_main_thread(
132 self.window, update)
133
134
135def main():
136 app = o3d.visualization.gui.Application.instance
137 app.initialize()
138
139 win = VideoWindow()
140
141 app.run()
142
143
144if __name__ == "__main__":
145 main()
vis_gui.py
27import glob
28import numpy as np
29import open3d as o3d
30import open3d.visualization.gui as gui
31import open3d.visualization.rendering as rendering
32import os
33import platform
34import sys
35
36isMacOS = (platform.system() == "Darwin")
37
38
39class Settings:
40 UNLIT = "defaultUnlit"
41 LIT = "defaultLit"
42 NORMALS = "normals"
43 DEPTH = "depth"
44
45 DEFAULT_PROFILE_NAME = "Bright day with sun at +Y [default]"
46 POINT_CLOUD_PROFILE_NAME = "Cloudy day (no direct sun)"
47 CUSTOM_PROFILE_NAME = "Custom"
48 LIGHTING_PROFILES = {
49 DEFAULT_PROFILE_NAME: {
50 "ibl_intensity": 45000,
51 "sun_intensity": 45000,
52 "sun_dir": [0.577, -0.577, -0.577],
53 # "ibl_rotation":
54 "use_ibl": True,
55 "use_sun": True,
56 },
57 "Bright day with sun at -Y": {
58 "ibl_intensity": 45000,
59 "sun_intensity": 45000,
60 "sun_dir": [0.577, 0.577, 0.577],
61 # "ibl_rotation":
62 "use_ibl": True,
63 "use_sun": True,
64 },
65 "Bright day with sun at +Z": {
66 "ibl_intensity": 45000,
67 "sun_intensity": 45000,
68 "sun_dir": [0.577, 0.577, -0.577],
69 # "ibl_rotation":
70 "use_ibl": True,
71 "use_sun": True,
72 },
73 "Less Bright day with sun at +Y": {
74 "ibl_intensity": 35000,
75 "sun_intensity": 50000,
76 "sun_dir": [0.577, -0.577, -0.577],
77 # "ibl_rotation":
78 "use_ibl": True,
79 "use_sun": True,
80 },
81 "Less Bright day with sun at -Y": {
82 "ibl_intensity": 35000,
83 "sun_intensity": 50000,
84 "sun_dir": [0.577, 0.577, 0.577],
85 # "ibl_rotation":
86 "use_ibl": True,
87 "use_sun": True,
88 },
89 "Less Bright day with sun at +Z": {
90 "ibl_intensity": 35000,
91 "sun_intensity": 50000,
92 "sun_dir": [0.577, 0.577, -0.577],
93 # "ibl_rotation":
94 "use_ibl": True,
95 "use_sun": True,
96 },
97 POINT_CLOUD_PROFILE_NAME: {
98 "ibl_intensity": 60000,
99 "sun_intensity": 50000,
100 "use_ibl": True,
101 "use_sun": False,
102 # "ibl_rotation":
103 },
104 }
105
106 DEFAULT_MATERIAL_NAME = "Polished ceramic [default]"
107 PREFAB = {
108 DEFAULT_MATERIAL_NAME: {
109 "metallic": 0.0,
110 "roughness": 0.7,
111 "reflectance": 0.5,
112 "clearcoat": 0.2,
113 "clearcoat_roughness": 0.2,
114 "anisotropy": 0.0
115 },
116 "Metal (rougher)": {
117 "metallic": 1.0,
118 "roughness": 0.5,
119 "reflectance": 0.9,
120 "clearcoat": 0.0,
121 "clearcoat_roughness": 0.0,
122 "anisotropy": 0.0
123 },
124 "Metal (smoother)": {
125 "metallic": 1.0,
126 "roughness": 0.3,
127 "reflectance": 0.9,
128 "clearcoat": 0.0,
129 "clearcoat_roughness": 0.0,
130 "anisotropy": 0.0
131 },
132 "Plastic": {
133 "metallic": 0.0,
134 "roughness": 0.5,
135 "reflectance": 0.5,
136 "clearcoat": 0.5,
137 "clearcoat_roughness": 0.2,
138 "anisotropy": 0.0
139 },
140 "Glazed ceramic": {
141 "metallic": 0.0,
142 "roughness": 0.5,
143 "reflectance": 0.9,
144 "clearcoat": 1.0,
145 "clearcoat_roughness": 0.1,
146 "anisotropy": 0.0
147 },
148 "Clay": {
149 "metallic": 0.0,
150 "roughness": 1.0,
151 "reflectance": 0.5,
152 "clearcoat": 0.1,
153 "clearcoat_roughness": 0.287,
154 "anisotropy": 0.0
155 },
156 }
157
158 def __init__(self):
159 self.mouse_model = gui.SceneWidget.Controls.ROTATE_CAMERA
160 self.bg_color = gui.Color(1, 1, 1)
161 self.show_skybox = False
162 self.show_axes = False
163 self.use_ibl = True
164 self.use_sun = True
165 self.new_ibl_name = None # clear to None after loading
166 self.ibl_intensity = 45000
167 self.sun_intensity = 45000
168 self.sun_dir = [0.577, -0.577, -0.577]
169 self.sun_color = gui.Color(1, 1, 1)
170
171 self.apply_material = True # clear to False after processing
172 self._materials = {
173 Settings.LIT: rendering.MaterialRecord(),
174 Settings.UNLIT: rendering.MaterialRecord(),
175 Settings.NORMALS: rendering.MaterialRecord(),
176 Settings.DEPTH: rendering.MaterialRecord()
177 }
178 self._materials[Settings.LIT].base_color = [0.9, 0.9, 0.9, 1.0]
179 self._materials[Settings.LIT].shader = Settings.LIT
180 self._materials[Settings.UNLIT].base_color = [0.9, 0.9, 0.9, 1.0]
181 self._materials[Settings.UNLIT].shader = Settings.UNLIT
182 self._materials[Settings.NORMALS].shader = Settings.NORMALS
183 self._materials[Settings.DEPTH].shader = Settings.DEPTH
184
185 # Conveniently, assigning from self._materials[...] assigns a reference,
186 # not a copy, so if we change the property of a material, then switch
187 # to another one, then come back, the old setting will still be there.
188 self.material = self._materials[Settings.LIT]
189
190 def set_material(self, name):
191 self.material = self._materials[name]
192 self.apply_material = True
193
194 def apply_material_prefab(self, name):
195 assert (self.material.shader == Settings.LIT)
196 prefab = Settings.PREFAB[name]
197 for key, val in prefab.items():
198 setattr(self.material, "base_" + key, val)
199
200 def apply_lighting_profile(self, name):
201 profile = Settings.LIGHTING_PROFILES[name]
202 for key, val in profile.items():
203 setattr(self, key, val)
204
205
206class AppWindow:
207 MENU_OPEN = 1
208 MENU_EXPORT = 2
209 MENU_QUIT = 3
210 MENU_SHOW_SETTINGS = 11
211 MENU_ABOUT = 21
212
213 DEFAULT_IBL = "default"
214
215 MATERIAL_NAMES = ["Lit", "Unlit", "Normals", "Depth"]
216 MATERIAL_SHADERS = [
217 Settings.LIT, Settings.UNLIT, Settings.NORMALS, Settings.DEPTH
218 ]
219
220 def __init__(self, width, height):
221 self.settings = Settings()
222 resource_path = gui.Application.instance.resource_path
223 self.settings.new_ibl_name = resource_path + "/" + AppWindow.DEFAULT_IBL
224
225 self.window = gui.Application.instance.create_window(
226 "Open3D", width, height)
227 w = self.window # to make the code more concise
228
229 # 3D widget
230 self._scene = gui.SceneWidget()
231 self._scene.scene = rendering.Open3DScene(w.renderer)
232 self._scene.set_on_sun_direction_changed(self._on_sun_dir)
233
234 # ---- Settings panel ----
235 # Rather than specifying sizes in pixels, which may vary in size based
236 # on the monitor, especially on macOS which has 220 dpi monitors, use
237 # the em-size. This way sizings will be proportional to the font size,
238 # which will create a more visually consistent size across platforms.
239 em = w.theme.font_size
240 separation_height = int(round(0.5 * em))
241
242 # Widgets are laid out in layouts: gui.Horiz, gui.Vert,
243 # gui.CollapsableVert, and gui.VGrid. By nesting the layouts we can
244 # achieve complex designs. Usually we use a vertical layout as the
245 # topmost widget, since widgets tend to be organized from top to bottom.
246 # Within that, we usually have a series of horizontal layouts for each
247 # row. All layouts take a spacing parameter, which is the spacing
248 # between items in the widget, and a margins parameter, which specifies
249 # the spacing of the left, top, right, bottom margins. (This acts like
250 # the 'padding' property in CSS.)
251 self._settings_panel = gui.Vert(
252 0, gui.Margins(0.25 * em, 0.25 * em, 0.25 * em, 0.25 * em))
253
254 # Create a collapsible vertical widget, which takes up enough vertical
255 # space for all its children when open, but only enough for text when
256 # closed. This is useful for property pages, so the user can hide sets
257 # of properties they rarely use.
258 view_ctrls = gui.CollapsableVert("View controls", 0.25 * em,
259 gui.Margins(em, 0, 0, 0))
260
261 self._arcball_button = gui.Button("Arcball")
262 self._arcball_button.horizontal_padding_em = 0.5
263 self._arcball_button.vertical_padding_em = 0
264 self._arcball_button.set_on_clicked(self._set_mouse_mode_rotate)
265 self._fly_button = gui.Button("Fly")
266 self._fly_button.horizontal_padding_em = 0.5
267 self._fly_button.vertical_padding_em = 0
268 self._fly_button.set_on_clicked(self._set_mouse_mode_fly)
269 self._model_button = gui.Button("Model")
270 self._model_button.horizontal_padding_em = 0.5
271 self._model_button.vertical_padding_em = 0
272 self._model_button.set_on_clicked(self._set_mouse_mode_model)
273 self._sun_button = gui.Button("Sun")
274 self._sun_button.horizontal_padding_em = 0.5
275 self._sun_button.vertical_padding_em = 0
276 self._sun_button.set_on_clicked(self._set_mouse_mode_sun)
277 self._ibl_button = gui.Button("Environment")
278 self._ibl_button.horizontal_padding_em = 0.5
279 self._ibl_button.vertical_padding_em = 0
280 self._ibl_button.set_on_clicked(self._set_mouse_mode_ibl)
281 view_ctrls.add_child(gui.Label("Mouse controls"))
282 # We want two rows of buttons, so make two horizontal layouts. We also
283 # want the buttons centered, which we can do be putting a stretch item
284 # as the first and last item. Stretch items take up as much space as
285 # possible, and since there are two, they will each take half the extra
286 # space, thus centering the buttons.
287 h = gui.Horiz(0.25 * em) # row 1
288 h.add_stretch()
289 h.add_child(self._arcball_button)
290 h.add_child(self._fly_button)
291 h.add_child(self._model_button)
292 h.add_stretch()
293 view_ctrls.add_child(h)
294 h = gui.Horiz(0.25 * em) # row 2
295 h.add_stretch()
296 h.add_child(self._sun_button)
297 h.add_child(self._ibl_button)
298 h.add_stretch()
299 view_ctrls.add_child(h)
300
301 self._show_skybox = gui.Checkbox("Show skymap")
302 self._show_skybox.set_on_checked(self._on_show_skybox)
303 view_ctrls.add_fixed(separation_height)
304 view_ctrls.add_child(self._show_skybox)
305
306 self._bg_color = gui.ColorEdit()
307 self._bg_color.set_on_value_changed(self._on_bg_color)
308
309 grid = gui.VGrid(2, 0.25 * em)
310 grid.add_child(gui.Label("BG Color"))
311 grid.add_child(self._bg_color)
312 view_ctrls.add_child(grid)
313
314 self._show_axes = gui.Checkbox("Show axes")
315 self._show_axes.set_on_checked(self._on_show_axes)
316 view_ctrls.add_fixed(separation_height)
317 view_ctrls.add_child(self._show_axes)
318
319 self._profiles = gui.Combobox()
320 for name in sorted(Settings.LIGHTING_PROFILES.keys()):
321 self._profiles.add_item(name)
322 self._profiles.add_item(Settings.CUSTOM_PROFILE_NAME)
323 self._profiles.set_on_selection_changed(self._on_lighting_profile)
324 view_ctrls.add_fixed(separation_height)
325 view_ctrls.add_child(gui.Label("Lighting profiles"))
326 view_ctrls.add_child(self._profiles)
327 self._settings_panel.add_fixed(separation_height)
328 self._settings_panel.add_child(view_ctrls)
329
330 advanced = gui.CollapsableVert("Advanced lighting", 0,
331 gui.Margins(em, 0, 0, 0))
332 advanced.set_is_open(False)
333
334 self._use_ibl = gui.Checkbox("HDR map")
335 self._use_ibl.set_on_checked(self._on_use_ibl)
336 self._use_sun = gui.Checkbox("Sun")
337 self._use_sun.set_on_checked(self._on_use_sun)
338 advanced.add_child(gui.Label("Light sources"))
339 h = gui.Horiz(em)
340 h.add_child(self._use_ibl)
341 h.add_child(self._use_sun)
342 advanced.add_child(h)
343
344 self._ibl_map = gui.Combobox()
345 for ibl in glob.glob(gui.Application.instance.resource_path +
346 "/*_ibl.ktx"):
347
348 self._ibl_map.add_item(os.path.basename(ibl[:-8]))
349 self._ibl_map.selected_text = AppWindow.DEFAULT_IBL
350 self._ibl_map.set_on_selection_changed(self._on_new_ibl)
351 self._ibl_intensity = gui.Slider(gui.Slider.INT)
352 self._ibl_intensity.set_limits(0, 200000)
353 self._ibl_intensity.set_on_value_changed(self._on_ibl_intensity)
354 grid = gui.VGrid(2, 0.25 * em)
355 grid.add_child(gui.Label("HDR map"))
356 grid.add_child(self._ibl_map)
357 grid.add_child(gui.Label("Intensity"))
358 grid.add_child(self._ibl_intensity)
359 advanced.add_fixed(separation_height)
360 advanced.add_child(gui.Label("Environment"))
361 advanced.add_child(grid)
362
363 self._sun_intensity = gui.Slider(gui.Slider.INT)
364 self._sun_intensity.set_limits(0, 200000)
365 self._sun_intensity.set_on_value_changed(self._on_sun_intensity)
366 self._sun_dir = gui.VectorEdit()
367 self._sun_dir.set_on_value_changed(self._on_sun_dir)
368 self._sun_color = gui.ColorEdit()
369 self._sun_color.set_on_value_changed(self._on_sun_color)
370 grid = gui.VGrid(2, 0.25 * em)
371 grid.add_child(gui.Label("Intensity"))
372 grid.add_child(self._sun_intensity)
373 grid.add_child(gui.Label("Direction"))
374 grid.add_child(self._sun_dir)
375 grid.add_child(gui.Label("Color"))
376 grid.add_child(self._sun_color)
377 advanced.add_fixed(separation_height)
378 advanced.add_child(gui.Label("Sun (Directional light)"))
379 advanced.add_child(grid)
380
381 self._settings_panel.add_fixed(separation_height)
382 self._settings_panel.add_child(advanced)
383
384 material_settings = gui.CollapsableVert("Material settings", 0,
385 gui.Margins(em, 0, 0, 0))
386
387 self._shader = gui.Combobox()
388 self._shader.add_item(AppWindow.MATERIAL_NAMES[0])
389 self._shader.add_item(AppWindow.MATERIAL_NAMES[1])
390 self._shader.add_item(AppWindow.MATERIAL_NAMES[2])
391 self._shader.add_item(AppWindow.MATERIAL_NAMES[3])
392 self._shader.set_on_selection_changed(self._on_shader)
393 self._material_prefab = gui.Combobox()
394 for prefab_name in sorted(Settings.PREFAB.keys()):
395 self._material_prefab.add_item(prefab_name)
396 self._material_prefab.selected_text = Settings.DEFAULT_MATERIAL_NAME
397 self._material_prefab.set_on_selection_changed(self._on_material_prefab)
398 self._material_color = gui.ColorEdit()
399 self._material_color.set_on_value_changed(self._on_material_color)
400 self._point_size = gui.Slider(gui.Slider.INT)
401 self._point_size.set_limits(1, 10)
402 self._point_size.set_on_value_changed(self._on_point_size)
403
404 grid = gui.VGrid(2, 0.25 * em)
405 grid.add_child(gui.Label("Type"))
406 grid.add_child(self._shader)
407 grid.add_child(gui.Label("Material"))
408 grid.add_child(self._material_prefab)
409 grid.add_child(gui.Label("Color"))
410 grid.add_child(self._material_color)
411 grid.add_child(gui.Label("Point size"))
412 grid.add_child(self._point_size)
413 material_settings.add_child(grid)
414
415 self._settings_panel.add_fixed(separation_height)
416 self._settings_panel.add_child(material_settings)
417 # ----
418
419 # Normally our user interface can be children of all one layout (usually
420 # a vertical layout), which is then the only child of the window. In our
421 # case we want the scene to take up all the space and the settings panel
422 # to go above it. We can do this custom layout by providing an on_layout
423 # callback. The on_layout callback should set the frame
424 # (position + size) of every child correctly. After the callback is
425 # done the window will layout the grandchildren.
426 w.set_on_layout(self._on_layout)
427 w.add_child(self._scene)
428 w.add_child(self._settings_panel)
429
430 # ---- Menu ----
431 # The menu is global (because the macOS menu is global), so only create
432 # it once, no matter how many windows are created
433 if gui.Application.instance.menubar is None:
434 if isMacOS:
435 app_menu = gui.Menu()
436 app_menu.add_item("About", AppWindow.MENU_ABOUT)
437 app_menu.add_separator()
438 app_menu.add_item("Quit", AppWindow.MENU_QUIT)
439 file_menu = gui.Menu()
440 file_menu.add_item("Open...", AppWindow.MENU_OPEN)
441 file_menu.add_item("Export Current Image...", AppWindow.MENU_EXPORT)
442 if not isMacOS:
443 file_menu.add_separator()
444 file_menu.add_item("Quit", AppWindow.MENU_QUIT)
445 settings_menu = gui.Menu()
446 settings_menu.add_item("Lighting & Materials",
447 AppWindow.MENU_SHOW_SETTINGS)
448 settings_menu.set_checked(AppWindow.MENU_SHOW_SETTINGS, True)
449 help_menu = gui.Menu()
450 help_menu.add_item("About", AppWindow.MENU_ABOUT)
451
452 menu = gui.Menu()
453 if isMacOS:
454 # macOS will name the first menu item for the running application
455 # (in our case, probably "Python"), regardless of what we call
456 # it. This is the application menu, and it is where the
457 # About..., Preferences..., and Quit menu items typically go.
458 menu.add_menu("Example", app_menu)
459 menu.add_menu("File", file_menu)
460 menu.add_menu("Settings", settings_menu)
461 # Don't include help menu unless it has something more than
462 # About...
463 else:
464 menu.add_menu("File", file_menu)
465 menu.add_menu("Settings", settings_menu)
466 menu.add_menu("Help", help_menu)
467 gui.Application.instance.menubar = menu
468
469 # The menubar is global, but we need to connect the menu items to the
470 # window, so that the window can call the appropriate function when the
471 # menu item is activated.
472 w.set_on_menu_item_activated(AppWindow.MENU_OPEN, self._on_menu_open)
473 w.set_on_menu_item_activated(AppWindow.MENU_EXPORT,
474 self._on_menu_export)
475 w.set_on_menu_item_activated(AppWindow.MENU_QUIT, self._on_menu_quit)
476 w.set_on_menu_item_activated(AppWindow.MENU_SHOW_SETTINGS,
477 self._on_menu_toggle_settings_panel)
478 w.set_on_menu_item_activated(AppWindow.MENU_ABOUT, self._on_menu_about)
479 # ----
480
481 self._apply_settings()
482
483 def _apply_settings(self):
484 bg_color = [
485 self.settings.bg_color.red, self.settings.bg_color.green,
486 self.settings.bg_color.blue, self.settings.bg_color.alpha
487 ]
488 self._scene.scene.set_background(bg_color)
489 self._scene.scene.show_skybox(self.settings.show_skybox)
490 self._scene.scene.show_axes(self.settings.show_axes)
491 if self.settings.new_ibl_name is not None:
492 self._scene.scene.scene.set_indirect_light(
493 self.settings.new_ibl_name)
494 # Clear new_ibl_name, so we don't keep reloading this image every
495 # time the settings are applied.
496 self.settings.new_ibl_name = None
497 self._scene.scene.scene.enable_indirect_light(self.settings.use_ibl)
498 self._scene.scene.scene.set_indirect_light_intensity(
499 self.settings.ibl_intensity)
500 sun_color = [
501 self.settings.sun_color.red, self.settings.sun_color.green,
502 self.settings.sun_color.blue
503 ]
504 self._scene.scene.scene.set_sun_light(self.settings.sun_dir, sun_color,
505 self.settings.sun_intensity)
506 self._scene.scene.scene.enable_sun_light(self.settings.use_sun)
507
508 if self.settings.apply_material:
509 self._scene.scene.update_material(self.settings.material)
510 self.settings.apply_material = False
511
512 self._bg_color.color_value = self.settings.bg_color
513 self._show_skybox.checked = self.settings.show_skybox
514 self._show_axes.checked = self.settings.show_axes
515 self._use_ibl.checked = self.settings.use_ibl
516 self._use_sun.checked = self.settings.use_sun
517 self._ibl_intensity.int_value = self.settings.ibl_intensity
518 self._sun_intensity.int_value = self.settings.sun_intensity
519 self._sun_dir.vector_value = self.settings.sun_dir
520 self._sun_color.color_value = self.settings.sun_color
521 self._material_prefab.enabled = (
522 self.settings.material.shader == Settings.LIT)
523 c = gui.Color(self.settings.material.base_color[0],
524 self.settings.material.base_color[1],
525 self.settings.material.base_color[2],
526 self.settings.material.base_color[3])
527 self._material_color.color_value = c
528 self._point_size.double_value = self.settings.material.point_size
529
530 def _on_layout(self, layout_context):
531 # The on_layout callback should set the frame (position + size) of every
532 # child correctly. After the callback is done the window will layout
533 # the grandchildren.
534 r = self.window.content_rect
535 self._scene.frame = r
536 width = 17 * layout_context.theme.font_size
537 height = min(
538 r.height,
539 self._settings_panel.calc_preferred_size(
540 layout_context, gui.Widget.Constraints()).height)
541 self._settings_panel.frame = gui.Rect(r.get_right() - width, r.y, width,
542 height)
543
544 def _set_mouse_mode_rotate(self):
545 self._scene.set_view_controls(gui.SceneWidget.Controls.ROTATE_CAMERA)
546
547 def _set_mouse_mode_fly(self):
548 self._scene.set_view_controls(gui.SceneWidget.Controls.FLY)
549
550 def _set_mouse_mode_sun(self):
551 self._scene.set_view_controls(gui.SceneWidget.Controls.ROTATE_SUN)
552
553 def _set_mouse_mode_ibl(self):
554 self._scene.set_view_controls(gui.SceneWidget.Controls.ROTATE_IBL)
555
556 def _set_mouse_mode_model(self):
557 self._scene.set_view_controls(gui.SceneWidget.Controls.ROTATE_MODEL)
558
559 def _on_bg_color(self, new_color):
560 self.settings.bg_color = new_color
561 self._apply_settings()
562
563 def _on_show_skybox(self, show):
564 self.settings.show_skybox = show
565 self._apply_settings()
566
567 def _on_show_axes(self, show):
568 self.settings.show_axes = show
569 self._apply_settings()
570
571 def _on_use_ibl(self, use):
572 self.settings.use_ibl = use
573 self._profiles.selected_text = Settings.CUSTOM_PROFILE_NAME
574 self._apply_settings()
575
576 def _on_use_sun(self, use):
577 self.settings.use_sun = use
578 self._profiles.selected_text = Settings.CUSTOM_PROFILE_NAME
579 self._apply_settings()
580
581 def _on_lighting_profile(self, name, index):
582 if name != Settings.CUSTOM_PROFILE_NAME:
583 self.settings.apply_lighting_profile(name)
584 self._apply_settings()
585
586 def _on_new_ibl(self, name, index):
587 self.settings.new_ibl_name = gui.Application.instance.resource_path + "/" + name
588 self._profiles.selected_text = Settings.CUSTOM_PROFILE_NAME
589 self._apply_settings()
590
591 def _on_ibl_intensity(self, intensity):
592 self.settings.ibl_intensity = int(intensity)
593 self._profiles.selected_text = Settings.CUSTOM_PROFILE_NAME
594 self._apply_settings()
595
596 def _on_sun_intensity(self, intensity):
597 self.settings.sun_intensity = int(intensity)
598 self._profiles.selected_text = Settings.CUSTOM_PROFILE_NAME
599 self._apply_settings()
600
601 def _on_sun_dir(self, sun_dir):
602 self.settings.sun_dir = sun_dir
603 self._profiles.selected_text = Settings.CUSTOM_PROFILE_NAME
604 self._apply_settings()
605
606 def _on_sun_color(self, color):
607 self.settings.sun_color = color
608 self._apply_settings()
609
610 def _on_shader(self, name, index):
611 self.settings.set_material(AppWindow.MATERIAL_SHADERS[index])
612 self._apply_settings()
613
614 def _on_material_prefab(self, name, index):
615 self.settings.apply_material_prefab(name)
616 self.settings.apply_material = True
617 self._apply_settings()
618
619 def _on_material_color(self, color):
620 self.settings.material.base_color = [
621 color.red, color.green, color.blue, color.alpha
622 ]
623 self.settings.apply_material = True
624 self._apply_settings()
625
626 def _on_point_size(self, size):
627 self.settings.material.point_size = int(size)
628 self.settings.apply_material = True
629 self._apply_settings()
630
631 def _on_menu_open(self):
632 dlg = gui.FileDialog(gui.FileDialog.OPEN, "Choose file to load",
633 self.window.theme)
634 dlg.add_filter(
635 ".ply .stl .fbx .obj .off .gltf .glb",
636 "Triangle mesh files (.ply, .stl, .fbx, .obj, .off, "
637 ".gltf, .glb)")
638 dlg.add_filter(
639 ".xyz .xyzn .xyzrgb .ply .pcd .pts",
640 "Point cloud files (.xyz, .xyzn, .xyzrgb, .ply, "
641 ".pcd, .pts)")
642 dlg.add_filter(".ply", "Polygon files (.ply)")
643 dlg.add_filter(".stl", "Stereolithography files (.stl)")
644 dlg.add_filter(".fbx", "Autodesk Filmbox files (.fbx)")
645 dlg.add_filter(".obj", "Wavefront OBJ files (.obj)")
646 dlg.add_filter(".off", "Object file format (.off)")
647 dlg.add_filter(".gltf", "OpenGL transfer files (.gltf)")
648 dlg.add_filter(".glb", "OpenGL binary transfer files (.glb)")
649 dlg.add_filter(".xyz", "ASCII point cloud files (.xyz)")
650 dlg.add_filter(".xyzn", "ASCII point cloud with normals (.xyzn)")
651 dlg.add_filter(".xyzrgb",
652 "ASCII point cloud files with colors (.xyzrgb)")
653 dlg.add_filter(".pcd", "Point Cloud Data files (.pcd)")
654 dlg.add_filter(".pts", "3D Points files (.pts)")
655 dlg.add_filter("", "All files")
656
657 # A file dialog MUST define on_cancel and on_done functions
658 dlg.set_on_cancel(self._on_file_dialog_cancel)
659 dlg.set_on_done(self._on_load_dialog_done)
660 self.window.show_dialog(dlg)
661
662 def _on_file_dialog_cancel(self):
663 self.window.close_dialog()
664
665 def _on_load_dialog_done(self, filename):
666 self.window.close_dialog()
667 self.load(filename)
668
669 def _on_menu_export(self):
670 dlg = gui.FileDialog(gui.FileDialog.SAVE, "Choose file to save",
671 self.window.theme)
672 dlg.add_filter(".png", "PNG files (.png)")
673 dlg.set_on_cancel(self._on_file_dialog_cancel)
674 dlg.set_on_done(self._on_export_dialog_done)
675 self.window.show_dialog(dlg)
676
677 def _on_export_dialog_done(self, filename):
678 self.window.close_dialog()
679 frame = self._scene.frame
680 self.export_image(filename, frame.width, frame.height)
681
682 def _on_menu_quit(self):
683 gui.Application.instance.quit()
684
685 def _on_menu_toggle_settings_panel(self):
686 self._settings_panel.visible = not self._settings_panel.visible
687 gui.Application.instance.menubar.set_checked(
688 AppWindow.MENU_SHOW_SETTINGS, self._settings_panel.visible)
689
690 def _on_menu_about(self):
691 # Show a simple dialog. Although the Dialog is actually a widget, you can
692 # treat it similar to a Window for layout and put all the widgets in a
693 # layout which you make the only child of the Dialog.
694 em = self.window.theme.font_size
695 dlg = gui.Dialog("About")
696
697 # Add the text
698 dlg_layout = gui.Vert(em, gui.Margins(em, em, em, em))
699 dlg_layout.add_child(gui.Label("Open3D GUI Example"))
700
701 # Add the Ok button. We need to define a callback function to handle
702 # the click.
703 ok = gui.Button("OK")
704 ok.set_on_clicked(self._on_about_ok)
705
706 # We want the Ok button to be an the right side, so we need to add
707 # a stretch item to the layout, otherwise the button will be the size
708 # of the entire row. A stretch item takes up as much space as it can,
709 # which forces the button to be its minimum size.
710 h = gui.Horiz()
711 h.add_stretch()
712 h.add_child(ok)
713 h.add_stretch()
714 dlg_layout.add_child(h)
715
716 dlg.add_child(dlg_layout)
717 self.window.show_dialog(dlg)
718
719 def _on_about_ok(self):
720 self.window.close_dialog()
721
722 def load(self, path):
723 self._scene.scene.clear_geometry()
724
725 geometry = None
726 geometry_type = o3d.io.read_file_geometry_type(path)
727
728 mesh = None
729 if geometry_type & o3d.io.CONTAINS_TRIANGLES:
730 mesh = o3d.io.read_triangle_model(path)
731 if mesh is None:
732 print("[Info]", path, "appears to be a point cloud")
733 cloud = None
734 try:
735 cloud = o3d.io.read_point_cloud(path)
736 except Exception:
737 pass
738 if cloud is not None:
739 print("[Info] Successfully read", path)
740 if not cloud.has_normals():
741 cloud.estimate_normals()
742 cloud.normalize_normals()
743 geometry = cloud
744 else:
745 print("[WARNING] Failed to read points", path)
746
747 if geometry is not None or mesh is not None:
748 try:
749 if mesh is not None:
750 # Triangle model
751 self._scene.scene.add_model("__model__", mesh)
752 else:
753 # Point cloud
754 self._scene.scene.add_geometry("__model__", geometry,
755 self.settings.material)
756 bounds = self._scene.scene.bounding_box
757 self._scene.setup_camera(60, bounds, bounds.get_center())
758 except Exception as e:
759 print(e)
760
761 def export_image(self, path, width, height):
762
763 def on_image(image):
764 img = image
765
766 quality = 9 # png
767 if path.endswith(".jpg"):
768 quality = 100
769 o3d.io.write_image(path, img, quality)
770
771 self._scene.scene.scene.render_to_image(on_image)
772
773
774def main():
775 # We need to initialize the application, which finds the necessary shaders
776 # for rendering and prepares the cross-platform window abstraction.
777 gui.Application.instance.initialize()
778
779 w = AppWindow(1024, 768)
780
781 if len(sys.argv) > 1:
782 path = sys.argv[1]
783 if os.path.exists(path):
784 w.load(path)
785 else:
786 w.window.show_message_box("Error",
787 "Could not open file '" + path + "'")
788
789 # Run the event loop. This will not return until the last window is closed.
790 gui.Application.instance.run()
791
792
793if __name__ == "__main__":
794 main()