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