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