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])