vedo.applications

This module contains vedo applications which provide some ready-to-use funcionalities

   1#!/usr/bin/env python3
   2# -*- coding: utf-8 -*-
   3import os
   4from typing import Union
   5
   6import numpy as np
   7
   8import vedo.vtkclasses as vtki
   9
  10import vedo
  11from vedo.colors import color_map, get_color
  12from vedo.utils import is_sequence, lin_interpolate, mag, precision
  13from vedo.plotter import Plotter
  14from vedo.pointcloud import fit_plane, Points
  15from vedo.shapes import Line, Ribbon, Spline, Text2D
  16from vedo.pyplot import CornerHistogram, histogram
  17from vedo.addons import SliderWidget
  18
  19
  20__docformat__ = "google"
  21
  22__doc__ = """
  23This module contains vedo applications which provide some *ready-to-use* funcionalities
  24
  25<img src="https://vedo.embl.es/images/advanced/app_raycaster.gif" width="500">
  26"""
  27
  28__all__ = [
  29    "Browser",
  30    "IsosurfaceBrowser",
  31    "FreeHandCutPlotter",
  32    "RayCastPlotter",
  33    "Slicer2DPlotter",
  34    "Slicer3DPlotter",
  35    "Slicer3DTwinPlotter",
  36    "MorphPlotter",
  37    "SplinePlotter",
  38    "AnimationPlayer",
  39]
  40
  41
  42#################################
  43class Slicer3DPlotter(Plotter):
  44    """
  45    Generate a rendering window with slicing planes for the input Volume.
  46    """
  47
  48    def __init__(
  49        self,
  50        volume: vedo.Volume,
  51        cmaps=("gist_ncar_r", "hot_r", "bone", "bone_r", "jet", "Spectral_r"),
  52        clamp=True,
  53        use_slider3d=False,
  54        show_histo=True,
  55        show_icon=True,
  56        draggable=False,
  57        at=0,
  58        **kwargs,
  59    ):
  60        """
  61        Generate a rendering window with slicing planes for the input Volume.
  62
  63        Arguments:
  64            cmaps : (list)
  65                list of color maps names to cycle when clicking button
  66            clamp : (bool)
  67                clamp scalar range to reduce the effect of tails in color mapping
  68            use_slider3d : (bool)
  69                show sliders attached along the axes
  70            show_histo : (bool)
  71                show histogram on bottom left
  72            show_icon : (bool)
  73                show a small 3D rendering icon of the volume
  74            draggable : (bool)
  75                make the 3D icon draggable
  76            at : (int)
  77                subwindow number to plot to
  78            **kwargs : (dict)
  79                keyword arguments to pass to a `vedo.plotter.Plotter` instance.
  80
  81        Examples:
  82            - [slicer1.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/slicer1.py)
  83
  84            <img src="https://vedo.embl.es/images/volumetric/slicer1.jpg" width="500">
  85        """
  86        ################################
  87        super().__init__(**kwargs)
  88        self.at(at)
  89        ################################
  90
  91        cx, cy, cz, ch = "dr", "dg", "db", (0.3, 0.3, 0.3)
  92        if np.sum(self.renderer.GetBackground()) < 1.5:
  93            cx, cy, cz = "lr", "lg", "lb"
  94            ch = (0.8, 0.8, 0.8)
  95
  96        if len(self.renderers) > 1:
  97            # 2d sliders do not work with multiple renderers
  98            use_slider3d = True
  99
 100        self.volume = volume
 101        box = volume.box().alpha(0.2)
 102        self.add(box)
 103
 104        volume_axes_inset = vedo.addons.Axes(
 105            box,
 106            xtitle=" ",
 107            ytitle=" ",
 108            ztitle=" ",
 109            yzgrid=False,
 110            xlabel_size=0,
 111            ylabel_size=0,
 112            zlabel_size=0,
 113            tip_size=0.08,
 114            axes_linewidth=3,
 115            xline_color="dr",
 116            yline_color="dg",
 117            zline_color="db",
 118        )
 119
 120        if show_icon:
 121            self.add_inset(
 122                volume,
 123                volume_axes_inset,
 124                pos=(0.9, 0.9),
 125                size=0.15,
 126                c="w",
 127                draggable=draggable,
 128            )
 129
 130        # inits
 131        la, ld = 0.7, 0.3  # ambient, diffuse
 132        dims = volume.dimensions()
 133        data = volume.pointdata[0]
 134        rmin, rmax = volume.scalar_range()
 135        if clamp:
 136            hdata, edg = np.histogram(data, bins=50)
 137            logdata = np.log(hdata + 1)
 138            # mean  of the logscale plot
 139            meanlog = np.sum(np.multiply(edg[:-1], logdata)) / np.sum(logdata)
 140            rmax = min(rmax, meanlog + (meanlog - rmin) * 0.9)
 141            rmin = max(rmin, meanlog - (rmax - meanlog) * 0.9)
 142            # print("scalar range clamped to range: ("
 143            #       + precision(rmin, 3) + ", " + precision(rmax, 3) + ")")
 144
 145        self.cmap_slicer = cmaps[0]
 146
 147        self.current_i = None
 148        self.current_j = None
 149        self.current_k = int(dims[2] / 2)
 150
 151        self.xslice = None
 152        self.yslice = None
 153        self.zslice = None
 154
 155        self.zslice = volume.zslice(self.current_k).lighting("", la, ld, 0)
 156        self.zslice.name = "ZSlice"
 157        self.zslice.cmap(self.cmap_slicer, vmin=rmin, vmax=rmax)
 158        self.add(self.zslice)
 159
 160        self.histogram = None
 161        data_reduced = data
 162        if show_histo:
 163            # try to reduce the number of values to histogram
 164            dims = self.volume.dimensions()
 165            n = (dims[0] - 1) * (dims[1] - 1) * (dims[2] - 1)
 166            n = min(1_000_000, n)
 167            if data.ndim == 1:
 168                data_reduced = np.random.choice(data, n)
 169                self.histogram = histogram(
 170                    data_reduced,
 171                    # title=volume.filename,
 172                    bins=20,
 173                    logscale=True,
 174                    c=self.cmap_slicer,
 175                    bg=ch,
 176                    alpha=1,
 177                    axes=dict(text_scale=2),
 178                ).clone2d(pos=[-0.925, -0.88], size=0.4)
 179                self.add(self.histogram)
 180
 181        #################
 182        def slider_function_x(_widget, _event):
 183            i = int(self.xslider.value)
 184            if i == self.current_i:
 185                return
 186            self.current_i = i
 187            self.xslice = volume.xslice(i).lighting("", la, ld, 0)
 188            self.xslice.cmap(self.cmap_slicer, vmin=rmin, vmax=rmax)
 189            self.xslice.name = "XSlice"
 190            self.remove("XSlice")  # removes the old one
 191            if 0 < i < dims[0]:
 192                self.add(self.xslice)
 193            self.render()
 194
 195        def slider_function_y(_widget, _event):
 196            j = int(self.yslider.value)
 197            if j == self.current_j:
 198                return
 199            self.current_j = j
 200            self.yslice = volume.yslice(j).lighting("", la, ld, 0)
 201            self.yslice.cmap(self.cmap_slicer, vmin=rmin, vmax=rmax)
 202            self.yslice.name = "YSlice"
 203            self.remove("YSlice")
 204            if 0 < j < dims[1]:
 205                self.add(self.yslice)
 206            self.render()
 207
 208        def slider_function_z(_widget, _event):
 209            k = int(self.zslider.value)
 210            if k == self.current_k:
 211                return
 212            self.current_k = k
 213            self.zslice = volume.zslice(k).lighting("", la, ld, 0)
 214            self.zslice.cmap(self.cmap_slicer, vmin=rmin, vmax=rmax)
 215            self.zslice.name = "ZSlice"
 216            self.remove("ZSlice")
 217            if 0 < k < dims[2]:
 218                self.add(self.zslice)
 219            self.render()
 220
 221        if not use_slider3d:
 222            self.xslider = self.add_slider(
 223                slider_function_x,
 224                0,
 225                dims[0],
 226                title="",
 227                title_size=0.5,
 228                pos=[(0.8, 0.12), (0.95, 0.12)],
 229                show_value=False,
 230                c=cx,
 231            )
 232            self.yslider = self.add_slider(
 233                slider_function_y,
 234                0,
 235                dims[1],
 236                title="",
 237                title_size=0.5,
 238                pos=[(0.8, 0.08), (0.95, 0.08)],
 239                show_value=False,
 240                c=cy,
 241            )
 242            self.zslider = self.add_slider(
 243                slider_function_z,
 244                0,
 245                dims[2],
 246                title="",
 247                title_size=0.6,
 248                value=int(dims[2] / 2),
 249                pos=[(0.8, 0.04), (0.95, 0.04)],
 250                show_value=False,
 251                c=cz,
 252            )
 253
 254        else:  # 3d sliders attached to the axes bounds
 255            bs = box.bounds()
 256            self.xslider = self.add_slider3d(
 257                slider_function_x,
 258                pos1=(bs[0], bs[2], bs[4]),
 259                pos2=(bs[1], bs[2], bs[4]),
 260                xmin=0,
 261                xmax=dims[0],
 262                t=box.diagonal_size() / mag(box.xbounds()) * 0.6,
 263                c=cx,
 264                show_value=False,
 265            )
 266            self.yslider = self.add_slider3d(
 267                slider_function_y,
 268                pos1=(bs[1], bs[2], bs[4]),
 269                pos2=(bs[1], bs[3], bs[4]),
 270                xmin=0,
 271                xmax=dims[1],
 272                t=box.diagonal_size() / mag(box.ybounds()) * 0.6,
 273                c=cy,
 274                show_value=False,
 275            )
 276            self.zslider = self.add_slider3d(
 277                slider_function_z,
 278                pos1=(bs[0], bs[2], bs[4]),
 279                pos2=(bs[0], bs[2], bs[5]),
 280                xmin=0,
 281                xmax=dims[2],
 282                value=int(dims[2] / 2),
 283                t=box.diagonal_size() / mag(box.zbounds()) * 0.6,
 284                c=cz,
 285                show_value=False,
 286            )
 287
 288        #################
 289        def button_func(_obj, _evtname):
 290            bu.switch()
 291            self.cmap_slicer = bu.status()
 292            for m in self.objects:
 293                try:
 294                    if "Slice" in m.name:
 295                        m.cmap(self.cmap_slicer, vmin=rmin, vmax=rmax)
 296                except AttributeError:
 297                    pass
 298            self.remove(self.histogram)
 299            if show_histo:
 300                self.histogram = histogram(
 301                    data_reduced,
 302                    # title=volume.filename,
 303                    bins=20,
 304                    logscale=True,
 305                    c=self.cmap_slicer,
 306                    bg=ch,
 307                    alpha=1,
 308                    axes=dict(text_scale=2),
 309                ).clone2d(pos=[-0.925, -0.88], size=0.4)
 310                self.add(self.histogram)
 311            self.render()
 312
 313        if len(cmaps) > 1:
 314            bu = self.add_button(
 315                button_func,
 316                states=cmaps,
 317                c=["k9"] * len(cmaps),
 318                bc=["k1"] * len(cmaps),  # colors of states
 319                size=16,
 320                bold=True,
 321            )
 322            if bu:
 323                bu.pos([0.04, 0.01], "bottom-left")
 324
 325
 326####################################################################################
 327class Slicer3DTwinPlotter(Plotter):
 328    """
 329    Create a window with two side-by-side 3D slicers for two Volumes.
 330
 331    Arguments:
 332        vol1 : (Volume)
 333            the first Volume object to be isosurfaced.
 334        vol2 : (Volume)
 335            the second Volume object to be isosurfaced.
 336        clamp : (bool)
 337            clamp scalar range to reduce the effect of tails in color mapping
 338        **kwargs : (dict)
 339            keyword arguments to pass to a `vedo.plotter.Plotter` instance.
 340
 341    Example:
 342        ```python
 343        from vedo import *
 344        from vedo.applications import Slicer3DTwinPlotter
 345
 346        vol1 = Volume(dataurl + "embryo.slc")
 347        vol2 = Volume(dataurl + "embryo.slc")
 348
 349        plt = Slicer3DTwinPlotter(
 350            vol1, vol2,
 351            shape=(1, 2),
 352            sharecam=True,
 353            bg="white",
 354            bg2="lightblue",
 355        )
 356
 357        plt.at(0).add(Text2D("Volume 1", pos="top-center"))
 358        plt.at(1).add(Text2D("Volume 2", pos="top-center"))
 359
 360        plt.show(viewup='z')
 361        plt.at(0).reset_camera()
 362        plt.interactive().close()
 363        ```
 364
 365        <img src="https://vedo.embl.es/images/volumetric/slicer3dtwin.png" width="650">
 366    """
 367
 368    def __init__(self, vol1: vedo.Volume, vol2: vedo.Volume, clamp=True, **kwargs):
 369
 370        super().__init__(**kwargs)
 371
 372        cmap = "gist_ncar_r"
 373        cx, cy, cz = "dr", "dg", "db"  # slider colors
 374        ambient, diffuse = 0.7, 0.3  # lighting params
 375
 376        self.at(0)
 377        box1 = vol1.box().alpha(0.1)
 378        box2 = vol2.box().alpha(0.1)
 379        self.add(box1)
 380
 381        self.at(1).add(box2)
 382        self.add_inset(vol2, pos=(0.85, 0.15), size=0.15, c="white", draggable=0)
 383
 384        dims = vol1.dimensions()
 385        data = vol1.pointdata[0]
 386        rmin, rmax = vol1.scalar_range()
 387        if clamp:
 388            hdata, edg = np.histogram(data, bins=50)
 389            logdata = np.log(hdata + 1)
 390            meanlog = np.sum(np.multiply(edg[:-1], logdata)) / np.sum(logdata)
 391            rmax = min(rmax, meanlog + (meanlog - rmin) * 0.9)
 392            rmin = max(rmin, meanlog - (rmax - meanlog) * 0.9)
 393
 394        def slider_function_x(_widget, _event):
 395            i = int(self.xslider.value)
 396            msh1 = vol1.xslice(i).lighting("", ambient, diffuse, 0)
 397            msh1.cmap(cmap, vmin=rmin, vmax=rmax)
 398            msh1.name = "XSlice"
 399            self.at(0).remove("XSlice")  # removes the old one
 400            msh2 = vol2.xslice(i).lighting("", ambient, diffuse, 0)
 401            msh2.cmap(cmap, vmin=rmin, vmax=rmax)
 402            msh2.name = "XSlice"
 403            self.at(1).remove("XSlice")
 404            if 0 < i < dims[0]:
 405                self.at(0).add(msh1)
 406                self.at(1).add(msh2)
 407
 408        def slider_function_y(_widget, _event):
 409            i = int(self.yslider.value)
 410            msh1 = vol1.yslice(i).lighting("", ambient, diffuse, 0)
 411            msh1.cmap(cmap, vmin=rmin, vmax=rmax)
 412            msh1.name = "YSlice"
 413            self.at(0).remove("YSlice")
 414            msh2 = vol2.yslice(i).lighting("", ambient, diffuse, 0)
 415            msh2.cmap(cmap, vmin=rmin, vmax=rmax)
 416            msh2.name = "YSlice"
 417            self.at(1).remove("YSlice")
 418            if 0 < i < dims[1]:
 419                self.at(0).add(msh1)
 420                self.at(1).add(msh2)
 421
 422        def slider_function_z(_widget, _event):
 423            i = int(self.zslider.value)
 424            msh1 = vol1.zslice(i).lighting("", ambient, diffuse, 0)
 425            msh1.cmap(cmap, vmin=rmin, vmax=rmax)
 426            msh1.name = "ZSlice"
 427            self.at(0).remove("ZSlice")
 428            msh2 = vol2.zslice(i).lighting("", ambient, diffuse, 0)
 429            msh2.cmap(cmap, vmin=rmin, vmax=rmax)
 430            msh2.name = "ZSlice"
 431            self.at(1).remove("ZSlice")
 432            if 0 < i < dims[2]:
 433                self.at(0).add(msh1)
 434                self.at(1).add(msh2)
 435
 436        self.at(0)
 437        bs = box1.bounds()
 438        self.xslider = self.add_slider3d(
 439            slider_function_x,
 440            pos1=(bs[0], bs[2], bs[4]),
 441            pos2=(bs[1], bs[2], bs[4]),
 442            xmin=0,
 443            xmax=dims[0],
 444            t=box1.diagonal_size() / mag(box1.xbounds()) * 0.6,
 445            c=cx,
 446            show_value=False,
 447        )
 448        self.yslider = self.add_slider3d(
 449            slider_function_y,
 450            pos1=(bs[1], bs[2], bs[4]),
 451            pos2=(bs[1], bs[3], bs[4]),
 452            xmin=0,
 453            xmax=dims[1],
 454            t=box1.diagonal_size() / mag(box1.ybounds()) * 0.6,
 455            c=cy,
 456            show_value=False,
 457        )
 458        self.zslider = self.add_slider3d(
 459            slider_function_z,
 460            pos1=(bs[0], bs[2], bs[4]),
 461            pos2=(bs[0], bs[2], bs[5]),
 462            xmin=0,
 463            xmax=dims[2],
 464            value=int(dims[2] / 2),
 465            t=box1.diagonal_size() / mag(box1.zbounds()) * 0.6,
 466            c=cz,
 467            show_value=False,
 468        )
 469
 470        #################
 471        hist = CornerHistogram(data, s=0.2, bins=25, logscale=True, c="k")
 472        self.add(hist)
 473        slider_function_z(0, 0)  ## init call
 474
 475
 476########################################################################################
 477class MorphPlotter(Plotter):
 478    """
 479    A Plotter with 3 renderers to show the source, target and warped meshes.
 480
 481    Examples:
 482        - [warp4b.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp4b.py)
 483
 484            ![](https://vedo.embl.es/images/advanced/warp4b.jpg)
 485    """
 486
 487    def __init__(self, source, target, **kwargs):
 488
 489        vedo.settings.enable_default_keyboard_callbacks = False
 490        vedo.settings.enable_default_mouse_callbacks = False
 491
 492        kwargs.update({"N": 3})
 493        kwargs.update({"sharecam": 0})
 494        super().__init__(**kwargs)
 495
 496        self.source = source.pickable(True)
 497        self.target = target.pickable(False)
 498        self.clicked = []
 499        self.sources = []
 500        self.targets = []
 501        self.warped = None
 502        self.source_labels = None
 503        self.target_labels = None
 504        self.automatic_picking_distance = 0.075
 505        self.cmap_name = "coolwarm"
 506        self.nbins = 25
 507        self.msg0 = Text2D("Pick a point on the surface",
 508                           pos="bottom-center", c='white', bg="blue4", alpha=1, font="Calco")
 509        self.msg1 = Text2D(pos="bottom-center", c='white', bg="blue4", alpha=1, font="Calco")
 510        self.instructions = Text2D(s=0.7, bg="blue4", alpha=0.1, font="Calco")
 511        self.instructions.text(
 512            "  Morphological alignment of 3D surfaces\n\n"
 513            "Pick a point on the source surface, then\n"
 514            "pick the corresponding point on the target \n"
 515            "Pick at least 4 point pairs. Press:\n"
 516            "- c to clear all landmarks\n"
 517            "- d to delete the last landmark pair\n"
 518            "- a to auto-pick additional landmarks\n"
 519            "- z to compute and show the residuals\n"
 520            "- q to quit and proceed"
 521        )
 522        self.at(0).add_renderer_frame()
 523        self.add(source, self.msg0, self.instructions).reset_camera()
 524        self.at(1).add_renderer_frame()
 525        self.add(Text2D(f"Target: {target.filename[-35:]}", bg="blue4", alpha=0.1, font="Calco"))
 526        self.add(self.msg1, target)
 527        cam1 = self.camera  # save camera at 1
 528        self.at(2).background("k9")
 529        self.add(target, Text2D("Morphing Output", font="Calco"))
 530        self.camera = cam1  # use the same camera of renderer1
 531
 532        self.add_renderer_frame()
 533
 534        self.callid1 = self.add_callback("KeyPress", self.on_keypress)
 535        self.callid2 = self.add_callback("LeftButtonPress", self.on_click)
 536        self._interactive = True
 537
 538    ################################################
 539    def update(self):
 540        """Update the rendering window"""
 541        source_pts = Points(self.sources).color("purple5").ps(12)
 542        target_pts = Points(self.targets).color("purple5").ps(12)
 543        source_pts.name = "source_pts"
 544        target_pts.name = "target_pts"
 545        self.source_labels = source_pts.labels2d("id", c="purple3")
 546        self.target_labels = target_pts.labels2d("id", c="purple3")
 547        self.source_labels.name = "source_pts"
 548        self.target_labels.name = "target_pts"
 549        self.at(0).remove("source_pts").add(source_pts, self.source_labels)
 550        self.at(1).remove("target_pts").add(target_pts, self.target_labels)
 551        self.render()
 552
 553        if len(self.sources) == len(self.targets) and len(self.sources) > 3:
 554            self.warped = self.source.clone().warp(self.sources, self.targets)
 555            self.warped.name = "warped"
 556            self.at(2).remove("warped").add(self.warped)
 557            self.render()
 558
 559    def on_click(self, evt):
 560        """Handle mouse click events"""
 561        if evt.object == self.source:
 562            self.sources.append(evt.picked3d)
 563            self.source.pickable(False)
 564            self.target.pickable(True)
 565            self.msg0.text("--->")
 566            self.msg1.text("now pick a target point")
 567            self.update()
 568        elif evt.object == self.target:
 569            self.targets.append(evt.picked3d)
 570            self.source.pickable(True)
 571            self.target.pickable(False)
 572            self.msg0.text("now pick a source point")
 573            self.msg1.text("<---")
 574            self.update()
 575
 576    def on_keypress(self, evt):
 577        """Handle keyboard events"""
 578        if evt.keypress == "c":
 579            self.sources.clear()
 580            self.targets.clear()
 581            self.at(0).remove("source_pts")
 582            self.at(1).remove("target_pts")
 583            self.at(2).remove("warped")
 584            self.msg0.text("CLEARED! Pick a point here")
 585            self.msg1.text("")
 586            self.source.pickable(True)
 587            self.target.pickable(False)
 588            self.update()
 589        if evt.keypress == "w":
 590            rep = (self.warped.properties.GetRepresentation() == 1)
 591            self.warped.wireframe(not rep)
 592            self.render()
 593        if evt.keypress == "d":
 594            n = min(len(self.sources), len(self.targets))
 595            self.sources = self.sources[:n-1]
 596            self.targets = self.targets[:n-1]
 597            self.msg0.text("Last point deleted! Pick a point here")
 598            self.msg1.text("")
 599            self.source.pickable(True)
 600            self.target.pickable(False)
 601            self.update()
 602        if evt.keypress == "a":
 603            # auto-pick points on the target surface
 604            if not self.warped:
 605                vedo.printc("At least 4 points are needed.", c="r")
 606                return
 607            pts = self.target.clone().subsample(self.automatic_picking_distance)
 608            if len(self.sources) > len(self.targets):
 609                self.sources.pop()
 610            d = self.target.diagonal_size()
 611            r = d * self.automatic_picking_distance
 612            TI = self.warped.transform.compute_inverse()
 613            for p in pts.coordinates:
 614                pp = vedo.utils.closest(p, self.targets)[1]
 615                if vedo.mag(pp - p) < r:
 616                    continue
 617                q = self.warped.closest_point(p)
 618                self.sources.append(TI(q))
 619                self.targets.append(p)
 620            self.source.pickable(True)
 621            self.target.pickable(False)
 622            self.update()
 623        if evt.keypress == "z" or evt.keypress == "a":
 624            dists = self.warped.distance_to(self.target, signed=True)
 625            v = np.std(dists) * 2
 626            self.warped.cmap(self.cmap_name, dists, vmin=-v, vmax=+v)
 627
 628            h = vedo.pyplot.histogram(
 629                dists,
 630                bins=self.nbins,
 631                title=" ",
 632                xtitle=f"STD = {v/2:.2f}",
 633                ytitle="",
 634                c=self.cmap_name,
 635                xlim=(-v, v),
 636                aspect=16/9,
 637                axes=dict(
 638                    number_of_divisions=5,
 639                    text_scale=2,
 640                    xtitle_offset=0.075,
 641                    xlabel_justify="top-center"),
 642            )
 643
 644            # try to fit a gaussian to the histogram
 645            def gauss(x, A, B, sigma):
 646                return A + B * np.exp(-x**2 / (2 * sigma**2))
 647            try:
 648                from scipy.optimize import curve_fit
 649                inits = [0, len(dists)/self.nbins*2.5, v/2]
 650                popt, _ = curve_fit(gauss, xdata=h.centers, ydata=h.frequencies, p0=inits)
 651                x = np.linspace(-v, v, 300)
 652                h += vedo.pyplot.plot(x, gauss(x, *popt), like=h, lw=1, lc="k2")
 653                h["Axes"]["xtitle"].text(f":sigma = {abs(popt[2]):.3f}", font="VictorMono")
 654            except:
 655                pass
 656
 657            h = h.clone2d(pos="bottom-left", size=0.575)
 658            h.name = "warped"
 659            self.at(2).add(h)
 660            self.render()
 661
 662        if evt.keypress == "q":
 663            self.break_interaction()
 664
 665
 666########################################################################################
 667class Slicer2DPlotter(Plotter):
 668    """
 669    A single slice of a Volume which always faces the camera,
 670    but at the same time can be oriented arbitrarily in space.
 671    """
 672
 673    def __init__(self, vol: vedo.Volume, levels=(None, None), histo_color="red4", **kwargs):
 674        """
 675        A single slice of a Volume which always faces the camera,
 676        but at the same time can be oriented arbitrarily in space.
 677
 678        Arguments:
 679            vol : (Volume)
 680                the Volume object to be isosurfaced.
 681            levels : (list)
 682                window and color levels
 683            histo_color : (color)
 684                histogram color, use `None` to disable it
 685            **kwargs : (dict)
 686                keyword arguments to pass to a `vedo.plotter.Plotter` instance.
 687
 688        <img src="https://vedo.embl.es/images/volumetric/read_volume3.jpg" width="500">
 689        """
 690
 691        if "shape" not in kwargs:
 692            custom_shape = [  # define here the 2 rendering rectangle spaces
 693                dict(bottomleft=(0.0, 0.0), topright=(1, 1), bg="k9"),  # the full window
 694                dict(bottomleft=(0.8, 0.8), topright=(1, 1), bg="k8", bg2="lb"),
 695            ]
 696            kwargs["shape"] = custom_shape
 697
 698        if "interactive" not in kwargs:
 699            kwargs["interactive"] = True
 700
 701        super().__init__(**kwargs)
 702
 703        self.user_mode("image")
 704        self.add_callback("KeyPress", self.on_key_press)
 705
 706        orig_volume = vol.clone(deep=False)
 707        self.volume = vol
 708
 709        self.volume.actor = vtki.new("ImageSlice")
 710
 711        self.volume.properties = self.volume.actor.GetProperty()
 712        self.volume.properties.SetInterpolationTypeToLinear()
 713
 714        self.volume.mapper = vtki.new("ImageResliceMapper")
 715        self.volume.mapper.SetInputData(self.volume.dataset)
 716        self.volume.mapper.SliceFacesCameraOn()
 717        self.volume.mapper.SliceAtFocalPointOn()
 718        self.volume.mapper.SetAutoAdjustImageQuality(False)
 719        self.volume.mapper.BorderOff()
 720
 721        # no argument will grab the existing cmap in vol (or use build_lut())
 722        self.lut = None
 723        self.cmap()
 724
 725        if levels[0] and levels[1]:
 726            self.lighting(window=levels[0], level=levels[1])
 727
 728        self.usage_txt = (
 729            "H                  :rightarrow Toggle this banner on/off\n"
 730            "Left click & drag  :rightarrow Modify luminosity and contrast\n"
 731            "SHIFT-Left click   :rightarrow Slice image obliquely\n"
 732            "SHIFT-Middle click :rightarrow Slice image perpendicularly\n"
 733            "SHIFT-R            :rightarrow Fly to closest cartesian view\n"
 734            "SHIFT-U            :rightarrow Toggle parallel projection"
 735        )
 736
 737        self.usage = Text2D(
 738            self.usage_txt, font="Calco", pos="top-left", s=0.8, bg="yellow", alpha=0.25
 739        )
 740
 741        hist = None
 742        if histo_color is not None:
 743            data = self.volume.pointdata[0]
 744            arr = data
 745            if data.ndim == 1:
 746                # try to reduce the number of values to histogram
 747                dims = self.volume.dimensions()
 748                n = (dims[0] - 1) * (dims[1] - 1) * (dims[2] - 1)
 749                n = min(1_000_000, n)
 750                arr = np.random.choice(self.volume.pointdata[0], n)
 751                hist = vedo.pyplot.histogram(
 752                    arr,
 753                    bins=12,
 754                    logscale=True,
 755                    c=histo_color,
 756                    ytitle="log_10 (counts)",
 757                    axes=dict(text_scale=1.9),
 758                ).clone2d(pos="bottom-left", size=0.4)
 759
 760        axes = kwargs.pop("axes", 7)
 761        axe = None
 762        if axes == 7:
 763            axe = vedo.addons.RulerAxes(
 764                orig_volume, xtitle="x - ", ytitle="y - ", ztitle="z - "
 765            )
 766
 767        box = orig_volume.box().alpha(0.25)
 768
 769        volume_axes_inset = vedo.addons.Axes(
 770            box,
 771            yzgrid=False,
 772            xlabel_size=0,
 773            ylabel_size=0,
 774            zlabel_size=0,
 775            tip_size=0.08,
 776            axes_linewidth=3,
 777            xline_color="dr",
 778            yline_color="dg",
 779            zline_color="db",
 780            xtitle_color="dr",
 781            ytitle_color="dg",
 782            ztitle_color="db",
 783            xtitle_size=0.1,
 784            ytitle_size=0.1,
 785            ztitle_size=0.1,
 786            title_font="VictorMono",
 787        )
 788
 789        self.at(0).add(self.volume, box, axe, self.usage, hist)
 790        self.at(1).add(orig_volume, volume_axes_inset)
 791        self.at(0)  # set focus at renderer 0
 792
 793    ####################################################################
 794    def on_key_press(self, evt):
 795        """Handle keyboard events"""
 796        if evt.keypress == "q":
 797            self.break_interaction()
 798        elif evt.keypress.lower() == "h":
 799            t = self.usage
 800            if len(t.text()) > 50:
 801                self.usage.text("Press H to show help")
 802            else:
 803                self.usage.text(self.usage_txt)
 804            self.render()
 805
 806    def cmap(self, lut=None, fix_scalar_range=False) -> "Slicer2DPlotter":
 807        """
 808        Assign a LUT (Look Up Table) to colorize the slice, leave it `None`
 809        to reuse an existing Volume color map.
 810        Use "bw" for automatic black and white.
 811        """
 812        if lut is None and self.lut:
 813            self.volume.properties.SetLookupTable(self.lut)
 814        elif isinstance(lut, vtki.vtkLookupTable):
 815            self.volume.properties.SetLookupTable(lut)
 816        elif lut == "bw":
 817            self.volume.properties.SetLookupTable(None)
 818        self.volume.properties.SetUseLookupTableScalarRange(fix_scalar_range)
 819        return self
 820
 821    def alpha(self, value: float) -> "Slicer2DPlotter":
 822        """Set opacity to the slice"""
 823        self.volume.properties.SetOpacity(value)
 824        return self
 825
 826    def auto_adjust_quality(self, value=True) -> "Slicer2DPlotter":
 827        """Automatically reduce the rendering quality for greater speed when interacting"""
 828        self.volume.mapper.SetAutoAdjustImageQuality(value)
 829        return self
 830
 831    def slab(self, thickness=0, mode=0, sample_factor=2) -> "Slicer2DPlotter":
 832        """
 833        Make a thick slice (slab).
 834
 835        Arguments:
 836            thickness : (float)
 837                set the slab thickness, for thick slicing
 838            mode : (int)
 839                The slab type:
 840                    0 = min
 841                    1 = max
 842                    2 = mean
 843                    3 = sum
 844            sample_factor : (float)
 845                Set the number of slab samples to use as a factor of the number of input slices
 846                within the slab thickness. The default value is 2, but 1 will increase speed
 847                with very little loss of quality.
 848        """
 849        self.volume.mapper.SetSlabThickness(thickness)
 850        self.volume.mapper.SetSlabType(mode)
 851        self.volume.mapper.SetSlabSampleFactor(sample_factor)
 852        return self
 853
 854    def face_camera(self, value=True) -> "Slicer2DPlotter":
 855        """Make the slice always face the camera or not."""
 856        self.volume.mapper.SetSliceFacesCameraOn(value)
 857        return self
 858
 859    def jump_to_nearest_slice(self, value=True) -> "Slicer2DPlotter":
 860        """
 861        This causes the slicing to occur at the closest slice to the focal point,
 862        instead of the default behavior where a new slice is interpolated between
 863        the original slices.
 864        Nothing happens if the plane is oblique to the original slices.
 865        """
 866        self.volume.mapper.SetJumpToNearestSlice(value)
 867        return self
 868
 869    def fill_background(self, value=True) -> "Slicer2DPlotter":
 870        """
 871        Instead of rendering only to the image border,
 872        render out to the viewport boundary with the background color.
 873        The background color will be the lowest color on the lookup
 874        table that is being used for the image.
 875        """
 876        self.volume.mapper.SetBackground(value)
 877        return self
 878
 879    def lighting(self, window, level, ambient=1.0, diffuse=0.0) -> "Slicer2DPlotter":
 880        """Assign the values for window and color level."""
 881        self.volume.properties.SetColorWindow(window)
 882        self.volume.properties.SetColorLevel(level)
 883        self.volume.properties.SetAmbient(ambient)
 884        self.volume.properties.SetDiffuse(diffuse)
 885        return self
 886
 887
 888########################################################################
 889class RayCastPlotter(Plotter):
 890    """
 891    Generate Volume rendering using ray casting.
 892    """
 893
 894    def __init__(self, volume, **kwargs):
 895        """
 896        Generate a window for Volume rendering using ray casting.
 897
 898        Arguments:
 899            volume : (Volume)
 900                the Volume object to be isosurfaced.
 901            **kwargs : (dict)
 902                keyword arguments to pass to a `vedo.plotter.Plotter` instance.
 903
 904        Returns:
 905            `vedo.Plotter` object.
 906
 907        Examples:
 908            - [app_raycaster.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/app_raycaster.py)
 909
 910            ![](https://vedo.embl.es/images/advanced/app_raycaster.gif)
 911        """
 912
 913        super().__init__(**kwargs)
 914
 915        self.alphaslider0 = 0.33
 916        self.alphaslider1 = 0.66
 917        self.alphaslider2 = 1
 918        self.color_scalarbar = None
 919
 920        self.properties = volume.properties
 921
 922        if volume.dimensions()[2] < 3:
 923            vedo.logger.error("RayCastPlotter: not enough z slices.")
 924            raise RuntimeError
 925
 926        smin, smax = volume.scalar_range()
 927        x0alpha = smin + (smax - smin) * 0.25
 928        x1alpha = smin + (smax - smin) * 0.5
 929        x2alpha = smin + (smax - smin) * 1.0
 930
 931        ############################## color map slider
 932        # Create transfer mapping scalar value to color
 933        cmaps = [
 934            "rainbow", "rainbow_r",
 935            "viridis", "viridis_r",
 936            "bone", "bone_r",
 937            "hot", "hot_r",
 938            "plasma", "plasma_r",
 939            "gist_earth", "gist_earth_r",
 940            "coolwarm", "coolwarm_r",
 941            "tab10_r",
 942        ]
 943        cols_cmaps = []
 944        for cm in cmaps:
 945            cols = color_map(range(0, 21), cm, 0, 20)  # sample 20 colors
 946            cols_cmaps.append(cols)
 947        Ncols = len(cmaps)
 948        csl = "k9"
 949        if sum(get_color(self.background())) > 1.5:
 950            csl = "k1"
 951
 952        def slider_cmap(widget=None, _event=""):
 953            if widget:
 954                k = int(widget.value)
 955                volume.cmap(cmaps[k])
 956            self.remove(self.color_scalarbar)
 957            self.color_scalarbar = vedo.addons.ScalarBar(
 958                volume,
 959                horizontal=True,
 960                pos=[(0.8, 0), (0.97, 0.1)],
 961                font_size=0
 962            )
 963            self.add(self.color_scalarbar)
 964
 965        w1 = self.add_slider(
 966            slider_cmap,
 967            0, Ncols - 1,
 968            value=0,
 969            show_value=False,
 970            c=csl,
 971            pos=[(0.8, 0.05), (0.965, 0.05)],
 972        )
 973        w1.representation.SetTitleHeight(0.018)
 974
 975        ############################## alpha sliders
 976        # Create transfer mapping scalar value to opacity transfer function
 977        def setOTF():
 978            otf = self.properties.GetScalarOpacity()
 979            otf.RemoveAllPoints()
 980            otf.AddPoint(smin, 0.0)
 981            otf.AddPoint(smin + (smax - smin) * 0.1, 0.0)
 982            otf.AddPoint(x0alpha, self.alphaslider0)
 983            otf.AddPoint(x1alpha, self.alphaslider1)
 984            otf.AddPoint(x2alpha, self.alphaslider2)
 985            slider_cmap()
 986
 987        setOTF()  ################
 988
 989        def sliderA0(widget, _event):
 990            self.alphaslider0 = widget.value
 991            setOTF()
 992
 993        self.add_slider(
 994            sliderA0,
 995            0, 1,
 996            value=self.alphaslider0,
 997            pos=[(0.84, 0.1), (0.84, 0.26)],
 998            c=csl,
 999            show_value=0,
1000        )
1001
1002        def sliderA1(widget, _event):
1003            self.alphaslider1 = widget.value
1004            setOTF()
1005
1006        self.add_slider(
1007            sliderA1,
1008            0, 1,
1009            value=self.alphaslider1,
1010            pos=[(0.89, 0.1), (0.89, 0.26)],
1011            c=csl,
1012            show_value=0,
1013        )
1014
1015        def sliderA2(widget, _event):
1016            self.alphaslider2 = widget.value
1017            setOTF()
1018
1019        w2 = self.add_slider(
1020            sliderA2,
1021            0, 1,
1022            value=self.alphaslider2,
1023            pos=[(0.96, 0.1), (0.96, 0.26)],
1024            c=csl,
1025            show_value=0,
1026            title="Opacity Levels",
1027        )
1028        w2.GetRepresentation().SetTitleHeight(0.015)
1029
1030        # add a button
1031        def button_func_mode(_obj, _ename):
1032            s = volume.mode()
1033            snew = (s + 1) % 2
1034            volume.mode(snew)
1035            bum.switch()
1036
1037        bum = self.add_button(
1038            button_func_mode,
1039            pos=(0.89, 0.31),
1040            states=["  composite   ", "max projection"],
1041            c=[ "k3", "k6"],
1042            bc=["k6", "k3"],  # colors of states
1043            font="Calco",
1044            size=18,
1045            bold=0,
1046            italic=False,
1047        )
1048        bum.frame(color="k6")
1049        bum.status(volume.mode())
1050
1051        slider_cmap() ############# init call to create scalarbar
1052
1053        # add histogram of scalar
1054        plot = CornerHistogram(
1055            volume,
1056            bins=25,
1057            logscale=1,
1058            c='k5',
1059            bg='k5',
1060            pos=(0.78, 0.065),
1061            lines=True,
1062            dots=False,
1063            nmax=3.1415e06,  # subsample otherwise is too slow
1064        )
1065
1066        plot.GetPosition2Coordinate().SetValue(0.197, 0.20, 0)
1067        plot.GetXAxisActor2D().SetFontFactor(0.7)
1068        plot.GetProperty().SetOpacity(0.5)
1069        self.add([plot, volume])
1070
1071
1072#####################################################################################
1073class IsosurfaceBrowser(Plotter):
1074    """
1075    Generate a Volume isosurfacing controlled by a slider.
1076    """
1077
1078    def __init__(
1079        self,
1080        volume: vedo.Volume,
1081        isovalue=None,
1082        scalar_range=(),
1083        c=None,
1084        alpha=1,
1085        lego=False,
1086        res=50,
1087        use_gpu=False,
1088        precompute=False,
1089        cmap="hot",
1090        delayed=False,
1091        sliderpos=4,
1092        **kwargs,
1093    ) -> None:
1094        """
1095        Generate a `vedo.Plotter` for Volume isosurfacing using a slider.
1096
1097        Arguments:
1098            volume : (Volume)
1099                the Volume object to be isosurfaced.
1100            isovalues : (float, list)
1101                isosurface value(s) to be displayed.
1102            scalar_range : (list)
1103                scalar range to be used.
1104            c : str, (list)
1105                color(s) of the isosurface(s).
1106            alpha : (float, list)
1107                opacity of the isosurface(s).
1108            lego : (bool)
1109                if True generate a lego plot instead of a surface.
1110            res : (int)
1111                resolution of the isosurface.
1112            use_gpu : (bool)
1113                use GPU acceleration.
1114            precompute : (bool)
1115                precompute the isosurfaces (so slider browsing will be smoother).
1116            cmap : (str)
1117                color map name to be used.
1118            delayed : (bool)
1119                delay the slider update on mouse release.
1120            sliderpos : (int)
1121                position of the slider.
1122            **kwargs : (dict)
1123                keyword arguments to pass to a `vedo.plotter.Plotter` instance.
1124
1125        Examples:
1126            - [app_isobrowser.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/app_isobrowser.py)
1127
1128                ![](https://vedo.embl.es/images/advanced/app_isobrowser.gif)
1129        """
1130
1131        super().__init__(**kwargs)
1132
1133        self.slider = None
1134
1135        ### GPU ################################
1136        if use_gpu and hasattr(volume.properties, "GetIsoSurfaceValues"):
1137
1138            if len(scalar_range) == 2:
1139                scrange = scalar_range
1140            else:
1141                scrange = volume.scalar_range()
1142            delta = scrange[1] - scrange[0]
1143            if not delta:
1144                return
1145
1146            if isovalue is None:
1147                isovalue = delta / 3.0 + scrange[0]
1148
1149            ### isovalue slider callback
1150            def slider_isovalue(widget, _event):
1151                value = widget.GetRepresentation().GetValue()
1152                isovals.SetValue(0, value)
1153
1154            isovals = volume.properties.GetIsoSurfaceValues()
1155            isovals.SetValue(0, isovalue)
1156            self.add(volume.mode(5).alpha(alpha).cmap(c))
1157
1158            self.slider = self.add_slider(
1159                slider_isovalue,
1160                scrange[0] + 0.02 * delta,
1161                scrange[1] - 0.02 * delta,
1162                value=isovalue,
1163                pos=sliderpos,
1164                title="scalar value",
1165                show_value=True,
1166                delayed=delayed,
1167            )
1168
1169        ### CPU ################################
1170        else:
1171
1172            self._prev_value = 1e30
1173
1174            scrange = volume.scalar_range()
1175            delta = scrange[1] - scrange[0]
1176            if not delta:
1177                return
1178
1179            if lego:
1180                res = int(res / 2)  # because lego is much slower
1181                slidertitle = ""
1182            else:
1183                slidertitle = "scalar value"
1184
1185            allowed_vals = np.linspace(scrange[0], scrange[1], num=res)
1186
1187            bacts = {}  # cache the meshes so we dont need to recompute
1188            if precompute:
1189                delayed = False  # no need to delay the slider in this case
1190
1191                for value in allowed_vals:
1192                    value_name = precision(value, 2)
1193                    if lego:
1194                        mesh = volume.legosurface(vmin=value)
1195                        if mesh.ncells:
1196                            mesh.cmap(cmap, vmin=scrange[0], vmax=scrange[1], on="cells")
1197                    else:
1198                        mesh = volume.isosurface(value).color(c).alpha(alpha)
1199                    bacts.update({value_name: mesh})  # store it
1200
1201            ### isovalue slider callback
1202            def slider_isovalue(widget, _event):
1203
1204                prevact = self.vol_actors[0]
1205                if isinstance(widget, float):
1206                    value = widget
1207                else:
1208                    value = widget.GetRepresentation().GetValue()
1209
1210                # snap to the closest
1211                idx = (np.abs(allowed_vals - value)).argmin()
1212                value = allowed_vals[idx]
1213
1214                if abs(value - self._prev_value) / delta < 0.001:
1215                    return
1216                self._prev_value = value
1217
1218                value_name = precision(value, 2)
1219                if value_name in bacts:  # reusing the already existing mesh
1220                    # print('reusing')
1221                    mesh = bacts[value_name]
1222                else:  # else generate it
1223                    # print('generating', value)
1224                    if lego:
1225                        mesh = volume.legosurface(vmin=value)
1226                        if mesh.ncells:
1227                            mesh.cmap(cmap, vmin=scrange[0], vmax=scrange[1], on="cells")
1228                    else:
1229                        mesh = volume.isosurface(value).color(c).alpha(alpha)
1230                    bacts.update({value_name: mesh})  # store it
1231
1232                self.remove(prevact).add(mesh)
1233                self.vol_actors[0] = mesh
1234
1235            ################################################
1236
1237            if isovalue is None:
1238                isovalue = delta / 3.0 + scrange[0]
1239
1240            self.vol_actors = [None]
1241            slider_isovalue(isovalue, "")  # init call
1242            if lego:
1243                if self.vol_actors[0]:
1244                    self.vol_actors[0].add_scalarbar(pos=(0.8, 0.12))
1245
1246            self.slider = self.add_slider(
1247                slider_isovalue,
1248                scrange[0] + 0.02 * delta,
1249                scrange[1] - 0.02 * delta,
1250                value=isovalue,
1251                pos=sliderpos,
1252                title=slidertitle,
1253                show_value=True,
1254                delayed=delayed,
1255            )
1256
1257
1258##############################################################################
1259class Browser(Plotter):
1260    """Browse a series of vedo objects by using a simple slider."""
1261
1262    def __init__(
1263        self,
1264        objects=(),
1265        sliderpos=((0.50, 0.07), (0.95, 0.07)),
1266        c=None,  # slider color
1267        slider_title="",
1268        font="Calco",  # slider font
1269        resetcam=False,  # resetcam while using the slider
1270        **kwargs,
1271    ):
1272        """
1273        Browse a series of vedo objects by using a simple slider.
1274
1275        The input object can be a list of objects or a list of lists of objects.
1276
1277        Arguments:
1278            objects : (list)
1279                list of objects to be browsed.
1280            sliderpos : (list)
1281                position of the slider.
1282            c : (str)
1283                color of the slider.
1284            slider_title : (str)
1285                title of the slider.
1286            font : (str)
1287                font of the slider.
1288            resetcam : (bool)
1289                resetcam while using the slider.
1290            **kwargs : (dict)
1291                keyword arguments to pass to a `vedo.plotter.Plotter` instance.
1292
1293        Examples:
1294            ```python
1295            from vedo import load, dataurl
1296            from vedo.applications import Browser
1297            meshes = load(dataurl+'timecourse1d.npy') # python list of Meshes
1298            plt = Browser(meshes, bg='k')             # vedo.Plotter
1299            plt.show(interactive=False, zoom='tight') # show the meshes
1300            plt.play(dt=50)                           # delay in milliseconds
1301            plt.close()
1302            ```
1303
1304        - [morphomatics_tube.py](https://github.com/marcomusy/vedo/tree/master/examples/other/morphomatics_tube.py)
1305        """
1306        kwargs.pop("N", 1)
1307        kwargs.pop("shape", [])
1308        kwargs.pop("axes", 1)
1309        super().__init__(**kwargs)
1310
1311        if isinstance(objects, str):
1312            objects = vedo.file_io.load(objects)
1313
1314        self += objects
1315
1316        if len(objects)>0 and is_sequence(objects[0]):
1317            nobs = len(objects[0])
1318            for ob in objects:
1319                n = len(ob)
1320                msg = f"in Browser lists must have the same length but found {n} and {nobs}"
1321                assert len(ob) == nobs, msg
1322        else:
1323            nobs = len(objects)
1324            if nobs:
1325                objects = [objects]
1326
1327        self.slider = None
1328        self.timer_callback_id = None
1329        self._oldk = None
1330
1331        # define the slider func ##########################
1332        def slider_function(_widget=None, _event=None):
1333
1334            k = int(self.slider.value)
1335
1336            if k == self._oldk:
1337                return  # no change
1338            self._oldk = k
1339
1340            n = len(objects)
1341            m = len(objects[0])
1342            for i in range(n):
1343                for j in range(m):
1344                    ak = objects[i][j]
1345                    try:
1346                        if j == k:
1347                            ak.on()
1348                            akon = ak
1349                        else:
1350                            ak.off()
1351                    except AttributeError:
1352                        pass
1353
1354            try:
1355                tx = str(k)
1356                if slider_title:
1357                    tx = slider_title + " " + tx
1358                elif n == 1 and akon.filename:
1359                    tx = akon.filename.split("/")[-1]
1360                    tx = tx.split("\\")[-1]  # windows os
1361                elif akon.name:
1362                    tx = ak.name + " " + tx
1363            except:
1364                pass
1365            self.slider.title = tx
1366
1367            if resetcam:
1368                self.reset_camera()
1369            self.render()
1370
1371        ##################################################
1372
1373        self.slider_function = slider_function
1374        self.slider = self.add_slider(
1375            slider_function,
1376            0.5,
1377            nobs - 0.5,
1378            pos=sliderpos,
1379            font=font,
1380            c=c,
1381            show_value=False,
1382        )
1383        self.slider.GetRepresentation().SetTitleHeight(0.020)
1384        slider_function()  # init call
1385
1386    def play(self, dt=100):
1387        """Start playing the slides at a given speed."""
1388        self.timer_callback_id = self.add_callback("timer", self.slider_function)
1389        self.timer_callback("start", dt=dt)
1390        self.interactive()
1391
1392
1393#############################################################################################
1394class FreeHandCutPlotter(Plotter):
1395    """A tool to edit meshes interactively."""
1396
1397    # thanks to Jakub Kaminski for the original version of this script
1398    def __init__(
1399        self,
1400        mesh: Union[vedo.Mesh, vedo.Points],
1401        splined=True,
1402        font="Bongas",
1403        alpha=0.9,
1404        lw=4,
1405        lc="red5",
1406        pc="red4",
1407        c="green3",
1408        tc="k9",
1409        tol=0.008,
1410        **options,
1411    ):
1412        """
1413        A `vedo.Plotter` derived class which edits polygonal meshes interactively.
1414
1415        Can also be invoked from command line with:
1416
1417        ```bash
1418        vedo --edit https://vedo.embl.es/examples/data/porsche.ply
1419        ```
1420
1421        Usage:
1422            - Left-click and hold to rotate
1423            - Right-click and move to draw line
1424            - Second right-click to stop drawing
1425            - Press "c" to clear points
1426            -       "z/Z" to cut mesh (Z inverts inside-out the selection area)
1427            -       "L" to keep only the largest connected surface
1428            -       "s" to save mesh to file (tag `_edited` is appended to filename)
1429            -       "u" to undo last action
1430            -       "h" for help, "i" for info
1431
1432        Arguments:
1433            mesh : (Mesh, Points)
1434                The input Mesh or pointcloud.
1435            splined : (bool)
1436                join points with a spline or a simple line.
1437            font : (str)
1438                Font name for the instructions.
1439            alpha : (float)
1440                transparency of the instruction message panel.
1441            lw : (str)
1442                selection line width.
1443            lc : (str)
1444                selection line color.
1445            pc : (str)
1446                selection points color.
1447            c : (str)
1448                background color of instructions.
1449            tc : (str)
1450                text color of instructions.
1451            tol : (int)
1452                tolerance of the point proximity.
1453            **kwargs : (dict)
1454                keyword arguments to pass to a `vedo.plotter.Plotter` instance.
1455
1456        Examples:
1457            - [cut_freehand.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/cut_freehand.py)
1458
1459                ![](https://vedo.embl.es/images/basic/cutFreeHand.gif)
1460        """
1461
1462        if not isinstance(mesh, Points):
1463            vedo.logger.error("FreeHandCutPlotter input must be Points or Mesh")
1464            raise RuntimeError()
1465
1466        super().__init__(**options)
1467
1468        self.mesh = mesh
1469        self.mesh_prev = mesh
1470        self.splined = splined
1471        self.linecolor = lc
1472        self.linewidth = lw
1473        self.pointcolor = pc
1474        self.color = c
1475        self.alpha = alpha
1476
1477        self.msg = "Right-click and move to draw line\n"
1478        self.msg += "Second right-click to stop drawing\n"
1479        self.msg += "Press L to extract largest surface\n"
1480        self.msg += "        z/Z to cut mesh (s to save)\n"
1481        self.msg += "        c to clear points, u to undo"
1482        self.txt2d = Text2D(self.msg, pos="top-left", font=font, s=0.9)
1483        self.txt2d.c(tc).background(c, alpha).frame()
1484
1485        self.idkeypress = self.add_callback("KeyPress", self._on_keypress)
1486        self.idrightclck = self.add_callback("RightButton", self._on_right_click)
1487        self.idmousemove = self.add_callback("MouseMove", self._on_mouse_move)
1488        self.drawmode = False
1489        self.tol = tol  # tolerance of point distance
1490        self.cpoints = []
1491        self.points = None
1492        self.spline = None
1493        self.jline = None
1494        self.topline = None
1495        self.top_pts = []
1496
1497    def init(self, init_points):
1498        """Set an initial number of points to define a region"""
1499        if isinstance(init_points, Points):
1500            self.cpoints = init_points.coordinates
1501        else:
1502            self.cpoints = np.array(init_points)
1503        self.points = Points(self.cpoints, r=self.linewidth).c(self.pointcolor).pickable(0)
1504        if self.splined:
1505            self.spline = Spline(self.cpoints, res=len(self.cpoints) * 4)
1506        else:
1507            self.spline = Line(self.cpoints)
1508        self.spline.lw(self.linewidth).c(self.linecolor).pickable(False)
1509        self.jline = Line(self.cpoints[0], self.cpoints[-1], lw=1, c=self.linecolor).pickable(0)
1510        self.add([self.points, self.spline, self.jline]).render()
1511        return self
1512
1513    def _on_right_click(self, _evt):
1514        self.drawmode = not self.drawmode  # toggle mode
1515        if self.drawmode:
1516            self.txt2d.background(self.linecolor, self.alpha)
1517        else:
1518            self.txt2d.background(self.color, self.alpha)
1519            if len(self.cpoints) > 2:
1520                self.remove([self.spline, self.jline])
1521                if self.splined:  # show the spline closed
1522                    self.spline = Spline(self.cpoints, closed=True, res=len(self.cpoints) * 4)
1523                else:
1524                    self.spline = Line(self.cpoints, closed=True)
1525                self.spline.lw(self.linewidth).c(self.linecolor).pickable(False)
1526                self.add(self.spline)
1527        self.render()
1528
1529    def _on_mouse_move(self, evt):
1530        if self.drawmode:
1531            cpt = self.compute_world_coordinate(evt.picked2d)  # make this 2d-screen point 3d
1532            if self.cpoints and mag(cpt - self.cpoints[-1]) < self.mesh.diagonal_size() * self.tol:
1533                return  # new point is too close to the last one. skip
1534            self.cpoints.append(cpt)
1535            if len(self.cpoints) > 2:
1536                self.remove([self.points, self.spline, self.jline, self.topline])
1537                self.points = Points(self.cpoints, r=self.linewidth).c(self.pointcolor).pickable(0)
1538                if self.splined:
1539                    self.spline = Spline(self.cpoints, res=len(self.cpoints) * 4)  # not closed here
1540                else:
1541                    self.spline = Line(self.cpoints)
1542
1543                if evt.actor:
1544                    self.top_pts.append(evt.picked3d)
1545                    self.topline = Points(self.top_pts, r=self.linewidth)
1546                    self.topline.c(self.linecolor).pickable(False)
1547
1548                self.spline.lw(self.linewidth).c(self.linecolor).pickable(False)
1549                self.txt2d.background(self.linecolor)
1550                self.jline = Line(self.cpoints[0], self.cpoints[-1], lw=1, c=self.linecolor).pickable(0)
1551                self.add([self.points, self.spline, self.jline, self.topline]).render()
1552
1553    def _on_keypress(self, evt):
1554        if evt.keypress.lower() == "z" and self.spline:  # Cut mesh with a ribbon-like surface
1555            inv = False
1556            if evt.keypress == "Z":
1557                inv = True
1558            self.txt2d.background("red8").text("  ... working ...  ")
1559            self.render()
1560            self.mesh_prev = self.mesh.clone()
1561            tol = self.mesh.diagonal_size() / 2  # size of ribbon (not shown)
1562            pts = self.spline.coordinates
1563            n = fit_plane(pts, signed=True).normal  # compute normal vector to points
1564            rb = Ribbon(pts - tol * n, pts + tol * n, closed=True)
1565            self.mesh.cut_with_mesh(rb, invert=inv)  # CUT
1566            self.txt2d.text(self.msg)  # put back original message
1567            if self.drawmode:
1568                self._on_right_click(evt)  # toggle mode to normal
1569            else:
1570                self.txt2d.background(self.color, self.alpha)
1571            self.remove([self.spline, self.points, self.jline, self.topline]).render()
1572            self.cpoints, self.points, self.spline = [], None, None
1573            self.top_pts, self.topline = [], None
1574
1575        elif evt.keypress == "L":
1576            self.txt2d.background("red8")
1577            self.txt2d.text(" ... removing smaller ... \n ... parts of the mesh ... ")
1578            self.render()
1579            self.remove(self.mesh)
1580            self.mesh_prev = self.mesh
1581            mcut = self.mesh.extract_largest_region()
1582            mcut.filename = self.mesh.filename  # copy over various properties
1583            mcut.name = self.mesh.name
1584            mcut.scalarbar = self.mesh.scalarbar
1585            mcut.info = self.mesh.info
1586            self.mesh = mcut                            # discard old mesh by overwriting it
1587            self.txt2d.text(self.msg).background(self.color)   # put back original message
1588            self.add(mcut).render()
1589
1590        elif evt.keypress == "u":  # Undo last action
1591            if self.drawmode:
1592                self._on_right_click(evt)  # toggle mode to normal
1593            else:
1594                self.txt2d.background(self.color, self.alpha)
1595            self.remove([self.mesh, self.spline, self.jline, self.points, self.topline])
1596            self.mesh = self.mesh_prev
1597            self.cpoints, self.points, self.spline = [], None, None
1598            self.top_pts, self.topline = [], None
1599            self.add(self.mesh).render()
1600
1601        elif evt.keypress in ("c", "Delete"):
1602            # clear all points
1603            self.remove([self.spline, self.points, self.jline, self.topline]).render()
1604            self.cpoints, self.points, self.spline = [], None, None
1605            self.top_pts, self.topline = [], None
1606
1607        elif evt.keypress == "r":  # reset camera and axes
1608            self.remove(self.axes_instances[0])
1609            self.axes_instances[0] = None
1610            self.add_global_axes(axtype=1, c=None)
1611            self.renderer.ResetCamera()
1612            self.render()
1613
1614        elif evt.keypress == "s":
1615            if self.mesh.filename:
1616                fname = os.path.basename(self.mesh.filename)
1617                fname, extension = os.path.splitext(fname)
1618                fname = fname.replace("_edited", "")
1619                fname = f"{fname}_edited{extension}"
1620            else:
1621                fname = "mesh_edited.vtk"
1622            self.write(fname)
1623
1624    def write(self, filename="mesh_edited.vtk") -> "FreeHandCutPlotter":
1625        """Save the resulting mesh to file"""
1626        self.mesh.write(filename)
1627        vedo.logger.info(f"mesh saved to file {filename}")
1628        return self
1629
1630    def start(self, *args, **kwargs) -> "FreeHandCutPlotter":
1631        """Start window interaction (with mouse and keyboard)"""
1632        acts = [self.txt2d, self.mesh, self.points, self.spline, self.jline]
1633        self.show(acts + list(args), **kwargs)
1634        return self
1635
1636
1637########################################################################
1638class SplinePlotter(Plotter):
1639    """
1640    Interactive drawing of splined curves on meshes.
1641    """
1642
1643    def __init__(self, obj, init_points=(), closed=False, splined=True, mode="auto", **kwargs):
1644        """
1645        Create an interactive application that allows the user to click points and
1646        retrieve the coordinates of such points and optionally a spline or line
1647        (open or closed).
1648        Input object can be a image file name or a 3D mesh.
1649
1650        Arguments:
1651            obj : (Mesh, str)
1652                The input object can be a image file name or a 3D mesh.
1653            init_points : (list)
1654                Set an initial number of points to define a region.
1655            closed : (bool)
1656                Close the spline or line.
1657            splined : (bool)
1658                Join points with a spline or a simple line.
1659            mode : (str)
1660                Set the mode of interaction.
1661            **kwargs : (dict)
1662                keyword arguments to pass to a `vedo.plotter.Plotter` instance.
1663        """
1664        super().__init__(**kwargs)
1665
1666        self.verbose = True
1667        self.splined = splined
1668        self.resolution = None  # spline resolution (None = automatic)
1669        self.closed = closed
1670        self.lcolor = "yellow4"
1671        self.lwidth = 3
1672        self.pcolor = "purple5"
1673        self.psize = 10
1674
1675        self.cpoints = list(init_points)
1676        self.vpoints = None
1677        self.line = None
1678
1679        if isinstance(obj, str):
1680            self.object = vedo.file_io.load(obj)
1681        else:
1682            self.object = obj
1683
1684        self.mode = mode
1685        if self.mode == "auto":
1686            if isinstance(self.object, vedo.Image):
1687                self.mode = "image"
1688                self.parallel_projection(True)
1689            else:
1690                self.mode = "TrackballCamera"
1691
1692        t = (
1693            "Click to add a point\n"
1694            "Right-click to remove it\n"
1695            "Drag mouse to change contrast\n"
1696            "Press c to clear points\n"
1697            "Press q to continue"
1698        )
1699        self.instructions = Text2D(t, pos="bottom-left", c="white", bg="green", font="Calco")
1700
1701        self += [self.object, self.instructions]
1702
1703        self.callid1 = self.add_callback("KeyPress", self._key_press)
1704        self.callid2 = self.add_callback("LeftButtonPress", self._on_left_click)
1705        self.callid3 = self.add_callback("RightButtonPress", self._on_right_click)
1706
1707
1708    def points(self, newpts=None) -> Union["SplinePlotter", np.ndarray]:
1709        """Retrieve the 3D coordinates of the clicked points"""
1710        if newpts is not None:
1711            self.cpoints = newpts
1712            self.update()
1713            return self
1714        return np.array(self.cpoints)
1715
1716    def _on_left_click(self, evt):
1717        if not evt.actor:
1718            return
1719        if evt.actor.name == "points":
1720            # remove clicked point if clicked twice
1721            pid = self.vpoints.closest_point(evt.picked3d, return_point_id=True)
1722            self.cpoints.pop(pid)
1723            self.update()
1724            return
1725        p = evt.picked3d
1726        self.cpoints.append(p)
1727        self.update()
1728        if self.verbose:
1729            vedo.colors.printc("Added point:", precision(p, 4), c="g")
1730
1731    def _on_right_click(self, evt):
1732        if evt.actor and len(self.cpoints) > 0:
1733            self.cpoints.pop()  # pop removes from the list the last pt
1734            self.update()
1735            if self.verbose:
1736                vedo.colors.printc("Deleted last point", c="r")
1737
1738    def update(self):
1739        """Update the plot with the new points"""
1740        self.remove(self.line, self.vpoints)  # remove old points and spline
1741        self.vpoints = Points(self.cpoints).ps(self.psize).c(self.pcolor)
1742        self.vpoints.name = "points"
1743        self.vpoints.pickable(True)  # to allow toggle
1744        minnr = 1
1745        if self.splined:
1746            minnr = 2
1747        if self.lwidth and len(self.cpoints) > minnr:
1748            if self.splined:
1749                try:
1750                    self.line = Spline(self.cpoints, closed=self.closed, res=self.resolution)
1751                except ValueError:
1752                    # if clicking too close splining might fail
1753                    self.cpoints.pop()
1754                    return
1755            else:
1756                self.line = Line(self.cpoints, closed=self.closed)
1757            self.line.c(self.lcolor).lw(self.lwidth).pickable(False)
1758            self.add(self.vpoints, self.line)
1759        else:
1760            self.add(self.vpoints)
1761
1762    def _key_press(self, evt):
1763        if evt.keypress == "c":
1764            self.cpoints = []
1765            self.remove(self.line, self.vpoints).render()
1766            if self.verbose:
1767                vedo.colors.printc("==== Cleared all points ====", c="r", invert=True)
1768
1769    def start(self) -> "SplinePlotter":
1770        """Start the interaction"""
1771        self.update()
1772        self.show(self.object, self.instructions, mode=self.mode)
1773        return self
1774
1775
1776########################################################################
1777class Animation(Plotter):
1778    """
1779    A `Plotter` derived class that allows to animate simultaneously various objects
1780    by specifying event times and durations of different visual effects.
1781
1782    Arguments:
1783        total_duration : (float)
1784            expand or shrink the total duration of video to this value
1785        time_resolution : (float)
1786            in seconds, save a frame at this rate
1787        show_progressbar : (bool)
1788            whether to show a progress bar or not
1789        video_filename : (str)
1790            output file name of the video
1791        video_fps : (int)
1792            desired value of the nr of frames per second
1793
1794    .. warning:: this is still very experimental at the moment.
1795    """
1796
1797    def __init__(
1798        self,
1799        total_duration=None,
1800        time_resolution=0.02,
1801        show_progressbar=True,
1802        video_filename="animation.mp4",
1803        video_fps=12,
1804    ):
1805        super().__init__()
1806        self.resetcam = True
1807
1808        self.events = []
1809        self.time_resolution = time_resolution
1810        self.total_duration = total_duration
1811        self.show_progressbar = show_progressbar
1812        self.video_filename = video_filename
1813        self.video_fps = video_fps
1814        self.bookingMode = True
1815        self._inputvalues = []
1816        self._performers = []
1817        self._lastT = None
1818        self._lastDuration = None
1819        self._lastActs = None
1820        self.eps = 0.00001
1821
1822    def _parse(self, objs, t, duration):
1823        if t is None:
1824            if self._lastT:
1825                t = self._lastT
1826            else:
1827                t = 0.0
1828        if duration is None:
1829            if self._lastDuration:
1830                duration = self._lastDuration
1831            else:
1832                duration = 0.0
1833        if objs is None:
1834            if self._lastActs:
1835                objs = self._lastActs
1836            else:
1837                vedo.logger.error("Need to specify actors!")
1838                raise RuntimeError
1839
1840        objs2 = objs
1841
1842        if is_sequence(objs):
1843            objs2 = objs
1844        else:
1845            objs2 = [objs]
1846
1847        # quantize time steps and duration
1848        t = int(t / self.time_resolution + 0.5) * self.time_resolution
1849        nsteps = int(duration / self.time_resolution + 0.5)
1850        duration = nsteps * self.time_resolution
1851
1852        rng = np.linspace(t, t + duration, nsteps + 1)
1853
1854        self._lastT = t
1855        self._lastDuration = duration
1856        self._lastActs = objs2
1857
1858        for a in objs2:
1859            if a not in self.objects:
1860                self.objects.append(a)
1861
1862        return objs2, t, duration, rng
1863
1864    def switch_on(self, acts=None, t=None):
1865        """Switch on the input list of meshes."""
1866        return self.fade_in(acts, t, 0)
1867
1868    def switch_off(self, acts=None, t=None):
1869        """Switch off the input list of meshes."""
1870        return self.fade_out(acts, t, 0)
1871
1872    def fade_in(self, acts=None, t=None, duration=None):
1873        """Gradually switch on the input list of meshes by increasing opacity."""
1874        if self.bookingMode:
1875            acts, t, duration, rng = self._parse(acts, t, duration)
1876            for tt in rng:
1877                alpha = lin_interpolate(tt, [t, t + duration], [0, 1])
1878                self.events.append((tt, self.fade_in, acts, alpha))
1879        else:
1880            for a in self._performers:
1881                if hasattr(a, "alpha"):
1882                    if a.alpha() >= self._inputvalues:
1883                        continue
1884                    a.alpha(self._inputvalues)
1885        return self
1886
1887    def fade_out(self, acts=None, t=None, duration=None):
1888        """Gradually switch off the input list of meshes by increasing transparency."""
1889        if self.bookingMode:
1890            acts, t, duration, rng = self._parse(acts, t, duration)
1891            for tt in rng:
1892                alpha = lin_interpolate(tt, [t, t + duration], [1, 0])
1893                self.events.append((tt, self.fade_out, acts, alpha))
1894        else:
1895            for a in self._performers:
1896                if a.alpha() <= self._inputvalues:
1897                    continue
1898                a.alpha(self._inputvalues)
1899        return self
1900
1901    def change_alpha_between(self, alpha1, alpha2, acts=None, t=None, duration=None):
1902        """Gradually change transparency for the input list of meshes."""
1903        if self.bookingMode:
1904            acts, t, duration, rng = self._parse(acts, t, duration)
1905            for tt in rng:
1906                alpha = lin_interpolate(tt, [t, t + duration], [alpha1, alpha2])
1907                self.events.append((tt, self.fade_out, acts, alpha))
1908        else:
1909            for a in self._performers:
1910                a.alpha(self._inputvalues)
1911        return self
1912
1913    def change_color(self, c, acts=None, t=None, duration=None):
1914        """Gradually change color for the input list of meshes."""
1915        if self.bookingMode:
1916            acts, t, duration, rng = self._parse(acts, t, duration)
1917
1918            col2 = get_color(c)
1919            for tt in rng:
1920                inputvalues = []
1921                for a in acts:
1922                    col1 = a.color()
1923                    r = lin_interpolate(tt, [t, t + duration], [col1[0], col2[0]])
1924                    g = lin_interpolate(tt, [t, t + duration], [col1[1], col2[1]])
1925                    b = lin_interpolate(tt, [t, t + duration], [col1[2], col2[2]])
1926                    inputvalues.append((r, g, b))
1927                self.events.append((tt, self.change_color, acts, inputvalues))
1928        else:
1929            for i, a in enumerate(self._performers):
1930                a.color(self._inputvalues[i])
1931        return self
1932
1933    def change_backcolor(self, c, acts=None, t=None, duration=None):
1934        """Gradually change backface color for the input list of meshes.
1935        An initial backface color should be set in advance."""
1936        if self.bookingMode:
1937            acts, t, duration, rng = self._parse(acts, t, duration)
1938
1939            col2 = get_color(c)
1940            for tt in rng:
1941                inputvalues = []
1942                for a in acts:
1943                    if a.GetBackfaceProperty():
1944                        col1 = a.backColor()
1945                        r = lin_interpolate(tt, [t, t + duration], [col1[0], col2[0]])
1946                        g = lin_interpolate(tt, [t, t + duration], [col1[1], col2[1]])
1947                        b = lin_interpolate(tt, [t, t + duration], [col1[2], col2[2]])
1948                        inputvalues.append((r, g, b))
1949                    else:
1950                        inputvalues.append(None)
1951                self.events.append((tt, self.change_backcolor, acts, inputvalues))
1952        else:
1953            for i, a in enumerate(self._performers):
1954                a.backColor(self._inputvalues[i])
1955        return self
1956
1957    def change_to_wireframe(self, acts=None, t=None):
1958        """Switch representation to wireframe for the input list of meshes at time `t`."""
1959        if self.bookingMode:
1960            acts, t, _, _ = self._parse(acts, t, None)
1961            self.events.append((t, self.change_to_wireframe, acts, True))
1962        else:
1963            for a in self._performers:
1964                a.wireframe(self._inputvalues)
1965        return self
1966
1967    def change_to_surface(self, acts=None, t=None):
1968        """Switch representation to surface for the input list of meshes at time `t`."""
1969        if self.bookingMode:
1970            acts, t, _, _ = self._parse(acts, t, None)
1971            self.events.append((t, self.change_to_surface, acts, False))
1972        else:
1973            for a in self._performers:
1974                a.wireframe(self._inputvalues)
1975        return self
1976
1977    def change_line_width(self, lw, acts=None, t=None, duration=None):
1978        """Gradually change line width of the mesh edges for the input list of meshes."""
1979        if self.bookingMode:
1980            acts, t, duration, rng = self._parse(acts, t, duration)
1981            for tt in rng:
1982                inputvalues = []
1983                for a in acts:
1984                    newlw = lin_interpolate(tt, [t, t + duration], [a.lw(), lw])
1985                    inputvalues.append(newlw)
1986                self.events.append((tt, self.change_line_width, acts, inputvalues))
1987        else:
1988            for i, a in enumerate(self._performers):
1989                a.lw(self._inputvalues[i])
1990        return self
1991
1992    def change_line_color(self, c, acts=None, t=None, duration=None):
1993        """Gradually change line color of the mesh edges for the input list of meshes."""
1994        if self.bookingMode:
1995            acts, t, duration, rng = self._parse(acts, t, duration)
1996            col2 = get_color(c)
1997            for tt in rng:
1998                inputvalues = []
1999                for a in acts:
2000                    col1 = a.linecolor()
2001                    r = lin_interpolate(tt, [t, t + duration], [col1[0], col2[0]])
2002                    g = lin_interpolate(tt, [t, t + duration], [col1[1], col2[1]])
2003                    b = lin_interpolate(tt, [t, t + duration], [col1[2], col2[2]])
2004                    inputvalues.append((r, g, b))
2005                self.events.append((tt, self.change_line_color, acts, inputvalues))
2006        else:
2007            for i, a in enumerate(self._performers):
2008                a.linecolor(self._inputvalues[i])
2009        return self
2010
2011    def change_lighting(self, style, acts=None, t=None, duration=None):
2012        """Gradually change the lighting style for the input list of meshes.
2013
2014        Allowed styles are: [metallic, plastic, shiny, glossy, default].
2015        """
2016        if self.bookingMode:
2017            acts, t, duration, rng = self._parse(acts, t, duration)
2018
2019            c = (1,1,0.99)
2020            if   style=='metallic': pars = [0.1, 0.3, 1.0, 10, c]
2021            elif style=='plastic' : pars = [0.3, 0.4, 0.3,  5, c]
2022            elif style=='shiny'   : pars = [0.2, 0.6, 0.8, 50, c]
2023            elif style=='glossy'  : pars = [0.1, 0.7, 0.9, 90, c]
2024            elif style=='default' : pars = [0.1, 1.0, 0.05, 5, c]
2025            else:
2026                vedo.logger.error(f"Unknown lighting style {style}")
2027
2028            for tt in rng:
2029                inputvalues = []
2030                for a in acts:
2031                    pr = a.properties
2032                    aa = pr.GetAmbient()
2033                    ad = pr.GetDiffuse()
2034                    asp = pr.GetSpecular()
2035                    aspp = pr.GetSpecularPower()
2036                    naa = lin_interpolate(tt, [t, t + duration], [aa, pars[0]])
2037                    nad = lin_interpolate(tt, [t, t + duration], [ad, pars[1]])
2038                    nasp = lin_interpolate(tt, [t, t + duration], [asp, pars[2]])
2039                    naspp = lin_interpolate(tt, [t, t + duration], [aspp, pars[3]])
2040                    inputvalues.append((naa, nad, nasp, naspp))
2041                self.events.append((tt, self.change_lighting, acts, inputvalues))
2042        else:
2043            for i, a in enumerate(self._performers):
2044                pr = a.properties
2045                vals = self._inputvalues[i]
2046                pr.SetAmbient(vals[0])
2047                pr.SetDiffuse(vals[1])
2048                pr.SetSpecular(vals[2])
2049                pr.SetSpecularPower(vals[3])
2050        return self
2051
2052    def move(self, act=None, pt=(0, 0, 0), t=None, duration=None, style="linear"):
2053        """Smoothly change the position of a specific object to a new point in space."""
2054        if self.bookingMode:
2055            acts, t, duration, rng = self._parse(act, t, duration)
2056            if len(acts) != 1:
2057                vedo.logger.error("in move(), can move only one object.")
2058            cpos = acts[0].pos()
2059            pt = np.array(pt)
2060            dv = (pt - cpos) / len(rng)
2061            for j, tt in enumerate(rng):
2062                i = j + 1
2063                if "quad" in style:
2064                    x = i / len(rng)
2065                    y = x * x
2066                    self.events.append((tt, self.move, acts, cpos + dv * i * y))
2067                else:
2068                    self.events.append((tt, self.move, acts, cpos + dv * i))
2069        else:
2070            self._performers[0].pos(self._inputvalues)
2071        return self
2072
2073    def rotate(self, act=None, axis=(1, 0, 0), angle=0, t=None, duration=None):
2074        """Smoothly rotate a specific object by a specified angle and axis."""
2075        if self.bookingMode:
2076            acts, t, duration, rng = self._parse(act, t, duration)
2077            if len(acts) != 1:
2078                vedo.logger.error("in rotate(), can move only one object.")
2079            for tt in rng:
2080                ang = angle / len(rng)
2081                self.events.append((tt, self.rotate, acts, (axis, ang)))
2082        else:
2083            ax = self._inputvalues[0]
2084            if ax == "x":
2085                self._performers[0].rotate_x(self._inputvalues[1])
2086            elif ax == "y":
2087                self._performers[0].rotate_y(self._inputvalues[1])
2088            elif ax == "z":
2089                self._performers[0].rotate_z(self._inputvalues[1])
2090        return self
2091
2092    def scale(self, acts=None, factor=1, t=None, duration=None):
2093        """Smoothly scale a specific object to a specified scale factor."""
2094        if self.bookingMode:
2095            acts, t, duration, rng = self._parse(acts, t, duration)
2096            for tt in rng:
2097                fac = lin_interpolate(tt, [t, t + duration], [1, factor])
2098                self.events.append((tt, self.scale, acts, fac))
2099        else:
2100            for a in self._performers:
2101                a.scale(self._inputvalues)
2102        return self
2103
2104    def mesh_erode(self, act=None, corner=6, t=None, duration=None):
2105        """Erode a mesh by removing cells that are close to one of the 8 corners
2106        of the bounding box.
2107        """
2108        if self.bookingMode:
2109            acts, t, duration, rng = self._parse(act, t, duration)
2110            if len(acts) != 1:
2111                vedo.logger.error("in meshErode(), can erode only one object.")
2112            diag = acts[0].diagonal_size()
2113            x0, x1, y0, y1, z0, z1 = acts[0].GetBounds()
2114            corners = [
2115                (x0, y0, z0),
2116                (x1, y0, z0),
2117                (x1, y1, z0),
2118                (x0, y1, z0),
2119                (x0, y0, z1),
2120                (x1, y0, z1),
2121                (x1, y1, z1),
2122                (x0, y1, z1),
2123            ]
2124            pcl = acts[0].closest_point(corners[corner])
2125            dmin = np.linalg.norm(pcl - corners[corner])
2126            for tt in rng:
2127                d = lin_interpolate(tt, [t, t + duration], [dmin, diag * 1.01])
2128                if d > 0:
2129                    ids = acts[0].closest_point(corners[corner], radius=d, return_point_id=True)
2130                    if len(ids) <= acts[0].npoints:
2131                        self.events.append((tt, self.mesh_erode, acts, ids))
2132        return self
2133
2134    def play(self):
2135        """Play the internal list of events and save a video."""
2136
2137        self.events = sorted(self.events, key=lambda x: x[0])
2138        self.bookingMode = False
2139
2140        if self.show_progressbar:
2141            pb = vedo.ProgressBar(0, len(self.events), c="g")
2142
2143        if self.total_duration is None:
2144            self.total_duration = self.events[-1][0] - self.events[0][0]
2145
2146        if self.video_filename:
2147            vd = vedo.Video(self.video_filename, fps=self.video_fps, duration=self.total_duration)
2148
2149        ttlast = 0
2150        for e in self.events:
2151
2152            tt, action, self._performers, self._inputvalues = e
2153            action(0, 0)
2154
2155            dt = tt - ttlast
2156            if dt > self.eps:
2157                self.show(interactive=False, resetcam=self.resetcam)
2158                if self.video_filename:
2159                    vd.add_frame()
2160
2161                if dt > self.time_resolution + self.eps:
2162                    if self.video_filename:
2163                        vd.pause(dt)
2164
2165            ttlast = tt
2166
2167            if self.show_progressbar:
2168                pb.print("t=" + str(int(tt * 100) / 100) + "s,  " + action.__name__)
2169
2170        self.show(interactive=False, resetcam=self.resetcam)
2171        if self.video_filename:
2172            vd.add_frame()
2173            vd.close()
2174
2175        self.show(interactive=True, resetcam=self.resetcam)
2176        self.bookingMode = True
2177
2178
2179########################################################################
2180class AnimationPlayer(vedo.Plotter):
2181    """
2182    A Plotter with play/pause, step forward/backward and slider functionalties.
2183    Useful for inspecting time series.
2184
2185    The user has the responsibility to update all actors in the callback function.
2186
2187    Arguments:
2188        func :  (Callable)
2189            a function that passes an integer as input and updates the scene
2190        irange : (tuple)
2191            the range of the integer input representing the time series index
2192        dt : (float)
2193            the time interval between two calls to `func` in milliseconds
2194        loop : (bool)
2195            whether to loop the animation
2196        c : (list, str)
2197            the color of the play/pause button
2198        bc : (list)
2199            the background color of the play/pause button and the slider
2200        button_size : (int)
2201            the size of the play/pause buttons
2202        button_pos : (float, float)
2203            the position of the play/pause buttons as a fraction of the window size
2204        button_gap : (float)
2205            the gap between the buttons
2206        slider_length : (float)
2207            the length of the slider as a fraction of the window size
2208        slider_pos : (float, float)
2209            the position of the slider as a fraction of the window size
2210        kwargs: (dict)
2211            keyword arguments to be passed to `Plotter`
2212
2213    Examples:
2214        - [aspring2_player.py](https://vedo.embl.es/images/simulations/spring_player.gif)
2215    """
2216
2217    # Original class contributed by @mikaeltulldahl (Mikael Tulldahl)
2218
2219    PLAY_SYMBOL        = "    \u23F5   "
2220    PAUSE_SYMBOL       = "   \u23F8   "
2221    ONE_BACK_SYMBOL    = " \u29CF"
2222    ONE_FORWARD_SYMBOL = "\u29D0 "
2223
2224    def __init__(
2225        self,
2226        func,
2227        irange: tuple,
2228        dt: float = 1.0,
2229        loop: bool = True,
2230        c=("white", "white"),
2231        bc=("green3", "red4"),
2232        button_size=25,
2233        button_pos=(0.5, 0.04),
2234        button_gap=0.055,
2235        slider_length=0.5,
2236        slider_pos=(0.5, 0.055),
2237        **kwargs,
2238    ):
2239        super().__init__(**kwargs)
2240
2241        min_value, max_value = np.array(irange).astype(int)
2242        button_pos = np.array(button_pos)
2243        slider_pos = np.array(slider_pos)
2244
2245        self._func = func
2246
2247        self.value = min_value - 1
2248        self.min_value = min_value
2249        self.max_value = max_value
2250        self.dt = max(dt, 1)
2251        self.is_playing = False
2252        self._loop = loop
2253
2254        self.timer_callback_id = self.add_callback(
2255            "timer", self._handle_timer, enable_picking=False
2256        )
2257        self.timer_id = None
2258
2259        self.play_pause_button = self.add_button(
2260            self.toggle,
2261            pos=button_pos,  # x,y fraction from bottom left corner
2262            states=[self.PLAY_SYMBOL, self.PAUSE_SYMBOL],
2263            font="Kanopus",
2264            size=button_size,
2265            bc=bc,
2266        )
2267        self.button_oneback = self.add_button(
2268            self.onebackward,
2269            pos=(-button_gap, 0) + button_pos,
2270            states=[self.ONE_BACK_SYMBOL],
2271            font="Kanopus",
2272            size=button_size,
2273            c=c,
2274            bc=bc,
2275        )
2276        self.button_oneforward = self.add_button(
2277            self.oneforward,
2278            pos=(button_gap, 0) + button_pos,
2279            states=[self.ONE_FORWARD_SYMBOL],
2280            font="Kanopus",
2281            size=button_size,
2282            bc=bc,
2283        )
2284        d = (1 - slider_length) / 2
2285        self.slider: SliderWidget = self.add_slider(
2286            self._slider_callback,
2287            self.min_value,
2288            self.max_value - 1,
2289            value=self.min_value,
2290            pos=[(d - 0.5, 0) + slider_pos, (0.5 - d, 0) + slider_pos],
2291            show_value=False,
2292            c=bc[0],
2293            alpha=1,
2294        )
2295
2296    def pause(self) -> None:
2297        """Pause the animation."""
2298        self.is_playing = False
2299        if self.timer_id is not None:
2300            self.timer_callback("destroy", self.timer_id)
2301            self.timer_id = None
2302        self.play_pause_button.status(self.PLAY_SYMBOL)
2303
2304    def resume(self) -> None:
2305        """Resume the animation."""
2306        if self.timer_id is not None:
2307            self.timer_callback("destroy", self.timer_id)
2308        self.timer_id = self.timer_callback("create", dt=int(self.dt))
2309        self.is_playing = True
2310        self.play_pause_button.status(self.PAUSE_SYMBOL)
2311
2312    def toggle(self, _obj, _evt) -> None:
2313        """Toggle between play and pause."""
2314        if not self.is_playing:
2315            self.resume()
2316        else:
2317            self.pause()
2318
2319    def oneforward(self, _obj, _evt) -> None:
2320        """Advance the animation by one frame."""
2321        self.pause()
2322        self.set_frame(self.value + 1)
2323
2324    def onebackward(self, _obj, _evt) -> None:
2325        """Go back one frame in the animation."""
2326        self.pause()
2327        self.set_frame(self.value - 1)
2328
2329    def set_frame(self, value: int) -> None:
2330        """Set the current value of the animation."""
2331        if self._loop:
2332            if value < self.min_value:
2333                value = self.max_value - 1
2334            elif value >= self.max_value:
2335                value = self.min_value
2336        else:
2337            if value < self.min_value:
2338                self.pause()
2339                value = self.min_value
2340            elif value >= self.max_value - 1:
2341                value = self.max_value - 1
2342                self.pause()
2343
2344        if self.value != value:
2345            self.value = value
2346            self.slider.value = value
2347            self._func(value)
2348
2349    def _slider_callback(self, widget: SliderWidget, _: str) -> None:
2350        self.pause()
2351        self.set_frame(int(round(widget.value)))
2352
2353    def _handle_timer(self, _evt=None) -> None:
2354        self.set_frame(self.value + 1)
2355
2356    def stop(self) -> "AnimationPlayer":
2357        """
2358        Stop the animation timers, remove buttons and slider.
2359        Behave like a normal `Plotter` after this.
2360        """
2361        # stop timer
2362        if self.timer_id is not None:
2363            self.timer_callback("destroy", self.timer_id)
2364            self.timer_id = None
2365
2366        # remove callbacks
2367        self.remove_callback(self.timer_callback_id)
2368
2369        # remove buttons
2370        self.slider.off()
2371        self.renderer.RemoveActor(self.play_pause_button.actor)
2372        self.renderer.RemoveActor(self.button_oneback.actor)
2373        self.renderer.RemoveActor(self.button_oneforward.actor)
2374        return self
2375
2376
2377########################################################################
2378class Clock(vedo.Assembly):
2379    """Create a clock with current time or user provided time."""
2380
2381    def __init__(self, h=None, m=None, s=None, font="Quikhand", title="", c="k"):
2382        """
2383        Create a clock with current time or user provided time.
2384
2385        Arguments:
2386            h : (int)
2387                hours in range [0,23]
2388            m : (int)
2389                minutes in range [0,59]
2390            s : (int)
2391                seconds in range [0,59]
2392            font : (str)
2393                font type
2394            title : (str)
2395                some extra text to show on the clock
2396            c : (str)
2397                color of the numbers
2398
2399        Example:
2400            ```python
2401            import time
2402            from vedo import show
2403            from vedo.applications import Clock
2404            clock = Clock()
2405            plt = show(clock, interactive=False)
2406            for i in range(10):
2407                time.sleep(1)
2408                clock.update()
2409                plt.render()
2410            plt.close()
2411            ```
2412            ![](https://vedo.embl.es/images/feats/clock.png)
2413        """
2414        import time
2415
2416        self.elapsed = 0
2417        self._start = time.time()
2418
2419        wd = ""
2420        if h is None and m is None:
2421            t = time.localtime()
2422            h = t.tm_hour
2423            m = t.tm_min
2424            s = t.tm_sec
2425            if not title:
2426                d = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
2427                wd = f"{d[t.tm_wday]} {t.tm_mday}/{t.tm_mon}/{t.tm_year} "
2428
2429        h = int(h) % 24
2430        m = int(m) % 60
2431        t = (h * 60 + m) / 12 / 60
2432
2433        alpha = 2 * np.pi * t + np.pi / 2
2434        beta = 12 * 2 * np.pi * t + np.pi / 2
2435
2436        x1, y1 = np.cos(alpha), np.sin(alpha)
2437        x2, y2 = np.cos(beta), np.sin(beta)
2438        if s is not None:
2439            s = int(s) % 60
2440            gamma = s * 2 * np.pi / 60 + np.pi / 2
2441            x3, y3 = np.cos(gamma), np.sin(gamma)
2442
2443        ore = Line([0, 0], [x1, y1], lw=14, c="red4").scale(0.5).mirror()
2444        minu = Line([0, 0], [x2, y2], lw=7, c="blue3").scale(0.75).mirror()
2445        secs = None
2446        if s is not None:
2447            secs = Line([0, 0], [x3, y3], lw=1, c="k").scale(0.95).mirror()
2448            secs.z(0.003)
2449        back1 = vedo.shapes.Circle(res=180, c="k5")
2450        back2 = vedo.shapes.Circle(res=12).mirror().scale(0.84).rotate_z(-360 / 12)
2451        labels = back2.labels(range(1, 13), justify="center", font=font, c=c, scale=0.14)
2452        txt = vedo.shapes.Text3D(wd + title, font="VictorMono", justify="top-center", s=0.07, c=c)
2453        txt.pos(0, -0.25, 0.001)
2454        labels.z(0.001)
2455        minu.z(0.002)
2456        super().__init__([back1, labels, ore, minu, secs, txt])
2457        self.name = "Clock"
2458
2459    def update(self, h=None, m=None, s=None) -> "Clock":
2460        """Update clock with current or user time."""
2461        import time
2462        parts = self.unpack()
2463        self.elapsed = time.time() - self._start
2464
2465        if h is None and m is None:
2466            t = time.localtime()
2467            h = t.tm_hour
2468            m = t.tm_min
2469            s = t.tm_sec
2470
2471        h = int(h) % 24
2472        m = int(m) % 60
2473        t = (h * 60 + m) / 12 / 60
2474
2475        alpha = 2 * np.pi * t + np.pi / 2
2476        beta = 12 * 2 * np.pi * t + np.pi / 2
2477
2478        x1, y1 = np.cos(alpha), np.sin(alpha)
2479        x2, y2 = np.cos(beta), np.sin(beta)
2480        if s is not None:
2481            s = int(s) % 60
2482            gamma = s * 2 * np.pi / 60 + np.pi / 2
2483            x3, y3 = np.cos(gamma), np.sin(gamma)
2484
2485        pts2 = parts[2].coordinates
2486        pts2[1] = [-x1 * 0.5, y1 * 0.5, 0.001]
2487        parts[2].coordinates = pts2
2488
2489        pts3 = parts[3].coordinates
2490        pts3[1] = [-x2 * 0.75, y2 * 0.75, 0.002]
2491        parts[3].coordinates = pts3
2492
2493        if s is not None:
2494            pts4 = parts[4].coordinates
2495            pts4[1] = [-x3 * 0.95, y3 * 0.95, 0.003]
2496            parts[4].coordinates = pts4
2497
2498        return self
class Browser(vedo.plotter.Plotter):
1260class Browser(Plotter):
1261    """Browse a series of vedo objects by using a simple slider."""
1262
1263    def __init__(
1264        self,
1265        objects=(),
1266        sliderpos=((0.50, 0.07), (0.95, 0.07)),
1267        c=None,  # slider color
1268        slider_title="",
1269        font="Calco",  # slider font
1270        resetcam=False,  # resetcam while using the slider
1271        **kwargs,
1272    ):
1273        """
1274        Browse a series of vedo objects by using a simple slider.
1275
1276        The input object can be a list of objects or a list of lists of objects.
1277
1278        Arguments:
1279            objects : (list)
1280                list of objects to be browsed.
1281            sliderpos : (list)
1282                position of the slider.
1283            c : (str)
1284                color of the slider.
1285            slider_title : (str)
1286                title of the slider.
1287            font : (str)
1288                font of the slider.
1289            resetcam : (bool)
1290                resetcam while using the slider.
1291            **kwargs : (dict)
1292                keyword arguments to pass to a `vedo.plotter.Plotter` instance.
1293
1294        Examples:
1295            ```python
1296            from vedo import load, dataurl
1297            from vedo.applications import Browser
1298            meshes = load(dataurl+'timecourse1d.npy') # python list of Meshes
1299            plt = Browser(meshes, bg='k')             # vedo.Plotter
1300            plt.show(interactive=False, zoom='tight') # show the meshes
1301            plt.play(dt=50)                           # delay in milliseconds
1302            plt.close()
1303            ```
1304
1305        - [morphomatics_tube.py](https://github.com/marcomusy/vedo/tree/master/examples/other/morphomatics_tube.py)
1306        """
1307        kwargs.pop("N", 1)
1308        kwargs.pop("shape", [])
1309        kwargs.pop("axes", 1)
1310        super().__init__(**kwargs)
1311
1312        if isinstance(objects, str):
1313            objects = vedo.file_io.load(objects)
1314
1315        self += objects
1316
1317        if len(objects)>0 and is_sequence(objects[0]):
1318            nobs = len(objects[0])
1319            for ob in objects:
1320                n = len(ob)
1321                msg = f"in Browser lists must have the same length but found {n} and {nobs}"
1322                assert len(ob) == nobs, msg
1323        else:
1324            nobs = len(objects)
1325            if nobs:
1326                objects = [objects]
1327
1328        self.slider = None
1329        self.timer_callback_id = None
1330        self._oldk = None
1331
1332        # define the slider func ##########################
1333        def slider_function(_widget=None, _event=None):
1334
1335            k = int(self.slider.value)
1336
1337            if k == self._oldk:
1338                return  # no change
1339            self._oldk = k
1340
1341            n = len(objects)
1342            m = len(objects[0])
1343            for i in range(n):
1344                for j in range(m):
1345                    ak = objects[i][j]
1346                    try:
1347                        if j == k:
1348                            ak.on()
1349                            akon = ak
1350                        else:
1351                            ak.off()
1352                    except AttributeError:
1353                        pass
1354
1355            try:
1356                tx = str(k)
1357                if slider_title:
1358                    tx = slider_title + " " + tx
1359                elif n == 1 and akon.filename:
1360                    tx = akon.filename.split("/")[-1]
1361                    tx = tx.split("\\")[-1]  # windows os
1362                elif akon.name:
1363                    tx = ak.name + " " + tx
1364            except:
1365                pass
1366            self.slider.title = tx
1367
1368            if resetcam:
1369                self.reset_camera()
1370            self.render()
1371
1372        ##################################################
1373
1374        self.slider_function = slider_function
1375        self.slider = self.add_slider(
1376            slider_function,
1377            0.5,
1378            nobs - 0.5,
1379            pos=sliderpos,
1380            font=font,
1381            c=c,
1382            show_value=False,
1383        )
1384        self.slider.GetRepresentation().SetTitleHeight(0.020)
1385        slider_function()  # init call
1386
1387    def play(self, dt=100):
1388        """Start playing the slides at a given speed."""
1389        self.timer_callback_id = self.add_callback("timer", self.slider_function)
1390        self.timer_callback("start", dt=dt)
1391        self.interactive()

Browse a series of vedo objects by using a simple slider.

Browser( objects=(), sliderpos=((0.5, 0.07), (0.95, 0.07)), c=None, slider_title='', font='Calco', resetcam=False, **kwargs)
1263    def __init__(
1264        self,
1265        objects=(),
1266        sliderpos=((0.50, 0.07), (0.95, 0.07)),
1267        c=None,  # slider color
1268        slider_title="",
1269        font="Calco",  # slider font
1270        resetcam=False,  # resetcam while using the slider
1271        **kwargs,
1272    ):
1273        """
1274        Browse a series of vedo objects by using a simple slider.
1275
1276        The input object can be a list of objects or a list of lists of objects.
1277
1278        Arguments:
1279            objects : (list)
1280                list of objects to be browsed.
1281            sliderpos : (list)
1282                position of the slider.
1283            c : (str)
1284                color of the slider.
1285            slider_title : (str)
1286                title of the slider.
1287            font : (str)
1288                font of the slider.
1289            resetcam : (bool)
1290                resetcam while using the slider.
1291            **kwargs : (dict)
1292                keyword arguments to pass to a `vedo.plotter.Plotter` instance.
1293
1294        Examples:
1295            ```python
1296            from vedo import load, dataurl
1297            from vedo.applications import Browser
1298            meshes = load(dataurl+'timecourse1d.npy') # python list of Meshes
1299            plt = Browser(meshes, bg='k')             # vedo.Plotter
1300            plt.show(interactive=False, zoom='tight') # show the meshes
1301            plt.play(dt=50)                           # delay in milliseconds
1302            plt.close()
1303            ```
1304
1305        - [morphomatics_tube.py](https://github.com/marcomusy/vedo/tree/master/examples/other/morphomatics_tube.py)
1306        """
1307        kwargs.pop("N", 1)
1308        kwargs.pop("shape", [])
1309        kwargs.pop("axes", 1)
1310        super().__init__(**kwargs)
1311
1312        if isinstance(objects, str):
1313            objects = vedo.file_io.load(objects)
1314
1315        self += objects
1316
1317        if len(objects)>0 and is_sequence(objects[0]):
1318            nobs = len(objects[0])
1319            for ob in objects:
1320                n = len(ob)
1321                msg = f"in Browser lists must have the same length but found {n} and {nobs}"
1322                assert len(ob) == nobs, msg
1323        else:
1324            nobs = len(objects)
1325            if nobs:
1326                objects = [objects]
1327
1328        self.slider = None
1329        self.timer_callback_id = None
1330        self._oldk = None
1331
1332        # define the slider func ##########################
1333        def slider_function(_widget=None, _event=None):
1334
1335            k = int(self.slider.value)
1336
1337            if k == self._oldk:
1338                return  # no change
1339            self._oldk = k
1340
1341            n = len(objects)
1342            m = len(objects[0])
1343            for i in range(n):
1344                for j in range(m):
1345                    ak = objects[i][j]
1346                    try:
1347                        if j == k:
1348                            ak.on()
1349                            akon = ak
1350                        else:
1351                            ak.off()
1352                    except AttributeError:
1353                        pass
1354
1355            try:
1356                tx = str(k)
1357                if slider_title:
1358                    tx = slider_title + " " + tx
1359                elif n == 1 and akon.filename:
1360                    tx = akon.filename.split("/")[-1]
1361                    tx = tx.split("\\")[-1]  # windows os
1362                elif akon.name:
1363                    tx = ak.name + " " + tx
1364            except:
1365                pass
1366            self.slider.title = tx
1367
1368            if resetcam:
1369                self.reset_camera()
1370            self.render()
1371
1372        ##################################################
1373
1374        self.slider_function = slider_function
1375        self.slider = self.add_slider(
1376            slider_function,
1377            0.5,
1378            nobs - 0.5,
1379            pos=sliderpos,
1380            font=font,
1381            c=c,
1382            show_value=False,
1383        )
1384        self.slider.GetRepresentation().SetTitleHeight(0.020)
1385        slider_function()  # init call

Browse a series of vedo objects by using a simple slider.

The input object can be a list of objects or a list of lists of objects.

Arguments:
  • objects : (list) list of objects to be browsed.
  • sliderpos : (list) position of the slider.
  • c : (str) color of the slider.
  • slider_title : (str) title of the slider.
  • font : (str) font of the slider.
  • resetcam : (bool) resetcam while using the slider.
  • **kwargs : (dict) keyword arguments to pass to a vedo.plotter.Plotter instance.
Examples:
from vedo import load, dataurl
from vedo.applications import Browser
meshes = load(dataurl+'timecourse1d.npy') # python list of Meshes
plt = Browser(meshes, bg='k')             # vedo.Plotter
plt.show(interactive=False, zoom='tight') # show the meshes
plt.play(dt=50)                           # delay in milliseconds
plt.close()
def play(self, dt=100):
1387    def play(self, dt=100):
1388        """Start playing the slides at a given speed."""
1389        self.timer_callback_id = self.add_callback("timer", self.slider_function)
1390        self.timer_callback("start", dt=dt)
1391        self.interactive()

Start playing the slides at a given speed.

class IsosurfaceBrowser(vedo.plotter.Plotter):
1074class IsosurfaceBrowser(Plotter):
1075    """
1076    Generate a Volume isosurfacing controlled by a slider.
1077    """
1078
1079    def __init__(
1080        self,
1081        volume: vedo.Volume,
1082        isovalue=None,
1083        scalar_range=(),
1084        c=None,
1085        alpha=1,
1086        lego=False,
1087        res=50,
1088        use_gpu=False,
1089        precompute=False,
1090        cmap="hot",
1091        delayed=False,
1092        sliderpos=4,
1093        **kwargs,
1094    ) -> None:
1095        """
1096        Generate a `vedo.Plotter` for Volume isosurfacing using a slider.
1097
1098        Arguments:
1099            volume : (Volume)
1100                the Volume object to be isosurfaced.
1101            isovalues : (float, list)
1102                isosurface value(s) to be displayed.
1103            scalar_range : (list)
1104                scalar range to be used.
1105            c : str, (list)
1106                color(s) of the isosurface(s).
1107            alpha : (float, list)
1108                opacity of the isosurface(s).
1109            lego : (bool)
1110                if True generate a lego plot instead of a surface.
1111            res : (int)
1112                resolution of the isosurface.
1113            use_gpu : (bool)
1114                use GPU acceleration.
1115            precompute : (bool)
1116                precompute the isosurfaces (so slider browsing will be smoother).
1117            cmap : (str)
1118                color map name to be used.
1119            delayed : (bool)
1120                delay the slider update on mouse release.
1121            sliderpos : (int)
1122                position of the slider.
1123            **kwargs : (dict)
1124                keyword arguments to pass to a `vedo.plotter.Plotter` instance.
1125
1126        Examples:
1127            - [app_isobrowser.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/app_isobrowser.py)
1128
1129                ![](https://vedo.embl.es/images/advanced/app_isobrowser.gif)
1130        """
1131
1132        super().__init__(**kwargs)
1133
1134        self.slider = None
1135
1136        ### GPU ################################
1137        if use_gpu and hasattr(volume.properties, "GetIsoSurfaceValues"):
1138
1139            if len(scalar_range) == 2:
1140                scrange = scalar_range
1141            else:
1142                scrange = volume.scalar_range()
1143            delta = scrange[1] - scrange[0]
1144            if not delta:
1145                return
1146
1147            if isovalue is None:
1148                isovalue = delta / 3.0 + scrange[0]
1149
1150            ### isovalue slider callback
1151            def slider_isovalue(widget, _event):
1152                value = widget.GetRepresentation().GetValue()
1153                isovals.SetValue(0, value)
1154
1155            isovals = volume.properties.GetIsoSurfaceValues()
1156            isovals.SetValue(0, isovalue)
1157            self.add(volume.mode(5).alpha(alpha).cmap(c))
1158
1159            self.slider = self.add_slider(
1160                slider_isovalue,
1161                scrange[0] + 0.02 * delta,
1162                scrange[1] - 0.02 * delta,
1163                value=isovalue,
1164                pos=sliderpos,
1165                title="scalar value",
1166                show_value=True,
1167                delayed=delayed,
1168            )
1169
1170        ### CPU ################################
1171        else:
1172
1173            self._prev_value = 1e30
1174
1175            scrange = volume.scalar_range()
1176            delta = scrange[1] - scrange[0]
1177            if not delta:
1178                return
1179
1180            if lego:
1181                res = int(res / 2)  # because lego is much slower
1182                slidertitle = ""
1183            else:
1184                slidertitle = "scalar value"
1185
1186            allowed_vals = np.linspace(scrange[0], scrange[1], num=res)
1187
1188            bacts = {}  # cache the meshes so we dont need to recompute
1189            if precompute:
1190                delayed = False  # no need to delay the slider in this case
1191
1192                for value in allowed_vals:
1193                    value_name = precision(value, 2)
1194                    if lego:
1195                        mesh = volume.legosurface(vmin=value)
1196                        if mesh.ncells:
1197                            mesh.cmap(cmap, vmin=scrange[0], vmax=scrange[1], on="cells")
1198                    else:
1199                        mesh = volume.isosurface(value).color(c).alpha(alpha)
1200                    bacts.update({value_name: mesh})  # store it
1201
1202            ### isovalue slider callback
1203            def slider_isovalue(widget, _event):
1204
1205                prevact = self.vol_actors[0]
1206                if isinstance(widget, float):
1207                    value = widget
1208                else:
1209                    value = widget.GetRepresentation().GetValue()
1210
1211                # snap to the closest
1212                idx = (np.abs(allowed_vals - value)).argmin()
1213                value = allowed_vals[idx]
1214
1215                if abs(value - self._prev_value) / delta < 0.001:
1216                    return
1217                self._prev_value = value
1218
1219                value_name = precision(value, 2)
1220                if value_name in bacts:  # reusing the already existing mesh
1221                    # print('reusing')
1222                    mesh = bacts[value_name]
1223                else:  # else generate it
1224                    # print('generating', value)
1225                    if lego:
1226                        mesh = volume.legosurface(vmin=value)
1227                        if mesh.ncells:
1228                            mesh.cmap(cmap, vmin=scrange[0], vmax=scrange[1], on="cells")
1229                    else:
1230                        mesh = volume.isosurface(value).color(c).alpha(alpha)
1231                    bacts.update({value_name: mesh})  # store it
1232
1233                self.remove(prevact).add(mesh)
1234                self.vol_actors[0] = mesh
1235
1236            ################################################
1237
1238            if isovalue is None:
1239                isovalue = delta / 3.0 + scrange[0]
1240
1241            self.vol_actors = [None]
1242            slider_isovalue(isovalue, "")  # init call
1243            if lego:
1244                if self.vol_actors[0]:
1245                    self.vol_actors[0].add_scalarbar(pos=(0.8, 0.12))
1246
1247            self.slider = self.add_slider(
1248                slider_isovalue,
1249                scrange[0] + 0.02 * delta,
1250                scrange[1] - 0.02 * delta,
1251                value=isovalue,
1252                pos=sliderpos,
1253                title=slidertitle,
1254                show_value=True,
1255                delayed=delayed,
1256            )

Generate a Volume isosurfacing controlled by a slider.

IsosurfaceBrowser( volume: vedo.volume.Volume, isovalue=None, scalar_range=(), c=None, alpha=1, lego=False, res=50, use_gpu=False, precompute=False, cmap='hot', delayed=False, sliderpos=4, **kwargs)
1079    def __init__(
1080        self,
1081        volume: vedo.Volume,
1082        isovalue=None,
1083        scalar_range=(),
1084        c=None,
1085        alpha=1,
1086        lego=False,
1087        res=50,
1088        use_gpu=False,
1089        precompute=False,
1090        cmap="hot",
1091        delayed=False,
1092        sliderpos=4,
1093        **kwargs,
1094    ) -> None:
1095        """
1096        Generate a `vedo.Plotter` for Volume isosurfacing using a slider.
1097
1098        Arguments:
1099            volume : (Volume)
1100                the Volume object to be isosurfaced.
1101            isovalues : (float, list)
1102                isosurface value(s) to be displayed.
1103            scalar_range : (list)
1104                scalar range to be used.
1105            c : str, (list)
1106                color(s) of the isosurface(s).
1107            alpha : (float, list)
1108                opacity of the isosurface(s).
1109            lego : (bool)
1110                if True generate a lego plot instead of a surface.
1111            res : (int)
1112                resolution of the isosurface.
1113            use_gpu : (bool)
1114                use GPU acceleration.
1115            precompute : (bool)
1116                precompute the isosurfaces (so slider browsing will be smoother).
1117            cmap : (str)
1118                color map name to be used.
1119            delayed : (bool)
1120                delay the slider update on mouse release.
1121            sliderpos : (int)
1122                position of the slider.
1123            **kwargs : (dict)
1124                keyword arguments to pass to a `vedo.plotter.Plotter` instance.
1125
1126        Examples:
1127            - [app_isobrowser.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/app_isobrowser.py)
1128
1129                ![](https://vedo.embl.es/images/advanced/app_isobrowser.gif)
1130        """
1131
1132        super().__init__(**kwargs)
1133
1134        self.slider = None
1135
1136        ### GPU ################################
1137        if use_gpu and hasattr(volume.properties, "GetIsoSurfaceValues"):
1138
1139            if len(scalar_range) == 2:
1140                scrange = scalar_range
1141            else:
1142                scrange = volume.scalar_range()
1143            delta = scrange[1] - scrange[0]
1144            if not delta:
1145                return
1146
1147            if isovalue is None:
1148                isovalue = delta / 3.0 + scrange[0]
1149
1150            ### isovalue slider callback
1151            def slider_isovalue(widget, _event):
1152                value = widget.GetRepresentation().GetValue()
1153                isovals.SetValue(0, value)
1154
1155            isovals = volume.properties.GetIsoSurfaceValues()
1156            isovals.SetValue(0, isovalue)
1157            self.add(volume.mode(5).alpha(alpha).cmap(c))
1158
1159            self.slider = self.add_slider(
1160                slider_isovalue,
1161                scrange[0] + 0.02 * delta,
1162                scrange[1] - 0.02 * delta,
1163                value=isovalue,
1164                pos=sliderpos,
1165                title="scalar value",
1166                show_value=True,
1167                delayed=delayed,
1168            )
1169
1170        ### CPU ################################
1171        else:
1172
1173            self._prev_value = 1e30
1174
1175            scrange = volume.scalar_range()
1176            delta = scrange[1] - scrange[0]
1177            if not delta:
1178                return
1179
1180            if lego:
1181                res = int(res / 2)  # because lego is much slower
1182                slidertitle = ""
1183            else:
1184                slidertitle = "scalar value"
1185
1186            allowed_vals = np.linspace(scrange[0], scrange[1], num=res)
1187
1188            bacts = {}  # cache the meshes so we dont need to recompute
1189            if precompute:
1190                delayed = False  # no need to delay the slider in this case
1191
1192                for value in allowed_vals:
1193                    value_name = precision(value, 2)
1194                    if lego:
1195                        mesh = volume.legosurface(vmin=value)
1196                        if mesh.ncells:
1197                            mesh.cmap(cmap, vmin=scrange[0], vmax=scrange[1], on="cells")
1198                    else:
1199                        mesh = volume.isosurface(value).color(c).alpha(alpha)
1200                    bacts.update({value_name: mesh})  # store it
1201
1202            ### isovalue slider callback
1203            def slider_isovalue(widget, _event):
1204
1205                prevact = self.vol_actors[0]
1206                if isinstance(widget, float):
1207                    value = widget
1208                else:
1209                    value = widget.GetRepresentation().GetValue()
1210
1211                # snap to the closest
1212                idx = (np.abs(allowed_vals - value)).argmin()
1213                value = allowed_vals[idx]
1214
1215                if abs(value - self._prev_value) / delta < 0.001:
1216                    return
1217                self._prev_value = value
1218
1219                value_name = precision(value, 2)
1220                if value_name in bacts:  # reusing the already existing mesh
1221                    # print('reusing')
1222                    mesh = bacts[value_name]
1223                else:  # else generate it
1224                    # print('generating', value)
1225                    if lego:
1226                        mesh = volume.legosurface(vmin=value)
1227                        if mesh.ncells:
1228                            mesh.cmap(cmap, vmin=scrange[0], vmax=scrange[1], on="cells")
1229                    else:
1230                        mesh = volume.isosurface(value).color(c).alpha(alpha)
1231                    bacts.update({value_name: mesh})  # store it
1232
1233                self.remove(prevact).add(mesh)
1234                self.vol_actors[0] = mesh
1235
1236            ################################################
1237
1238            if isovalue is None:
1239                isovalue = delta / 3.0 + scrange[0]
1240
1241            self.vol_actors = [None]
1242            slider_isovalue(isovalue, "")  # init call
1243            if lego:
1244                if self.vol_actors[0]:
1245                    self.vol_actors[0].add_scalarbar(pos=(0.8, 0.12))
1246
1247            self.slider = self.add_slider(
1248                slider_isovalue,
1249                scrange[0] + 0.02 * delta,
1250                scrange[1] - 0.02 * delta,
1251                value=isovalue,
1252                pos=sliderpos,
1253                title=slidertitle,
1254                show_value=True,
1255                delayed=delayed,
1256            )

Generate a vedo.Plotter for Volume isosurfacing using a slider.

Arguments:
  • volume : (Volume) the Volume object to be isosurfaced.
  • isovalues : (float, list) isosurface value(s) to be displayed.
  • scalar_range : (list) scalar range to be used.
  • c : str, (list) color(s) of the isosurface(s).
  • alpha : (float, list) opacity of the isosurface(s).
  • lego : (bool) if True generate a lego plot instead of a surface.
  • res : (int) resolution of the isosurface.
  • use_gpu : (bool) use GPU acceleration.
  • precompute : (bool) precompute the isosurfaces (so slider browsing will be smoother).
  • cmap : (str) color map name to be used.
  • delayed : (bool) delay the slider update on mouse release.
  • sliderpos : (int) position of the slider.
  • **kwargs : (dict) keyword arguments to pass to a vedo.plotter.Plotter instance.
Examples:
class FreeHandCutPlotter(vedo.plotter.Plotter):
1395class FreeHandCutPlotter(Plotter):
1396    """A tool to edit meshes interactively."""
1397
1398    # thanks to Jakub Kaminski for the original version of this script
1399    def __init__(
1400        self,
1401        mesh: Union[vedo.Mesh, vedo.Points],
1402        splined=True,
1403        font="Bongas",
1404        alpha=0.9,
1405        lw=4,
1406        lc="red5",
1407        pc="red4",
1408        c="green3",
1409        tc="k9",
1410        tol=0.008,
1411        **options,
1412    ):
1413        """
1414        A `vedo.Plotter` derived class which edits polygonal meshes interactively.
1415
1416        Can also be invoked from command line with:
1417
1418        ```bash
1419        vedo --edit https://vedo.embl.es/examples/data/porsche.ply
1420        ```
1421
1422        Usage:
1423            - Left-click and hold to rotate
1424            - Right-click and move to draw line
1425            - Second right-click to stop drawing
1426            - Press "c" to clear points
1427            -       "z/Z" to cut mesh (Z inverts inside-out the selection area)
1428            -       "L" to keep only the largest connected surface
1429            -       "s" to save mesh to file (tag `_edited` is appended to filename)
1430            -       "u" to undo last action
1431            -       "h" for help, "i" for info
1432
1433        Arguments:
1434            mesh : (Mesh, Points)
1435                The input Mesh or pointcloud.
1436            splined : (bool)
1437                join points with a spline or a simple line.
1438            font : (str)
1439                Font name for the instructions.
1440            alpha : (float)
1441                transparency of the instruction message panel.
1442            lw : (str)
1443                selection line width.
1444            lc : (str)
1445                selection line color.
1446            pc : (str)
1447                selection points color.
1448            c : (str)
1449                background color of instructions.
1450            tc : (str)
1451                text color of instructions.
1452            tol : (int)
1453                tolerance of the point proximity.
1454            **kwargs : (dict)
1455                keyword arguments to pass to a `vedo.plotter.Plotter` instance.
1456
1457        Examples:
1458            - [cut_freehand.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/cut_freehand.py)
1459
1460                ![](https://vedo.embl.es/images/basic/cutFreeHand.gif)
1461        """
1462
1463        if not isinstance(mesh, Points):
1464            vedo.logger.error("FreeHandCutPlotter input must be Points or Mesh")
1465            raise RuntimeError()
1466
1467        super().__init__(**options)
1468
1469        self.mesh = mesh
1470        self.mesh_prev = mesh
1471        self.splined = splined
1472        self.linecolor = lc
1473        self.linewidth = lw
1474        self.pointcolor = pc
1475        self.color = c
1476        self.alpha = alpha
1477
1478        self.msg = "Right-click and move to draw line\n"
1479        self.msg += "Second right-click to stop drawing\n"
1480        self.msg += "Press L to extract largest surface\n"
1481        self.msg += "        z/Z to cut mesh (s to save)\n"
1482        self.msg += "        c to clear points, u to undo"
1483        self.txt2d = Text2D(self.msg, pos="top-left", font=font, s=0.9)
1484        self.txt2d.c(tc).background(c, alpha).frame()
1485
1486        self.idkeypress = self.add_callback("KeyPress", self._on_keypress)
1487        self.idrightclck = self.add_callback("RightButton", self._on_right_click)
1488        self.idmousemove = self.add_callback("MouseMove", self._on_mouse_move)
1489        self.drawmode = False
1490        self.tol = tol  # tolerance of point distance
1491        self.cpoints = []
1492        self.points = None
1493        self.spline = None
1494        self.jline = None
1495        self.topline = None
1496        self.top_pts = []
1497
1498    def init(self, init_points):
1499        """Set an initial number of points to define a region"""
1500        if isinstance(init_points, Points):
1501            self.cpoints = init_points.coordinates
1502        else:
1503            self.cpoints = np.array(init_points)
1504        self.points = Points(self.cpoints, r=self.linewidth).c(self.pointcolor).pickable(0)
1505        if self.splined:
1506            self.spline = Spline(self.cpoints, res=len(self.cpoints) * 4)
1507        else:
1508            self.spline = Line(self.cpoints)
1509        self.spline.lw(self.linewidth).c(self.linecolor).pickable(False)
1510        self.jline = Line(self.cpoints[0], self.cpoints[-1], lw=1, c=self.linecolor).pickable(0)
1511        self.add([self.points, self.spline, self.jline]).render()
1512        return self
1513
1514    def _on_right_click(self, _evt):
1515        self.drawmode = not self.drawmode  # toggle mode
1516        if self.drawmode:
1517            self.txt2d.background(self.linecolor, self.alpha)
1518        else:
1519            self.txt2d.background(self.color, self.alpha)
1520            if len(self.cpoints) > 2:
1521                self.remove([self.spline, self.jline])
1522                if self.splined:  # show the spline closed
1523                    self.spline = Spline(self.cpoints, closed=True, res=len(self.cpoints) * 4)
1524                else:
1525                    self.spline = Line(self.cpoints, closed=True)
1526                self.spline.lw(self.linewidth).c(self.linecolor).pickable(False)
1527                self.add(self.spline)
1528        self.render()
1529
1530    def _on_mouse_move(self, evt):
1531        if self.drawmode:
1532            cpt = self.compute_world_coordinate(evt.picked2d)  # make this 2d-screen point 3d
1533            if self.cpoints and mag(cpt - self.cpoints[-1]) < self.mesh.diagonal_size() * self.tol:
1534                return  # new point is too close to the last one. skip
1535            self.cpoints.append(cpt)
1536            if len(self.cpoints) > 2:
1537                self.remove([self.points, self.spline, self.jline, self.topline])
1538                self.points = Points(self.cpoints, r=self.linewidth).c(self.pointcolor).pickable(0)
1539                if self.splined:
1540                    self.spline = Spline(self.cpoints, res=len(self.cpoints) * 4)  # not closed here
1541                else:
1542                    self.spline = Line(self.cpoints)
1543
1544                if evt.actor:
1545                    self.top_pts.append(evt.picked3d)
1546                    self.topline = Points(self.top_pts, r=self.linewidth)
1547                    self.topline.c(self.linecolor).pickable(False)
1548
1549                self.spline.lw(self.linewidth).c(self.linecolor).pickable(False)
1550                self.txt2d.background(self.linecolor)
1551                self.jline = Line(self.cpoints[0], self.cpoints[-1], lw=1, c=self.linecolor).pickable(0)
1552                self.add([self.points, self.spline, self.jline, self.topline]).render()
1553
1554    def _on_keypress(self, evt):
1555        if evt.keypress.lower() == "z" and self.spline:  # Cut mesh with a ribbon-like surface
1556            inv = False
1557            if evt.keypress == "Z":
1558                inv = True
1559            self.txt2d.background("red8").text("  ... working ...  ")
1560            self.render()
1561            self.mesh_prev = self.mesh.clone()
1562            tol = self.mesh.diagonal_size() / 2  # size of ribbon (not shown)
1563            pts = self.spline.coordinates
1564            n = fit_plane(pts, signed=True).normal  # compute normal vector to points
1565            rb = Ribbon(pts - tol * n, pts + tol * n, closed=True)
1566            self.mesh.cut_with_mesh(rb, invert=inv)  # CUT
1567            self.txt2d.text(self.msg)  # put back original message
1568            if self.drawmode:
1569                self._on_right_click(evt)  # toggle mode to normal
1570            else:
1571                self.txt2d.background(self.color, self.alpha)
1572            self.remove([self.spline, self.points, self.jline, self.topline]).render()
1573            self.cpoints, self.points, self.spline = [], None, None
1574            self.top_pts, self.topline = [], None
1575
1576        elif evt.keypress == "L":
1577            self.txt2d.background("red8")
1578            self.txt2d.text(" ... removing smaller ... \n ... parts of the mesh ... ")
1579            self.render()
1580            self.remove(self.mesh)
1581            self.mesh_prev = self.mesh
1582            mcut = self.mesh.extract_largest_region()
1583            mcut.filename = self.mesh.filename  # copy over various properties
1584            mcut.name = self.mesh.name
1585            mcut.scalarbar = self.mesh.scalarbar
1586            mcut.info = self.mesh.info
1587            self.mesh = mcut                            # discard old mesh by overwriting it
1588            self.txt2d.text(self.msg).background(self.color)   # put back original message
1589            self.add(mcut).render()
1590
1591        elif evt.keypress == "u":  # Undo last action
1592            if self.drawmode:
1593                self._on_right_click(evt)  # toggle mode to normal
1594            else:
1595                self.txt2d.background(self.color, self.alpha)
1596            self.remove([self.mesh, self.spline, self.jline, self.points, self.topline])
1597            self.mesh = self.mesh_prev
1598            self.cpoints, self.points, self.spline = [], None, None
1599            self.top_pts, self.topline = [], None
1600            self.add(self.mesh).render()
1601
1602        elif evt.keypress in ("c", "Delete"):
1603            # clear all points
1604            self.remove([self.spline, self.points, self.jline, self.topline]).render()
1605            self.cpoints, self.points, self.spline = [], None, None
1606            self.top_pts, self.topline = [], None
1607
1608        elif evt.keypress == "r":  # reset camera and axes
1609            self.remove(self.axes_instances[0])
1610            self.axes_instances[0] = None
1611            self.add_global_axes(axtype=1, c=None)
1612            self.renderer.ResetCamera()
1613            self.render()
1614
1615        elif evt.keypress == "s":
1616            if self.mesh.filename:
1617                fname = os.path.basename(self.mesh.filename)
1618                fname, extension = os.path.splitext(fname)
1619                fname = fname.replace("_edited", "")
1620                fname = f"{fname}_edited{extension}"
1621            else:
1622                fname = "mesh_edited.vtk"
1623            self.write(fname)
1624
1625    def write(self, filename="mesh_edited.vtk") -> "FreeHandCutPlotter":
1626        """Save the resulting mesh to file"""
1627        self.mesh.write(filename)
1628        vedo.logger.info(f"mesh saved to file {filename}")
1629        return self
1630
1631    def start(self, *args, **kwargs) -> "FreeHandCutPlotter":
1632        """Start window interaction (with mouse and keyboard)"""
1633        acts = [self.txt2d, self.mesh, self.points, self.spline, self.jline]
1634        self.show(acts + list(args), **kwargs)
1635        return self

A tool to edit meshes interactively.

FreeHandCutPlotter( mesh: Union[vedo.mesh.Mesh, vedo.pointcloud.Points], splined=True, font='Bongas', alpha=0.9, lw=4, lc='red5', pc='red4', c='green3', tc='k9', tol=0.008, **options)
1399    def __init__(
1400        self,
1401        mesh: Union[vedo.Mesh, vedo.Points],
1402        splined=True,
1403        font="Bongas",
1404        alpha=0.9,
1405        lw=4,
1406        lc="red5",
1407        pc="red4",
1408        c="green3",
1409        tc="k9",
1410        tol=0.008,
1411        **options,
1412    ):
1413        """
1414        A `vedo.Plotter` derived class which edits polygonal meshes interactively.
1415
1416        Can also be invoked from command line with:
1417
1418        ```bash
1419        vedo --edit https://vedo.embl.es/examples/data/porsche.ply
1420        ```
1421
1422        Usage:
1423            - Left-click and hold to rotate
1424            - Right-click and move to draw line
1425            - Second right-click to stop drawing
1426            - Press "c" to clear points
1427            -       "z/Z" to cut mesh (Z inverts inside-out the selection area)
1428            -       "L" to keep only the largest connected surface
1429            -       "s" to save mesh to file (tag `_edited` is appended to filename)
1430            -       "u" to undo last action
1431            -       "h" for help, "i" for info
1432
1433        Arguments:
1434            mesh : (Mesh, Points)
1435                The input Mesh or pointcloud.
1436            splined : (bool)
1437                join points with a spline or a simple line.
1438            font : (str)
1439                Font name for the instructions.
1440            alpha : (float)
1441                transparency of the instruction message panel.
1442            lw : (str)
1443                selection line width.
1444            lc : (str)
1445                selection line color.
1446            pc : (str)
1447                selection points color.
1448            c : (str)
1449                background color of instructions.
1450            tc : (str)
1451                text color of instructions.
1452            tol : (int)
1453                tolerance of the point proximity.
1454            **kwargs : (dict)
1455                keyword arguments to pass to a `vedo.plotter.Plotter` instance.
1456
1457        Examples:
1458            - [cut_freehand.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/cut_freehand.py)
1459
1460                ![](https://vedo.embl.es/images/basic/cutFreeHand.gif)
1461        """
1462
1463        if not isinstance(mesh, Points):
1464            vedo.logger.error("FreeHandCutPlotter input must be Points or Mesh")
1465            raise RuntimeError()
1466
1467        super().__init__(**options)
1468
1469        self.mesh = mesh
1470        self.mesh_prev = mesh
1471        self.splined = splined
1472        self.linecolor = lc
1473        self.linewidth = lw
1474        self.pointcolor = pc
1475        self.color = c
1476        self.alpha = alpha
1477
1478        self.msg = "Right-click and move to draw line\n"
1479        self.msg += "Second right-click to stop drawing\n"
1480        self.msg += "Press L to extract largest surface\n"
1481        self.msg += "        z/Z to cut mesh (s to save)\n"
1482        self.msg += "        c to clear points, u to undo"
1483        self.txt2d = Text2D(self.msg, pos="top-left", font=font, s=0.9)
1484        self.txt2d.c(tc).background(c, alpha).frame()
1485
1486        self.idkeypress = self.add_callback("KeyPress", self._on_keypress)
1487        self.idrightclck = self.add_callback("RightButton", self._on_right_click)
1488        self.idmousemove = self.add_callback("MouseMove", self._on_mouse_move)
1489        self.drawmode = False
1490        self.tol = tol  # tolerance of point distance
1491        self.cpoints = []
1492        self.points = None
1493        self.spline = None
1494        self.jline = None
1495        self.topline = None
1496        self.top_pts = []

A vedo.Plotter derived class which edits polygonal meshes interactively.

Can also be invoked from command line with:

vedo --edit https://vedo.embl.es/examples/data/porsche.ply
Usage:
  • Left-click and hold to rotate
  • Right-click and move to draw line
  • Second right-click to stop drawing
  • Press "c" to clear points
  • "z/Z" to cut mesh (Z inverts inside-out the selection area)
  • "L" to keep only the largest connected surface
  • "s" to save mesh to file (tag _edited is appended to filename)
  • "u" to undo last action
  • "h" for help, "i" for info
Arguments:
  • mesh : (Mesh, Points) The input Mesh or pointcloud.
  • splined : (bool) join points with a spline or a simple line.
  • font : (str) Font name for the instructions.
  • alpha : (float) transparency of the instruction message panel.
  • lw : (str) selection line width.
  • lc : (str) selection line color.
  • pc : (str) selection points color.
  • c : (str) background color of instructions.
  • tc : (str) text color of instructions.
  • tol : (int) tolerance of the point proximity.
  • **kwargs : (dict) keyword arguments to pass to a vedo.plotter.Plotter instance.
Examples:
def init(self, init_points):
1498    def init(self, init_points):
1499        """Set an initial number of points to define a region"""
1500        if isinstance(init_points, Points):
1501            self.cpoints = init_points.coordinates
1502        else:
1503            self.cpoints = np.array(init_points)
1504        self.points = Points(self.cpoints, r=self.linewidth).c(self.pointcolor).pickable(0)
1505        if self.splined:
1506            self.spline = Spline(self.cpoints, res=len(self.cpoints) * 4)
1507        else:
1508            self.spline = Line(self.cpoints)
1509        self.spline.lw(self.linewidth).c(self.linecolor).pickable(False)
1510        self.jline = Line(self.cpoints[0], self.cpoints[-1], lw=1, c=self.linecolor).pickable(0)
1511        self.add([self.points, self.spline, self.jline]).render()
1512        return self

Set an initial number of points to define a region

def write(self, filename='mesh_edited.vtk') -> FreeHandCutPlotter:
1625    def write(self, filename="mesh_edited.vtk") -> "FreeHandCutPlotter":
1626        """Save the resulting mesh to file"""
1627        self.mesh.write(filename)
1628        vedo.logger.info(f"mesh saved to file {filename}")
1629        return self

Save the resulting mesh to file

def start(self, *args, **kwargs) -> FreeHandCutPlotter:
1631    def start(self, *args, **kwargs) -> "FreeHandCutPlotter":
1632        """Start window interaction (with mouse and keyboard)"""
1633        acts = [self.txt2d, self.mesh, self.points, self.spline, self.jline]
1634        self.show(acts + list(args), **kwargs)
1635        return self

Start window interaction (with mouse and keyboard)

class RayCastPlotter(vedo.plotter.Plotter):
 890class RayCastPlotter(Plotter):
 891    """
 892    Generate Volume rendering using ray casting.
 893    """
 894
 895    def __init__(self, volume, **kwargs):
 896        """
 897        Generate a window for Volume rendering using ray casting.
 898
 899        Arguments:
 900            volume : (Volume)
 901                the Volume object to be isosurfaced.
 902            **kwargs : (dict)
 903                keyword arguments to pass to a `vedo.plotter.Plotter` instance.
 904
 905        Returns:
 906            `vedo.Plotter` object.
 907
 908        Examples:
 909            - [app_raycaster.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/app_raycaster.py)
 910
 911            ![](https://vedo.embl.es/images/advanced/app_raycaster.gif)
 912        """
 913
 914        super().__init__(**kwargs)
 915
 916        self.alphaslider0 = 0.33
 917        self.alphaslider1 = 0.66
 918        self.alphaslider2 = 1
 919        self.color_scalarbar = None
 920
 921        self.properties = volume.properties
 922
 923        if volume.dimensions()[2] < 3:
 924            vedo.logger.error("RayCastPlotter: not enough z slices.")
 925            raise RuntimeError
 926
 927        smin, smax = volume.scalar_range()
 928        x0alpha = smin + (smax - smin) * 0.25
 929        x1alpha = smin + (smax - smin) * 0.5
 930        x2alpha = smin + (smax - smin) * 1.0
 931
 932        ############################## color map slider
 933        # Create transfer mapping scalar value to color
 934        cmaps = [
 935            "rainbow", "rainbow_r",
 936            "viridis", "viridis_r",
 937            "bone", "bone_r",
 938            "hot", "hot_r",
 939            "plasma", "plasma_r",
 940            "gist_earth", "gist_earth_r",
 941            "coolwarm", "coolwarm_r",
 942            "tab10_r",
 943        ]
 944        cols_cmaps = []
 945        for cm in cmaps:
 946            cols = color_map(range(0, 21), cm, 0, 20)  # sample 20 colors
 947            cols_cmaps.append(cols)
 948        Ncols = len(cmaps)
 949        csl = "k9"
 950        if sum(get_color(self.background())) > 1.5:
 951            csl = "k1"
 952
 953        def slider_cmap(widget=None, _event=""):
 954            if widget:
 955                k = int(widget.value)
 956                volume.cmap(cmaps[k])
 957            self.remove(self.color_scalarbar)
 958            self.color_scalarbar = vedo.addons.ScalarBar(
 959                volume,
 960                horizontal=True,
 961                pos=[(0.8, 0), (0.97, 0.1)],
 962                font_size=0
 963            )
 964            self.add(self.color_scalarbar)
 965
 966        w1 = self.add_slider(
 967            slider_cmap,
 968            0, Ncols - 1,
 969            value=0,
 970            show_value=False,
 971            c=csl,
 972            pos=[(0.8, 0.05), (0.965, 0.05)],
 973        )
 974        w1.representation.SetTitleHeight(0.018)
 975
 976        ############################## alpha sliders
 977        # Create transfer mapping scalar value to opacity transfer function
 978        def setOTF():
 979            otf = self.properties.GetScalarOpacity()
 980            otf.RemoveAllPoints()
 981            otf.AddPoint(smin, 0.0)
 982            otf.AddPoint(smin + (smax - smin) * 0.1, 0.0)
 983            otf.AddPoint(x0alpha, self.alphaslider0)
 984            otf.AddPoint(x1alpha, self.alphaslider1)
 985            otf.AddPoint(x2alpha, self.alphaslider2)
 986            slider_cmap()
 987
 988        setOTF()  ################
 989
 990        def sliderA0(widget, _event):
 991            self.alphaslider0 = widget.value
 992            setOTF()
 993
 994        self.add_slider(
 995            sliderA0,
 996            0, 1,
 997            value=self.alphaslider0,
 998            pos=[(0.84, 0.1), (0.84, 0.26)],
 999            c=csl,
1000            show_value=0,
1001        )
1002
1003        def sliderA1(widget, _event):
1004            self.alphaslider1 = widget.value
1005            setOTF()
1006
1007        self.add_slider(
1008            sliderA1,
1009            0, 1,
1010            value=self.alphaslider1,
1011            pos=[(0.89, 0.1), (0.89, 0.26)],
1012            c=csl,
1013            show_value=0,
1014        )
1015
1016        def sliderA2(widget, _event):
1017            self.alphaslider2 = widget.value
1018            setOTF()
1019
1020        w2 = self.add_slider(
1021            sliderA2,
1022            0, 1,
1023            value=self.alphaslider2,
1024            pos=[(0.96, 0.1), (0.96, 0.26)],
1025            c=csl,
1026            show_value=0,
1027            title="Opacity Levels",
1028        )
1029        w2.GetRepresentation().SetTitleHeight(0.015)
1030
1031        # add a button
1032        def button_func_mode(_obj, _ename):
1033            s = volume.mode()
1034            snew = (s + 1) % 2
1035            volume.mode(snew)
1036            bum.switch()
1037
1038        bum = self.add_button(
1039            button_func_mode,
1040            pos=(0.89, 0.31),
1041            states=["  composite   ", "max projection"],
1042            c=[ "k3", "k6"],
1043            bc=["k6", "k3"],  # colors of states
1044            font="Calco",
1045            size=18,
1046            bold=0,
1047            italic=False,
1048        )
1049        bum.frame(color="k6")
1050        bum.status(volume.mode())
1051
1052        slider_cmap() ############# init call to create scalarbar
1053
1054        # add histogram of scalar
1055        plot = CornerHistogram(
1056            volume,
1057            bins=25,
1058            logscale=1,
1059            c='k5',
1060            bg='k5',
1061            pos=(0.78, 0.065),
1062            lines=True,
1063            dots=False,
1064            nmax=3.1415e06,  # subsample otherwise is too slow
1065        )
1066
1067        plot.GetPosition2Coordinate().SetValue(0.197, 0.20, 0)
1068        plot.GetXAxisActor2D().SetFontFactor(0.7)
1069        plot.GetProperty().SetOpacity(0.5)
1070        self.add([plot, volume])

Generate Volume rendering using ray casting.

RayCastPlotter(volume, **kwargs)
 895    def __init__(self, volume, **kwargs):
 896        """
 897        Generate a window for Volume rendering using ray casting.
 898
 899        Arguments:
 900            volume : (Volume)
 901                the Volume object to be isosurfaced.
 902            **kwargs : (dict)
 903                keyword arguments to pass to a `vedo.plotter.Plotter` instance.
 904
 905        Returns:
 906            `vedo.Plotter` object.
 907
 908        Examples:
 909            - [app_raycaster.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/app_raycaster.py)
 910
 911            ![](https://vedo.embl.es/images/advanced/app_raycaster.gif)
 912        """
 913
 914        super().__init__(**kwargs)
 915
 916        self.alphaslider0 = 0.33
 917        self.alphaslider1 = 0.66
 918        self.alphaslider2 = 1
 919        self.color_scalarbar = None
 920
 921        self.properties = volume.properties
 922
 923        if volume.dimensions()[2] < 3:
 924            vedo.logger.error("RayCastPlotter: not enough z slices.")
 925            raise RuntimeError
 926
 927        smin, smax = volume.scalar_range()
 928        x0alpha = smin + (smax - smin) * 0.25
 929        x1alpha = smin + (smax - smin) * 0.5
 930        x2alpha = smin + (smax - smin) * 1.0
 931
 932        ############################## color map slider
 933        # Create transfer mapping scalar value to color
 934        cmaps = [
 935            "rainbow", "rainbow_r",
 936            "viridis", "viridis_r",
 937            "bone", "bone_r",
 938            "hot", "hot_r",
 939            "plasma", "plasma_r",
 940            "gist_earth", "gist_earth_r",
 941            "coolwarm", "coolwarm_r",
 942            "tab10_r",
 943        ]
 944        cols_cmaps = []
 945        for cm in cmaps:
 946            cols = color_map(range(0, 21), cm, 0, 20)  # sample 20 colors
 947            cols_cmaps.append(cols)
 948        Ncols = len(cmaps)
 949        csl = "k9"
 950        if sum(get_color(self.background())) > 1.5:
 951            csl = "k1"
 952
 953        def slider_cmap(widget=None, _event=""):
 954            if widget:
 955                k = int(widget.value)
 956                volume.cmap(cmaps[k])
 957            self.remove(self.color_scalarbar)
 958            self.color_scalarbar = vedo.addons.ScalarBar(
 959                volume,
 960                horizontal=True,
 961                pos=[(0.8, 0), (0.97, 0.1)],
 962                font_size=0
 963            )
 964            self.add(self.color_scalarbar)
 965
 966        w1 = self.add_slider(
 967            slider_cmap,
 968            0, Ncols - 1,
 969            value=0,
 970            show_value=False,
 971            c=csl,
 972            pos=[(0.8, 0.05), (0.965, 0.05)],
 973        )
 974        w1.representation.SetTitleHeight(0.018)
 975
 976        ############################## alpha sliders
 977        # Create transfer mapping scalar value to opacity transfer function
 978        def setOTF():
 979            otf = self.properties.GetScalarOpacity()
 980            otf.RemoveAllPoints()
 981            otf.AddPoint(smin, 0.0)
 982            otf.AddPoint(smin + (smax - smin) * 0.1, 0.0)
 983            otf.AddPoint(x0alpha, self.alphaslider0)
 984            otf.AddPoint(x1alpha, self.alphaslider1)
 985            otf.AddPoint(x2alpha, self.alphaslider2)
 986            slider_cmap()
 987
 988        setOTF()  ################
 989
 990        def sliderA0(widget, _event):
 991            self.alphaslider0 = widget.value
 992            setOTF()
 993
 994        self.add_slider(
 995            sliderA0,
 996            0, 1,
 997            value=self.alphaslider0,
 998            pos=[(0.84, 0.1), (0.84, 0.26)],
 999            c=csl,
1000            show_value=0,
1001        )
1002
1003        def sliderA1(widget, _event):
1004            self.alphaslider1 = widget.value
1005            setOTF()
1006
1007        self.add_slider(
1008            sliderA1,
1009            0, 1,
1010            value=self.alphaslider1,
1011            pos=[(0.89, 0.1), (0.89, 0.26)],
1012            c=csl,
1013            show_value=0,
1014        )
1015
1016        def sliderA2(widget, _event):
1017            self.alphaslider2 = widget.value
1018            setOTF()
1019
1020        w2 = self.add_slider(
1021            sliderA2,
1022            0, 1,
1023            value=self.alphaslider2,
1024            pos=[(0.96, 0.1), (0.96, 0.26)],
1025            c=csl,
1026            show_value=0,
1027            title="Opacity Levels",
1028        )
1029        w2.GetRepresentation().SetTitleHeight(0.015)
1030
1031        # add a button
1032        def button_func_mode(_obj, _ename):
1033            s = volume.mode()
1034            snew = (s + 1) % 2
1035            volume.mode(snew)
1036            bum.switch()
1037
1038        bum = self.add_button(
1039            button_func_mode,
1040            pos=(0.89, 0.31),
1041            states=["  composite   ", "max projection"],
1042            c=[ "k3", "k6"],
1043            bc=["k6", "k3"],  # colors of states
1044            font="Calco",
1045            size=18,
1046            bold=0,
1047            italic=False,
1048        )
1049        bum.frame(color="k6")
1050        bum.status(volume.mode())
1051
1052        slider_cmap() ############# init call to create scalarbar
1053
1054        # add histogram of scalar
1055        plot = CornerHistogram(
1056            volume,
1057            bins=25,
1058            logscale=1,
1059            c='k5',
1060            bg='k5',
1061            pos=(0.78, 0.065),
1062            lines=True,
1063            dots=False,
1064            nmax=3.1415e06,  # subsample otherwise is too slow
1065        )
1066
1067        plot.GetPosition2Coordinate().SetValue(0.197, 0.20, 0)
1068        plot.GetXAxisActor2D().SetFontFactor(0.7)
1069        plot.GetProperty().SetOpacity(0.5)
1070        self.add([plot, volume])

Generate a window for Volume rendering using ray casting.

Arguments:
  • volume : (Volume) the Volume object to be isosurfaced.
  • **kwargs : (dict) keyword arguments to pass to a vedo.plotter.Plotter instance.
Returns:

vedo.Plotter object.

Examples:

class Slicer2DPlotter(vedo.plotter.Plotter):
668class Slicer2DPlotter(Plotter):
669    """
670    A single slice of a Volume which always faces the camera,
671    but at the same time can be oriented arbitrarily in space.
672    """
673
674    def __init__(self, vol: vedo.Volume, levels=(None, None), histo_color="red4", **kwargs):
675        """
676        A single slice of a Volume which always faces the camera,
677        but at the same time can be oriented arbitrarily in space.
678
679        Arguments:
680            vol : (Volume)
681                the Volume object to be isosurfaced.
682            levels : (list)
683                window and color levels
684            histo_color : (color)
685                histogram color, use `None` to disable it
686            **kwargs : (dict)
687                keyword arguments to pass to a `vedo.plotter.Plotter` instance.
688
689        <img src="https://vedo.embl.es/images/volumetric/read_volume3.jpg" width="500">
690        """
691
692        if "shape" not in kwargs:
693            custom_shape = [  # define here the 2 rendering rectangle spaces
694                dict(bottomleft=(0.0, 0.0), topright=(1, 1), bg="k9"),  # the full window
695                dict(bottomleft=(0.8, 0.8), topright=(1, 1), bg="k8", bg2="lb"),
696            ]
697            kwargs["shape"] = custom_shape
698
699        if "interactive" not in kwargs:
700            kwargs["interactive"] = True
701
702        super().__init__(**kwargs)
703
704        self.user_mode("image")
705        self.add_callback("KeyPress", self.on_key_press)
706
707        orig_volume = vol.clone(deep=False)
708        self.volume = vol
709
710        self.volume.actor = vtki.new("ImageSlice")
711
712        self.volume.properties = self.volume.actor.GetProperty()
713        self.volume.properties.SetInterpolationTypeToLinear()
714
715        self.volume.mapper = vtki.new("ImageResliceMapper")
716        self.volume.mapper.SetInputData(self.volume.dataset)
717        self.volume.mapper.SliceFacesCameraOn()
718        self.volume.mapper.SliceAtFocalPointOn()
719        self.volume.mapper.SetAutoAdjustImageQuality(False)
720        self.volume.mapper.BorderOff()
721
722        # no argument will grab the existing cmap in vol (or use build_lut())
723        self.lut = None
724        self.cmap()
725
726        if levels[0] and levels[1]:
727            self.lighting(window=levels[0], level=levels[1])
728
729        self.usage_txt = (
730            "H                  :rightarrow Toggle this banner on/off\n"
731            "Left click & drag  :rightarrow Modify luminosity and contrast\n"
732            "SHIFT-Left click   :rightarrow Slice image obliquely\n"
733            "SHIFT-Middle click :rightarrow Slice image perpendicularly\n"
734            "SHIFT-R            :rightarrow Fly to closest cartesian view\n"
735            "SHIFT-U            :rightarrow Toggle parallel projection"
736        )
737
738        self.usage = Text2D(
739            self.usage_txt, font="Calco", pos="top-left", s=0.8, bg="yellow", alpha=0.25
740        )
741
742        hist = None
743        if histo_color is not None:
744            data = self.volume.pointdata[0]
745            arr = data
746            if data.ndim == 1:
747                # try to reduce the number of values to histogram
748                dims = self.volume.dimensions()
749                n = (dims[0] - 1) * (dims[1] - 1) * (dims[2] - 1)
750                n = min(1_000_000, n)
751                arr = np.random.choice(self.volume.pointdata[0], n)
752                hist = vedo.pyplot.histogram(
753                    arr,
754                    bins=12,
755                    logscale=True,
756                    c=histo_color,
757                    ytitle="log_10 (counts)",
758                    axes=dict(text_scale=1.9),
759                ).clone2d(pos="bottom-left", size=0.4)
760
761        axes = kwargs.pop("axes", 7)
762        axe = None
763        if axes == 7:
764            axe = vedo.addons.RulerAxes(
765                orig_volume, xtitle="x - ", ytitle="y - ", ztitle="z - "
766            )
767
768        box = orig_volume.box().alpha(0.25)
769
770        volume_axes_inset = vedo.addons.Axes(
771            box,
772            yzgrid=False,
773            xlabel_size=0,
774            ylabel_size=0,
775            zlabel_size=0,
776            tip_size=0.08,
777            axes_linewidth=3,
778            xline_color="dr",
779            yline_color="dg",
780            zline_color="db",
781            xtitle_color="dr",
782            ytitle_color="dg",
783            ztitle_color="db",
784            xtitle_size=0.1,
785            ytitle_size=0.1,
786            ztitle_size=0.1,
787            title_font="VictorMono",
788        )
789
790        self.at(0).add(self.volume, box, axe, self.usage, hist)
791        self.at(1).add(orig_volume, volume_axes_inset)
792        self.at(0)  # set focus at renderer 0
793
794    ####################################################################
795    def on_key_press(self, evt):
796        """Handle keyboard events"""
797        if evt.keypress == "q":
798            self.break_interaction()
799        elif evt.keypress.lower() == "h":
800            t = self.usage
801            if len(t.text()) > 50:
802                self.usage.text("Press H to show help")
803            else:
804                self.usage.text(self.usage_txt)
805            self.render()
806
807    def cmap(self, lut=None, fix_scalar_range=False) -> "Slicer2DPlotter":
808        """
809        Assign a LUT (Look Up Table) to colorize the slice, leave it `None`
810        to reuse an existing Volume color map.
811        Use "bw" for automatic black and white.
812        """
813        if lut is None and self.lut:
814            self.volume.properties.SetLookupTable(self.lut)
815        elif isinstance(lut, vtki.vtkLookupTable):
816            self.volume.properties.SetLookupTable(lut)
817        elif lut == "bw":
818            self.volume.properties.SetLookupTable(None)
819        self.volume.properties.SetUseLookupTableScalarRange(fix_scalar_range)
820        return self
821
822    def alpha(self, value: float) -> "Slicer2DPlotter":
823        """Set opacity to the slice"""
824        self.volume.properties.SetOpacity(value)
825        return self
826
827    def auto_adjust_quality(self, value=True) -> "Slicer2DPlotter":
828        """Automatically reduce the rendering quality for greater speed when interacting"""
829        self.volume.mapper.SetAutoAdjustImageQuality(value)
830        return self
831
832    def slab(self, thickness=0, mode=0, sample_factor=2) -> "Slicer2DPlotter":
833        """
834        Make a thick slice (slab).
835
836        Arguments:
837            thickness : (float)
838                set the slab thickness, for thick slicing
839            mode : (int)
840                The slab type:
841                    0 = min
842                    1 = max
843                    2 = mean
844                    3 = sum
845            sample_factor : (float)
846                Set the number of slab samples to use as a factor of the number of input slices
847                within the slab thickness. The default value is 2, but 1 will increase speed
848                with very little loss of quality.
849        """
850        self.volume.mapper.SetSlabThickness(thickness)
851        self.volume.mapper.SetSlabType(mode)
852        self.volume.mapper.SetSlabSampleFactor(sample_factor)
853        return self
854
855    def face_camera(self, value=True) -> "Slicer2DPlotter":
856        """Make the slice always face the camera or not."""
857        self.volume.mapper.SetSliceFacesCameraOn(value)
858        return self
859
860    def jump_to_nearest_slice(self, value=True) -> "Slicer2DPlotter":
861        """
862        This causes the slicing to occur at the closest slice to the focal point,
863        instead of the default behavior where a new slice is interpolated between
864        the original slices.
865        Nothing happens if the plane is oblique to the original slices.
866        """
867        self.volume.mapper.SetJumpToNearestSlice(value)
868        return self
869
870    def fill_background(self, value=True) -> "Slicer2DPlotter":
871        """
872        Instead of rendering only to the image border,
873        render out to the viewport boundary with the background color.
874        The background color will be the lowest color on the lookup
875        table that is being used for the image.
876        """
877        self.volume.mapper.SetBackground(value)
878        return self
879
880    def lighting(self, window, level, ambient=1.0, diffuse=0.0) -> "Slicer2DPlotter":
881        """Assign the values for window and color level."""
882        self.volume.properties.SetColorWindow(window)
883        self.volume.properties.SetColorLevel(level)
884        self.volume.properties.SetAmbient(ambient)
885        self.volume.properties.SetDiffuse(diffuse)
886        return self

A single slice of a Volume which always faces the camera, but at the same time can be oriented arbitrarily in space.

Slicer2DPlotter( vol: vedo.volume.Volume, levels=(None, None), histo_color='red4', **kwargs)
674    def __init__(self, vol: vedo.Volume, levels=(None, None), histo_color="red4", **kwargs):
675        """
676        A single slice of a Volume which always faces the camera,
677        but at the same time can be oriented arbitrarily in space.
678
679        Arguments:
680            vol : (Volume)
681                the Volume object to be isosurfaced.
682            levels : (list)
683                window and color levels
684            histo_color : (color)
685                histogram color, use `None` to disable it
686            **kwargs : (dict)
687                keyword arguments to pass to a `vedo.plotter.Plotter` instance.
688
689        <img src="https://vedo.embl.es/images/volumetric/read_volume3.jpg" width="500">
690        """
691
692        if "shape" not in kwargs:
693            custom_shape = [  # define here the 2 rendering rectangle spaces
694                dict(bottomleft=(0.0, 0.0), topright=(1, 1), bg="k9"),  # the full window
695                dict(bottomleft=(0.8, 0.8), topright=(1, 1), bg="k8", bg2="lb"),
696            ]
697            kwargs["shape"] = custom_shape
698
699        if "interactive" not in kwargs:
700            kwargs["interactive"] = True
701
702        super().__init__(**kwargs)
703
704        self.user_mode("image")
705        self.add_callback("KeyPress", self.on_key_press)
706
707        orig_volume = vol.clone(deep=False)
708        self.volume = vol
709
710        self.volume.actor = vtki.new("ImageSlice")
711
712        self.volume.properties = self.volume.actor.GetProperty()
713        self.volume.properties.SetInterpolationTypeToLinear()
714
715        self.volume.mapper = vtki.new("ImageResliceMapper")
716        self.volume.mapper.SetInputData(self.volume.dataset)
717        self.volume.mapper.SliceFacesCameraOn()
718        self.volume.mapper.SliceAtFocalPointOn()
719        self.volume.mapper.SetAutoAdjustImageQuality(False)
720        self.volume.mapper.BorderOff()
721
722        # no argument will grab the existing cmap in vol (or use build_lut())
723        self.lut = None
724        self.cmap()
725
726        if levels[0] and levels[1]:
727            self.lighting(window=levels[0], level=levels[1])
728
729        self.usage_txt = (
730            "H                  :rightarrow Toggle this banner on/off\n"
731            "Left click & drag  :rightarrow Modify luminosity and contrast\n"
732            "SHIFT-Left click   :rightarrow Slice image obliquely\n"
733            "SHIFT-Middle click :rightarrow Slice image perpendicularly\n"
734            "SHIFT-R            :rightarrow Fly to closest cartesian view\n"
735            "SHIFT-U            :rightarrow Toggle parallel projection"
736        )
737
738        self.usage = Text2D(
739            self.usage_txt, font="Calco", pos="top-left", s=0.8, bg="yellow", alpha=0.25
740        )
741
742        hist = None
743        if histo_color is not None:
744            data = self.volume.pointdata[0]
745            arr = data
746            if data.ndim == 1:
747                # try to reduce the number of values to histogram
748                dims = self.volume.dimensions()
749                n = (dims[0] - 1) * (dims[1] - 1) * (dims[2] - 1)
750                n = min(1_000_000, n)
751                arr = np.random.choice(self.volume.pointdata[0], n)
752                hist = vedo.pyplot.histogram(
753                    arr,
754                    bins=12,
755                    logscale=True,
756                    c=histo_color,
757                    ytitle="log_10 (counts)",
758                    axes=dict(text_scale=1.9),
759                ).clone2d(pos="bottom-left", size=0.4)
760
761        axes = kwargs.pop("axes", 7)
762        axe = None
763        if axes == 7:
764            axe = vedo.addons.RulerAxes(
765                orig_volume, xtitle="x - ", ytitle="y - ", ztitle="z - "
766            )
767
768        box = orig_volume.box().alpha(0.25)
769
770        volume_axes_inset = vedo.addons.Axes(
771            box,
772            yzgrid=False,
773            xlabel_size=0,
774            ylabel_size=0,
775            zlabel_size=0,
776            tip_size=0.08,
777            axes_linewidth=3,
778            xline_color="dr",
779            yline_color="dg",
780            zline_color="db",
781            xtitle_color="dr",
782            ytitle_color="dg",
783            ztitle_color="db",
784            xtitle_size=0.1,
785            ytitle_size=0.1,
786            ztitle_size=0.1,
787            title_font="VictorMono",
788        )
789
790        self.at(0).add(self.volume, box, axe, self.usage, hist)
791        self.at(1).add(orig_volume, volume_axes_inset)
792        self.at(0)  # set focus at renderer 0

A single slice of a Volume which always faces the camera, but at the same time can be oriented arbitrarily in space.

Arguments:
  • vol : (Volume) the Volume object to be isosurfaced.
  • levels : (list) window and color levels
  • histo_color : (color) histogram color, use None to disable it
  • **kwargs : (dict) keyword arguments to pass to a vedo.plotter.Plotter instance.

def on_key_press(self, evt):
795    def on_key_press(self, evt):
796        """Handle keyboard events"""
797        if evt.keypress == "q":
798            self.break_interaction()
799        elif evt.keypress.lower() == "h":
800            t = self.usage
801            if len(t.text()) > 50:
802                self.usage.text("Press H to show help")
803            else:
804                self.usage.text(self.usage_txt)
805            self.render()

Handle keyboard events

def cmap( self, lut=None, fix_scalar_range=False) -> Slicer2DPlotter:
807    def cmap(self, lut=None, fix_scalar_range=False) -> "Slicer2DPlotter":
808        """
809        Assign a LUT (Look Up Table) to colorize the slice, leave it `None`
810        to reuse an existing Volume color map.
811        Use "bw" for automatic black and white.
812        """
813        if lut is None and self.lut:
814            self.volume.properties.SetLookupTable(self.lut)
815        elif isinstance(lut, vtki.vtkLookupTable):
816            self.volume.properties.SetLookupTable(lut)
817        elif lut == "bw":
818            self.volume.properties.SetLookupTable(None)
819        self.volume.properties.SetUseLookupTableScalarRange(fix_scalar_range)
820        return self

Assign a LUT (Look Up Table) to colorize the slice, leave it None to reuse an existing Volume color map. Use "bw" for automatic black and white.

def alpha(self, value: float) -> Slicer2DPlotter:
822    def alpha(self, value: float) -> "Slicer2DPlotter":
823        """Set opacity to the slice"""
824        self.volume.properties.SetOpacity(value)
825        return self

Set opacity to the slice

def auto_adjust_quality(self, value=True) -> Slicer2DPlotter:
827    def auto_adjust_quality(self, value=True) -> "Slicer2DPlotter":
828        """Automatically reduce the rendering quality for greater speed when interacting"""
829        self.volume.mapper.SetAutoAdjustImageQuality(value)
830        return self

Automatically reduce the rendering quality for greater speed when interacting

def slab( self, thickness=0, mode=0, sample_factor=2) -> Slicer2DPlotter:
832    def slab(self, thickness=0, mode=0, sample_factor=2) -> "Slicer2DPlotter":
833        """
834        Make a thick slice (slab).
835
836        Arguments:
837            thickness : (float)
838                set the slab thickness, for thick slicing
839            mode : (int)
840                The slab type:
841                    0 = min
842                    1 = max
843                    2 = mean
844                    3 = sum
845            sample_factor : (float)
846                Set the number of slab samples to use as a factor of the number of input slices
847                within the slab thickness. The default value is 2, but 1 will increase speed
848                with very little loss of quality.
849        """
850        self.volume.mapper.SetSlabThickness(thickness)
851        self.volume.mapper.SetSlabType(mode)
852        self.volume.mapper.SetSlabSampleFactor(sample_factor)
853        return self

Make a thick slice (slab).

Arguments:
  • thickness : (float) set the slab thickness, for thick slicing
  • mode : (int) The slab type: 0 = min 1 = max 2 = mean 3 = sum
  • sample_factor : (float) Set the number of slab samples to use as a factor of the number of input slices within the slab thickness. The default value is 2, but 1 will increase speed with very little loss of quality.
def face_camera(self, value=True) -> Slicer2DPlotter:
855    def face_camera(self, value=True) -> "Slicer2DPlotter":
856        """Make the slice always face the camera or not."""
857        self.volume.mapper.SetSliceFacesCameraOn(value)
858        return self

Make the slice always face the camera or not.

def jump_to_nearest_slice(self, value=True) -> Slicer2DPlotter:
860    def jump_to_nearest_slice(self, value=True) -> "Slicer2DPlotter":
861        """
862        This causes the slicing to occur at the closest slice to the focal point,
863        instead of the default behavior where a new slice is interpolated between
864        the original slices.
865        Nothing happens if the plane is oblique to the original slices.
866        """
867        self.volume.mapper.SetJumpToNearestSlice(value)
868        return self

This causes the slicing to occur at the closest slice to the focal point, instead of the default behavior where a new slice is interpolated between the original slices. Nothing happens if the plane is oblique to the original slices.

def fill_background(self, value=True) -> Slicer2DPlotter:
870    def fill_background(self, value=True) -> "Slicer2DPlotter":
871        """
872        Instead of rendering only to the image border,
873        render out to the viewport boundary with the background color.
874        The background color will be the lowest color on the lookup
875        table that is being used for the image.
876        """
877        self.volume.mapper.SetBackground(value)
878        return self

Instead of rendering only to the image border, render out to the viewport boundary with the background color. The background color will be the lowest color on the lookup table that is being used for the image.

def lighting( self, window, level, ambient=1.0, diffuse=0.0) -> Slicer2DPlotter:
880    def lighting(self, window, level, ambient=1.0, diffuse=0.0) -> "Slicer2DPlotter":
881        """Assign the values for window and color level."""
882        self.volume.properties.SetColorWindow(window)
883        self.volume.properties.SetColorLevel(level)
884        self.volume.properties.SetAmbient(ambient)
885        self.volume.properties.SetDiffuse(diffuse)
886        return self

Assign the values for window and color level.

class Slicer3DPlotter(vedo.plotter.Plotter):
 44class Slicer3DPlotter(Plotter):
 45    """
 46    Generate a rendering window with slicing planes for the input Volume.
 47    """
 48
 49    def __init__(
 50        self,
 51        volume: vedo.Volume,
 52        cmaps=("gist_ncar_r", "hot_r", "bone", "bone_r", "jet", "Spectral_r"),
 53        clamp=True,
 54        use_slider3d=False,
 55        show_histo=True,
 56        show_icon=True,
 57        draggable=False,
 58        at=0,
 59        **kwargs,
 60    ):
 61        """
 62        Generate a rendering window with slicing planes for the input Volume.
 63
 64        Arguments:
 65            cmaps : (list)
 66                list of color maps names to cycle when clicking button
 67            clamp : (bool)
 68                clamp scalar range to reduce the effect of tails in color mapping
 69            use_slider3d : (bool)
 70                show sliders attached along the axes
 71            show_histo : (bool)
 72                show histogram on bottom left
 73            show_icon : (bool)
 74                show a small 3D rendering icon of the volume
 75            draggable : (bool)
 76                make the 3D icon draggable
 77            at : (int)
 78                subwindow number to plot to
 79            **kwargs : (dict)
 80                keyword arguments to pass to a `vedo.plotter.Plotter` instance.
 81
 82        Examples:
 83            - [slicer1.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/slicer1.py)
 84
 85            <img src="https://vedo.embl.es/images/volumetric/slicer1.jpg" width="500">
 86        """
 87        ################################
 88        super().__init__(**kwargs)
 89        self.at(at)
 90        ################################
 91
 92        cx, cy, cz, ch = "dr", "dg", "db", (0.3, 0.3, 0.3)
 93        if np.sum(self.renderer.GetBackground()) < 1.5:
 94            cx, cy, cz = "lr", "lg", "lb"
 95            ch = (0.8, 0.8, 0.8)
 96
 97        if len(self.renderers) > 1:
 98            # 2d sliders do not work with multiple renderers
 99            use_slider3d = True
100
101        self.volume = volume
102        box = volume.box().alpha(0.2)
103        self.add(box)
104
105        volume_axes_inset = vedo.addons.Axes(
106            box,
107            xtitle=" ",
108            ytitle=" ",
109            ztitle=" ",
110            yzgrid=False,
111            xlabel_size=0,
112            ylabel_size=0,
113            zlabel_size=0,
114            tip_size=0.08,
115            axes_linewidth=3,
116            xline_color="dr",
117            yline_color="dg",
118            zline_color="db",
119        )
120
121        if show_icon:
122            self.add_inset(
123                volume,
124                volume_axes_inset,
125                pos=(0.9, 0.9),
126                size=0.15,
127                c="w",
128                draggable=draggable,
129            )
130
131        # inits
132        la, ld = 0.7, 0.3  # ambient, diffuse
133        dims = volume.dimensions()
134        data = volume.pointdata[0]
135        rmin, rmax = volume.scalar_range()
136        if clamp:
137            hdata, edg = np.histogram(data, bins=50)
138            logdata = np.log(hdata + 1)
139            # mean  of the logscale plot
140            meanlog = np.sum(np.multiply(edg[:-1], logdata)) / np.sum(logdata)
141            rmax = min(rmax, meanlog + (meanlog - rmin) * 0.9)
142            rmin = max(rmin, meanlog - (rmax - meanlog) * 0.9)
143            # print("scalar range clamped to range: ("
144            #       + precision(rmin, 3) + ", " + precision(rmax, 3) + ")")
145
146        self.cmap_slicer = cmaps[0]
147
148        self.current_i = None
149        self.current_j = None
150        self.current_k = int(dims[2] / 2)
151
152        self.xslice = None
153        self.yslice = None
154        self.zslice = None
155
156        self.zslice = volume.zslice(self.current_k).lighting("", la, ld, 0)
157        self.zslice.name = "ZSlice"
158        self.zslice.cmap(self.cmap_slicer, vmin=rmin, vmax=rmax)
159        self.add(self.zslice)
160
161        self.histogram = None
162        data_reduced = data
163        if show_histo:
164            # try to reduce the number of values to histogram
165            dims = self.volume.dimensions()
166            n = (dims[0] - 1) * (dims[1] - 1) * (dims[2] - 1)
167            n = min(1_000_000, n)
168            if data.ndim == 1:
169                data_reduced = np.random.choice(data, n)
170                self.histogram = histogram(
171                    data_reduced,
172                    # title=volume.filename,
173                    bins=20,
174                    logscale=True,
175                    c=self.cmap_slicer,
176                    bg=ch,
177                    alpha=1,
178                    axes=dict(text_scale=2),
179                ).clone2d(pos=[-0.925, -0.88], size=0.4)
180                self.add(self.histogram)
181
182        #################
183        def slider_function_x(_widget, _event):
184            i = int(self.xslider.value)
185            if i == self.current_i:
186                return
187            self.current_i = i
188            self.xslice = volume.xslice(i).lighting("", la, ld, 0)
189            self.xslice.cmap(self.cmap_slicer, vmin=rmin, vmax=rmax)
190            self.xslice.name = "XSlice"
191            self.remove("XSlice")  # removes the old one
192            if 0 < i < dims[0]:
193                self.add(self.xslice)
194            self.render()
195
196        def slider_function_y(_widget, _event):
197            j = int(self.yslider.value)
198            if j == self.current_j:
199                return
200            self.current_j = j
201            self.yslice = volume.yslice(j).lighting("", la, ld, 0)
202            self.yslice.cmap(self.cmap_slicer, vmin=rmin, vmax=rmax)
203            self.yslice.name = "YSlice"
204            self.remove("YSlice")
205            if 0 < j < dims[1]:
206                self.add(self.yslice)
207            self.render()
208
209        def slider_function_z(_widget, _event):
210            k = int(self.zslider.value)
211            if k == self.current_k:
212                return
213            self.current_k = k
214            self.zslice = volume.zslice(k).lighting("", la, ld, 0)
215            self.zslice.cmap(self.cmap_slicer, vmin=rmin, vmax=rmax)
216            self.zslice.name = "ZSlice"
217            self.remove("ZSlice")
218            if 0 < k < dims[2]:
219                self.add(self.zslice)
220            self.render()
221
222        if not use_slider3d:
223            self.xslider = self.add_slider(
224                slider_function_x,
225                0,
226                dims[0],
227                title="",
228                title_size=0.5,
229                pos=[(0.8, 0.12), (0.95, 0.12)],
230                show_value=False,
231                c=cx,
232            )
233            self.yslider = self.add_slider(
234                slider_function_y,
235                0,
236                dims[1],
237                title="",
238                title_size=0.5,
239                pos=[(0.8, 0.08), (0.95, 0.08)],
240                show_value=False,
241                c=cy,
242            )
243            self.zslider = self.add_slider(
244                slider_function_z,
245                0,
246                dims[2],
247                title="",
248                title_size=0.6,
249                value=int(dims[2] / 2),
250                pos=[(0.8, 0.04), (0.95, 0.04)],
251                show_value=False,
252                c=cz,
253            )
254
255        else:  # 3d sliders attached to the axes bounds
256            bs = box.bounds()
257            self.xslider = self.add_slider3d(
258                slider_function_x,
259                pos1=(bs[0], bs[2], bs[4]),
260                pos2=(bs[1], bs[2], bs[4]),
261                xmin=0,
262                xmax=dims[0],
263                t=box.diagonal_size() / mag(box.xbounds()) * 0.6,
264                c=cx,
265                show_value=False,
266            )
267            self.yslider = self.add_slider3d(
268                slider_function_y,
269                pos1=(bs[1], bs[2], bs[4]),
270                pos2=(bs[1], bs[3], bs[4]),
271                xmin=0,
272                xmax=dims[1],
273                t=box.diagonal_size() / mag(box.ybounds()) * 0.6,
274                c=cy,
275                show_value=False,
276            )
277            self.zslider = self.add_slider3d(
278                slider_function_z,
279                pos1=(bs[0], bs[2], bs[4]),
280                pos2=(bs[0], bs[2], bs[5]),
281                xmin=0,
282                xmax=dims[2],
283                value=int(dims[2] / 2),
284                t=box.diagonal_size() / mag(box.zbounds()) * 0.6,
285                c=cz,
286                show_value=False,
287            )
288
289        #################
290        def button_func(_obj, _evtname):
291            bu.switch()
292            self.cmap_slicer = bu.status()
293            for m in self.objects:
294                try:
295                    if "Slice" in m.name:
296                        m.cmap(self.cmap_slicer, vmin=rmin, vmax=rmax)
297                except AttributeError:
298                    pass
299            self.remove(self.histogram)
300            if show_histo:
301                self.histogram = histogram(
302                    data_reduced,
303                    # title=volume.filename,
304                    bins=20,
305                    logscale=True,
306                    c=self.cmap_slicer,
307                    bg=ch,
308                    alpha=1,
309                    axes=dict(text_scale=2),
310                ).clone2d(pos=[-0.925, -0.88], size=0.4)
311                self.add(self.histogram)
312            self.render()
313
314        if len(cmaps) > 1:
315            bu = self.add_button(
316                button_func,
317                states=cmaps,
318                c=["k9"] * len(cmaps),
319                bc=["k1"] * len(cmaps),  # colors of states
320                size=16,
321                bold=True,
322            )
323            if bu:
324                bu.pos([0.04, 0.01], "bottom-left")

Generate a rendering window with slicing planes for the input Volume.

Slicer3DPlotter( volume: vedo.volume.Volume, cmaps=('gist_ncar_r', 'hot_r', 'bone', 'bone_r', 'jet', 'Spectral_r'), clamp=True, use_slider3d=False, show_histo=True, show_icon=True, draggable=False, at=0, **kwargs)
 49    def __init__(
 50        self,
 51        volume: vedo.Volume,
 52        cmaps=("gist_ncar_r", "hot_r", "bone", "bone_r", "jet", "Spectral_r"),
 53        clamp=True,
 54        use_slider3d=False,
 55        show_histo=True,
 56        show_icon=True,
 57        draggable=False,
 58        at=0,
 59        **kwargs,
 60    ):
 61        """
 62        Generate a rendering window with slicing planes for the input Volume.
 63
 64        Arguments:
 65            cmaps : (list)
 66                list of color maps names to cycle when clicking button
 67            clamp : (bool)
 68                clamp scalar range to reduce the effect of tails in color mapping
 69            use_slider3d : (bool)
 70                show sliders attached along the axes
 71            show_histo : (bool)
 72                show histogram on bottom left
 73            show_icon : (bool)
 74                show a small 3D rendering icon of the volume
 75            draggable : (bool)
 76                make the 3D icon draggable
 77            at : (int)
 78                subwindow number to plot to
 79            **kwargs : (dict)
 80                keyword arguments to pass to a `vedo.plotter.Plotter` instance.
 81
 82        Examples:
 83            - [slicer1.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/slicer1.py)
 84
 85            <img src="https://vedo.embl.es/images/volumetric/slicer1.jpg" width="500">
 86        """
 87        ################################
 88        super().__init__(**kwargs)
 89        self.at(at)
 90        ################################
 91
 92        cx, cy, cz, ch = "dr", "dg", "db", (0.3, 0.3, 0.3)
 93        if np.sum(self.renderer.GetBackground()) < 1.5:
 94            cx, cy, cz = "lr", "lg", "lb"
 95            ch = (0.8, 0.8, 0.8)
 96
 97        if len(self.renderers) > 1:
 98            # 2d sliders do not work with multiple renderers
 99            use_slider3d = True
100
101        self.volume = volume
102        box = volume.box().alpha(0.2)
103        self.add(box)
104
105        volume_axes_inset = vedo.addons.Axes(
106            box,
107            xtitle=" ",
108            ytitle=" ",
109            ztitle=" ",
110            yzgrid=False,
111            xlabel_size=0,
112            ylabel_size=0,
113            zlabel_size=0,
114            tip_size=0.08,
115            axes_linewidth=3,
116            xline_color="dr",
117            yline_color="dg",
118            zline_color="db",
119        )
120
121        if show_icon:
122            self.add_inset(
123                volume,
124                volume_axes_inset,
125                pos=(0.9, 0.9),
126                size=0.15,
127                c="w",
128                draggable=draggable,
129            )
130
131        # inits
132        la, ld = 0.7, 0.3  # ambient, diffuse
133        dims = volume.dimensions()
134        data = volume.pointdata[0]
135        rmin, rmax = volume.scalar_range()
136        if clamp:
137            hdata, edg = np.histogram(data, bins=50)
138            logdata = np.log(hdata + 1)
139            # mean  of the logscale plot
140            meanlog = np.sum(np.multiply(edg[:-1], logdata)) / np.sum(logdata)
141            rmax = min(rmax, meanlog + (meanlog - rmin) * 0.9)
142            rmin = max(rmin, meanlog - (rmax - meanlog) * 0.9)
143            # print("scalar range clamped to range: ("
144            #       + precision(rmin, 3) + ", " + precision(rmax, 3) + ")")
145
146        self.cmap_slicer = cmaps[0]
147
148        self.current_i = None
149        self.current_j = None
150        self.current_k = int(dims[2] / 2)
151
152        self.xslice = None
153        self.yslice = None
154        self.zslice = None
155
156        self.zslice = volume.zslice(self.current_k).lighting("", la, ld, 0)
157        self.zslice.name = "ZSlice"
158        self.zslice.cmap(self.cmap_slicer, vmin=rmin, vmax=rmax)
159        self.add(self.zslice)
160
161        self.histogram = None
162        data_reduced = data
163        if show_histo:
164            # try to reduce the number of values to histogram
165            dims = self.volume.dimensions()
166            n = (dims[0] - 1) * (dims[1] - 1) * (dims[2] - 1)
167            n = min(1_000_000, n)
168            if data.ndim == 1:
169                data_reduced = np.random.choice(data, n)
170                self.histogram = histogram(
171                    data_reduced,
172                    # title=volume.filename,
173                    bins=20,
174                    logscale=True,
175                    c=self.cmap_slicer,
176                    bg=ch,
177                    alpha=1,
178                    axes=dict(text_scale=2),
179                ).clone2d(pos=[-0.925, -0.88], size=0.4)
180                self.add(self.histogram)
181
182        #################
183        def slider_function_x(_widget, _event):
184            i = int(self.xslider.value)
185            if i == self.current_i:
186                return
187            self.current_i = i
188            self.xslice = volume.xslice(i).lighting("", la, ld, 0)
189            self.xslice.cmap(self.cmap_slicer, vmin=rmin, vmax=rmax)
190            self.xslice.name = "XSlice"
191            self.remove("XSlice")  # removes the old one
192            if 0 < i < dims[0]:
193                self.add(self.xslice)
194            self.render()
195
196        def slider_function_y(_widget, _event):
197            j = int(self.yslider.value)
198            if j == self.current_j:
199                return
200            self.current_j = j
201            self.yslice = volume.yslice(j).lighting("", la, ld, 0)
202            self.yslice.cmap(self.cmap_slicer, vmin=rmin, vmax=rmax)
203            self.yslice.name = "YSlice"
204            self.remove("YSlice")
205            if 0 < j < dims[1]:
206                self.add(self.yslice)
207            self.render()
208
209        def slider_function_z(_widget, _event):
210            k = int(self.zslider.value)
211            if k == self.current_k:
212                return
213            self.current_k = k
214            self.zslice = volume.zslice(k).lighting("", la, ld, 0)
215            self.zslice.cmap(self.cmap_slicer, vmin=rmin, vmax=rmax)
216            self.zslice.name = "ZSlice"
217            self.remove("ZSlice")
218            if 0 < k < dims[2]:
219                self.add(self.zslice)
220            self.render()
221
222        if not use_slider3d:
223            self.xslider = self.add_slider(
224                slider_function_x,
225                0,
226                dims[0],
227                title="",
228                title_size=0.5,
229                pos=[(0.8, 0.12), (0.95, 0.12)],
230                show_value=False,
231                c=cx,
232            )
233            self.yslider = self.add_slider(
234                slider_function_y,
235                0,
236                dims[1],
237                title="",
238                title_size=0.5,
239                pos=[(0.8, 0.08), (0.95, 0.08)],
240                show_value=False,
241                c=cy,
242            )
243            self.zslider = self.add_slider(
244                slider_function_z,
245                0,
246                dims[2],
247                title="",
248                title_size=0.6,
249                value=int(dims[2] / 2),
250                pos=[(0.8, 0.04), (0.95, 0.04)],
251                show_value=False,
252                c=cz,
253            )
254
255        else:  # 3d sliders attached to the axes bounds
256            bs = box.bounds()
257            self.xslider = self.add_slider3d(
258                slider_function_x,
259                pos1=(bs[0], bs[2], bs[4]),
260                pos2=(bs[1], bs[2], bs[4]),
261                xmin=0,
262                xmax=dims[0],
263                t=box.diagonal_size() / mag(box.xbounds()) * 0.6,
264                c=cx,
265                show_value=False,
266            )
267            self.yslider = self.add_slider3d(
268                slider_function_y,
269                pos1=(bs[1], bs[2], bs[4]),
270                pos2=(bs[1], bs[3], bs[4]),
271                xmin=0,
272                xmax=dims[1],
273                t=box.diagonal_size() / mag(box.ybounds()) * 0.6,
274                c=cy,
275                show_value=False,
276            )
277            self.zslider = self.add_slider3d(
278                slider_function_z,
279                pos1=(bs[0], bs[2], bs[4]),
280                pos2=(bs[0], bs[2], bs[5]),
281                xmin=0,
282                xmax=dims[2],
283                value=int(dims[2] / 2),
284                t=box.diagonal_size() / mag(box.zbounds()) * 0.6,
285                c=cz,
286                show_value=False,
287            )
288
289        #################
290        def button_func(_obj, _evtname):
291            bu.switch()
292            self.cmap_slicer = bu.status()
293            for m in self.objects:
294                try:
295                    if "Slice" in m.name:
296                        m.cmap(self.cmap_slicer, vmin=rmin, vmax=rmax)
297                except AttributeError:
298                    pass
299            self.remove(self.histogram)
300            if show_histo:
301                self.histogram = histogram(
302                    data_reduced,
303                    # title=volume.filename,
304                    bins=20,
305                    logscale=True,
306                    c=self.cmap_slicer,
307                    bg=ch,
308                    alpha=1,
309                    axes=dict(text_scale=2),
310                ).clone2d(pos=[-0.925, -0.88], size=0.4)
311                self.add(self.histogram)
312            self.render()
313
314        if len(cmaps) > 1:
315            bu = self.add_button(
316                button_func,
317                states=cmaps,
318                c=["k9"] * len(cmaps),
319                bc=["k1"] * len(cmaps),  # colors of states
320                size=16,
321                bold=True,
322            )
323            if bu:
324                bu.pos([0.04, 0.01], "bottom-left")

Generate a rendering window with slicing planes for the input Volume.

Arguments:
  • cmaps : (list) list of color maps names to cycle when clicking button
  • clamp : (bool) clamp scalar range to reduce the effect of tails in color mapping
  • use_slider3d : (bool) show sliders attached along the axes
  • show_histo : (bool) show histogram on bottom left
  • show_icon : (bool) show a small 3D rendering icon of the volume
  • draggable : (bool) make the 3D icon draggable
  • at : (int) subwindow number to plot to
  • **kwargs : (dict) keyword arguments to pass to a vedo.plotter.Plotter instance.
Examples:

class Slicer3DTwinPlotter(vedo.plotter.Plotter):
328class Slicer3DTwinPlotter(Plotter):
329    """
330    Create a window with two side-by-side 3D slicers for two Volumes.
331
332    Arguments:
333        vol1 : (Volume)
334            the first Volume object to be isosurfaced.
335        vol2 : (Volume)
336            the second Volume object to be isosurfaced.
337        clamp : (bool)
338            clamp scalar range to reduce the effect of tails in color mapping
339        **kwargs : (dict)
340            keyword arguments to pass to a `vedo.plotter.Plotter` instance.
341
342    Example:
343        ```python
344        from vedo import *
345        from vedo.applications import Slicer3DTwinPlotter
346
347        vol1 = Volume(dataurl + "embryo.slc")
348        vol2 = Volume(dataurl + "embryo.slc")
349
350        plt = Slicer3DTwinPlotter(
351            vol1, vol2,
352            shape=(1, 2),
353            sharecam=True,
354            bg="white",
355            bg2="lightblue",
356        )
357
358        plt.at(0).add(Text2D("Volume 1", pos="top-center"))
359        plt.at(1).add(Text2D("Volume 2", pos="top-center"))
360
361        plt.show(viewup='z')
362        plt.at(0).reset_camera()
363        plt.interactive().close()
364        ```
365
366        <img src="https://vedo.embl.es/images/volumetric/slicer3dtwin.png" width="650">
367    """
368
369    def __init__(self, vol1: vedo.Volume, vol2: vedo.Volume, clamp=True, **kwargs):
370
371        super().__init__(**kwargs)
372
373        cmap = "gist_ncar_r"
374        cx, cy, cz = "dr", "dg", "db"  # slider colors
375        ambient, diffuse = 0.7, 0.3  # lighting params
376
377        self.at(0)
378        box1 = vol1.box().alpha(0.1)
379        box2 = vol2.box().alpha(0.1)
380        self.add(box1)
381
382        self.at(1).add(box2)
383        self.add_inset(vol2, pos=(0.85, 0.15), size=0.15, c="white", draggable=0)
384
385        dims = vol1.dimensions()
386        data = vol1.pointdata[0]
387        rmin, rmax = vol1.scalar_range()
388        if clamp:
389            hdata, edg = np.histogram(data, bins=50)
390            logdata = np.log(hdata + 1)
391            meanlog = np.sum(np.multiply(edg[:-1], logdata)) / np.sum(logdata)
392            rmax = min(rmax, meanlog + (meanlog - rmin) * 0.9)
393            rmin = max(rmin, meanlog - (rmax - meanlog) * 0.9)
394
395        def slider_function_x(_widget, _event):
396            i = int(self.xslider.value)
397            msh1 = vol1.xslice(i).lighting("", ambient, diffuse, 0)
398            msh1.cmap(cmap, vmin=rmin, vmax=rmax)
399            msh1.name = "XSlice"
400            self.at(0).remove("XSlice")  # removes the old one
401            msh2 = vol2.xslice(i).lighting("", ambient, diffuse, 0)
402            msh2.cmap(cmap, vmin=rmin, vmax=rmax)
403            msh2.name = "XSlice"
404            self.at(1).remove("XSlice")
405            if 0 < i < dims[0]:
406                self.at(0).add(msh1)
407                self.at(1).add(msh2)
408
409        def slider_function_y(_widget, _event):
410            i = int(self.yslider.value)
411            msh1 = vol1.yslice(i).lighting("", ambient, diffuse, 0)
412            msh1.cmap(cmap, vmin=rmin, vmax=rmax)
413            msh1.name = "YSlice"
414            self.at(0).remove("YSlice")
415            msh2 = vol2.yslice(i).lighting("", ambient, diffuse, 0)
416            msh2.cmap(cmap, vmin=rmin, vmax=rmax)
417            msh2.name = "YSlice"
418            self.at(1).remove("YSlice")
419            if 0 < i < dims[1]:
420                self.at(0).add(msh1)
421                self.at(1).add(msh2)
422
423        def slider_function_z(_widget, _event):
424            i = int(self.zslider.value)
425            msh1 = vol1.zslice(i).lighting("", ambient, diffuse, 0)
426            msh1.cmap(cmap, vmin=rmin, vmax=rmax)
427            msh1.name = "ZSlice"
428            self.at(0).remove("ZSlice")
429            msh2 = vol2.zslice(i).lighting("", ambient, diffuse, 0)
430            msh2.cmap(cmap, vmin=rmin, vmax=rmax)
431            msh2.name = "ZSlice"
432            self.at(1).remove("ZSlice")
433            if 0 < i < dims[2]:
434                self.at(0).add(msh1)
435                self.at(1).add(msh2)
436
437        self.at(0)
438        bs = box1.bounds()
439        self.xslider = self.add_slider3d(
440            slider_function_x,
441            pos1=(bs[0], bs[2], bs[4]),
442            pos2=(bs[1], bs[2], bs[4]),
443            xmin=0,
444            xmax=dims[0],
445            t=box1.diagonal_size() / mag(box1.xbounds()) * 0.6,
446            c=cx,
447            show_value=False,
448        )
449        self.yslider = self.add_slider3d(
450            slider_function_y,
451            pos1=(bs[1], bs[2], bs[4]),
452            pos2=(bs[1], bs[3], bs[4]),
453            xmin=0,
454            xmax=dims[1],
455            t=box1.diagonal_size() / mag(box1.ybounds()) * 0.6,
456            c=cy,
457            show_value=False,
458        )
459        self.zslider = self.add_slider3d(
460            slider_function_z,
461            pos1=(bs[0], bs[2], bs[4]),
462            pos2=(bs[0], bs[2], bs[5]),
463            xmin=0,
464            xmax=dims[2],
465            value=int(dims[2] / 2),
466            t=box1.diagonal_size() / mag(box1.zbounds()) * 0.6,
467            c=cz,
468            show_value=False,
469        )
470
471        #################
472        hist = CornerHistogram(data, s=0.2, bins=25, logscale=True, c="k")
473        self.add(hist)
474        slider_function_z(0, 0)  ## init call

Create a window with two side-by-side 3D slicers for two Volumes.

Arguments:
  • vol1 : (Volume) the first Volume object to be isosurfaced.
  • vol2 : (Volume) the second Volume object to be isosurfaced.
  • clamp : (bool) clamp scalar range to reduce the effect of tails in color mapping
  • **kwargs : (dict) keyword arguments to pass to a vedo.plotter.Plotter instance.
Example:
from vedo import *
from vedo.applications import Slicer3DTwinPlotter

vol1 = Volume(dataurl + "embryo.slc")
vol2 = Volume(dataurl + "embryo.slc")

plt = Slicer3DTwinPlotter(
    vol1, vol2,
    shape=(1, 2),
    sharecam=True,
    bg="white",
    bg2="lightblue",
)

plt.at(0).add(Text2D("Volume 1", pos="top-center"))
plt.at(1).add(Text2D("Volume 2", pos="top-center"))

plt.show(viewup='z')
plt.at(0).reset_camera()
plt.interactive().close()

Slicer3DTwinPlotter( vol1: vedo.volume.Volume, vol2: vedo.volume.Volume, clamp=True, **kwargs)
369    def __init__(self, vol1: vedo.Volume, vol2: vedo.Volume, clamp=True, **kwargs):
370
371        super().__init__(**kwargs)
372
373        cmap = "gist_ncar_r"
374        cx, cy, cz = "dr", "dg", "db"  # slider colors
375        ambient, diffuse = 0.7, 0.3  # lighting params
376
377        self.at(0)
378        box1 = vol1.box().alpha(0.1)
379        box2 = vol2.box().alpha(0.1)
380        self.add(box1)
381
382        self.at(1).add(box2)
383        self.add_inset(vol2, pos=(0.85, 0.15), size=0.15, c="white", draggable=0)
384
385        dims = vol1.dimensions()
386        data = vol1.pointdata[0]
387        rmin, rmax = vol1.scalar_range()
388        if clamp:
389            hdata, edg = np.histogram(data, bins=50)
390            logdata = np.log(hdata + 1)
391            meanlog = np.sum(np.multiply(edg[:-1], logdata)) / np.sum(logdata)
392            rmax = min(rmax, meanlog + (meanlog - rmin) * 0.9)
393            rmin = max(rmin, meanlog - (rmax - meanlog) * 0.9)
394
395        def slider_function_x(_widget, _event):
396            i = int(self.xslider.value)
397            msh1 = vol1.xslice(i).lighting("", ambient, diffuse, 0)
398            msh1.cmap(cmap, vmin=rmin, vmax=rmax)
399            msh1.name = "XSlice"
400            self.at(0).remove("XSlice")  # removes the old one
401            msh2 = vol2.xslice(i).lighting("", ambient, diffuse, 0)
402            msh2.cmap(cmap, vmin=rmin, vmax=rmax)
403            msh2.name = "XSlice"
404            self.at(1).remove("XSlice")
405            if 0 < i < dims[0]:
406                self.at(0).add(msh1)
407                self.at(1).add(msh2)
408
409        def slider_function_y(_widget, _event):
410            i = int(self.yslider.value)
411            msh1 = vol1.yslice(i).lighting("", ambient, diffuse, 0)
412            msh1.cmap(cmap, vmin=rmin, vmax=rmax)
413            msh1.name = "YSlice"
414            self.at(0).remove("YSlice")
415            msh2 = vol2.yslice(i).lighting("", ambient, diffuse, 0)
416            msh2.cmap(cmap, vmin=rmin, vmax=rmax)
417            msh2.name = "YSlice"
418            self.at(1).remove("YSlice")
419            if 0 < i < dims[1]:
420                self.at(0).add(msh1)
421                self.at(1).add(msh2)
422
423        def slider_function_z(_widget, _event):
424            i = int(self.zslider.value)
425            msh1 = vol1.zslice(i).lighting("", ambient, diffuse, 0)
426            msh1.cmap(cmap, vmin=rmin, vmax=rmax)
427            msh1.name = "ZSlice"
428            self.at(0).remove("ZSlice")
429            msh2 = vol2.zslice(i).lighting("", ambient, diffuse, 0)
430            msh2.cmap(cmap, vmin=rmin, vmax=rmax)
431            msh2.name = "ZSlice"
432            self.at(1).remove("ZSlice")
433            if 0 < i < dims[2]:
434                self.at(0).add(msh1)
435                self.at(1).add(msh2)
436
437        self.at(0)
438        bs = box1.bounds()
439        self.xslider = self.add_slider3d(
440            slider_function_x,
441            pos1=(bs[0], bs[2], bs[4]),
442            pos2=(bs[1], bs[2], bs[4]),
443            xmin=0,
444            xmax=dims[0],
445            t=box1.diagonal_size() / mag(box1.xbounds()) * 0.6,
446            c=cx,
447            show_value=False,
448        )
449        self.yslider = self.add_slider3d(
450            slider_function_y,
451            pos1=(bs[1], bs[2], bs[4]),
452            pos2=(bs[1], bs[3], bs[4]),
453            xmin=0,
454            xmax=dims[1],
455            t=box1.diagonal_size() / mag(box1.ybounds()) * 0.6,
456            c=cy,
457            show_value=False,
458        )
459        self.zslider = self.add_slider3d(
460            slider_function_z,
461            pos1=(bs[0], bs[2], bs[4]),
462            pos2=(bs[0], bs[2], bs[5]),
463            xmin=0,
464            xmax=dims[2],
465            value=int(dims[2] / 2),
466            t=box1.diagonal_size() / mag(box1.zbounds()) * 0.6,
467            c=cz,
468            show_value=False,
469        )
470
471        #################
472        hist = CornerHistogram(data, s=0.2, bins=25, logscale=True, c="k")
473        self.add(hist)
474        slider_function_z(0, 0)  ## init call
Arguments:
  • shape : (str, list) shape of the grid of renderers in format (rows, columns). Ignored if N is specified.
  • N : (int) number of desired renderers arranged in a grid automatically.
  • pos : (list) (x,y) position in pixels of top-left corner of the rendering window on the screen
  • size : (str, list) size of the rendering window. If 'auto', guess it based on screensize.
  • screensize : (list) physical size of the monitor screen in pixels
  • bg : (color, str) background color or specify jpg image file name with path
  • bg2 : (color) background color of a gradient towards the top
  • title : (str) window title
  • axes : (int) axis type-1 can be fully customized by passing a dictionary. Check addons.Axes() for the full list of options. Set the type of axes to be shown:
    • 0, no axes
    • 1, draw three gray grid walls
    • 2, show cartesian axes from (0,0,0)
    • 3, show positive range of cartesian axes from (0,0,0)
    • 4, show a triad at bottom left
    • 5, show a cube at bottom left
    • 6, mark the corners of the bounding box
    • 7, draw a 3D ruler at each side of the cartesian axes
    • 8, show the vtkCubeAxesActor object
    • 9, show the bounding box outLine
    • 10, show three circles representing the maximum bounding box
    • 11, show a large grid on the x-y plane
    • 12, show polar axes
    • 13, draw a simple ruler at the bottom of the window
    • 14: draw a camera orientation widget
  • sharecam : (bool) if False each renderer will have an independent camera
  • interactive : (bool) if True will stop after show() to allow interaction with the 3d scene
  • offscreen : (bool) if True will not show the rendering window
  • qt_widget : (QVTKRenderWindowInteractor) render in a Qt-Widget using an QVTKRenderWindowInteractor. See examples qt_windows[1,2,3].py and qt_cutter.py.
class MorphPlotter(vedo.plotter.Plotter):
478class MorphPlotter(Plotter):
479    """
480    A Plotter with 3 renderers to show the source, target and warped meshes.
481
482    Examples:
483        - [warp4b.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp4b.py)
484
485            ![](https://vedo.embl.es/images/advanced/warp4b.jpg)
486    """
487
488    def __init__(self, source, target, **kwargs):
489
490        vedo.settings.enable_default_keyboard_callbacks = False
491        vedo.settings.enable_default_mouse_callbacks = False
492
493        kwargs.update({"N": 3})
494        kwargs.update({"sharecam": 0})
495        super().__init__(**kwargs)
496
497        self.source = source.pickable(True)
498        self.target = target.pickable(False)
499        self.clicked = []
500        self.sources = []
501        self.targets = []
502        self.warped = None
503        self.source_labels = None
504        self.target_labels = None
505        self.automatic_picking_distance = 0.075
506        self.cmap_name = "coolwarm"
507        self.nbins = 25
508        self.msg0 = Text2D("Pick a point on the surface",
509                           pos="bottom-center", c='white', bg="blue4", alpha=1, font="Calco")
510        self.msg1 = Text2D(pos="bottom-center", c='white', bg="blue4", alpha=1, font="Calco")
511        self.instructions = Text2D(s=0.7, bg="blue4", alpha=0.1, font="Calco")
512        self.instructions.text(
513            "  Morphological alignment of 3D surfaces\n\n"
514            "Pick a point on the source surface, then\n"
515            "pick the corresponding point on the target \n"
516            "Pick at least 4 point pairs. Press:\n"
517            "- c to clear all landmarks\n"
518            "- d to delete the last landmark pair\n"
519            "- a to auto-pick additional landmarks\n"
520            "- z to compute and show the residuals\n"
521            "- q to quit and proceed"
522        )
523        self.at(0).add_renderer_frame()
524        self.add(source, self.msg0, self.instructions).reset_camera()
525        self.at(1).add_renderer_frame()
526        self.add(Text2D(f"Target: {target.filename[-35:]}", bg="blue4", alpha=0.1, font="Calco"))
527        self.add(self.msg1, target)
528        cam1 = self.camera  # save camera at 1
529        self.at(2).background("k9")
530        self.add(target, Text2D("Morphing Output", font="Calco"))
531        self.camera = cam1  # use the same camera of renderer1
532
533        self.add_renderer_frame()
534
535        self.callid1 = self.add_callback("KeyPress", self.on_keypress)
536        self.callid2 = self.add_callback("LeftButtonPress", self.on_click)
537        self._interactive = True
538
539    ################################################
540    def update(self):
541        """Update the rendering window"""
542        source_pts = Points(self.sources).color("purple5").ps(12)
543        target_pts = Points(self.targets).color("purple5").ps(12)
544        source_pts.name = "source_pts"
545        target_pts.name = "target_pts"
546        self.source_labels = source_pts.labels2d("id", c="purple3")
547        self.target_labels = target_pts.labels2d("id", c="purple3")
548        self.source_labels.name = "source_pts"
549        self.target_labels.name = "target_pts"
550        self.at(0).remove("source_pts").add(source_pts, self.source_labels)
551        self.at(1).remove("target_pts").add(target_pts, self.target_labels)
552        self.render()
553
554        if len(self.sources) == len(self.targets) and len(self.sources) > 3:
555            self.warped = self.source.clone().warp(self.sources, self.targets)
556            self.warped.name = "warped"
557            self.at(2).remove("warped").add(self.warped)
558            self.render()
559
560    def on_click(self, evt):
561        """Handle mouse click events"""
562        if evt.object == self.source:
563            self.sources.append(evt.picked3d)
564            self.source.pickable(False)
565            self.target.pickable(True)
566            self.msg0.text("--->")
567            self.msg1.text("now pick a target point")
568            self.update()
569        elif evt.object == self.target:
570            self.targets.append(evt.picked3d)
571            self.source.pickable(True)
572            self.target.pickable(False)
573            self.msg0.text("now pick a source point")
574            self.msg1.text("<---")
575            self.update()
576
577    def on_keypress(self, evt):
578        """Handle keyboard events"""
579        if evt.keypress == "c":
580            self.sources.clear()
581            self.targets.clear()
582            self.at(0).remove("source_pts")
583            self.at(1).remove("target_pts")
584            self.at(2).remove("warped")
585            self.msg0.text("CLEARED! Pick a point here")
586            self.msg1.text("")
587            self.source.pickable(True)
588            self.target.pickable(False)
589            self.update()
590        if evt.keypress == "w":
591            rep = (self.warped.properties.GetRepresentation() == 1)
592            self.warped.wireframe(not rep)
593            self.render()
594        if evt.keypress == "d":
595            n = min(len(self.sources), len(self.targets))
596            self.sources = self.sources[:n-1]
597            self.targets = self.targets[:n-1]
598            self.msg0.text("Last point deleted! Pick a point here")
599            self.msg1.text("")
600            self.source.pickable(True)
601            self.target.pickable(False)
602            self.update()
603        if evt.keypress == "a":
604            # auto-pick points on the target surface
605            if not self.warped:
606                vedo.printc("At least 4 points are needed.", c="r")
607                return
608            pts = self.target.clone().subsample(self.automatic_picking_distance)
609            if len(self.sources) > len(self.targets):
610                self.sources.pop()
611            d = self.target.diagonal_size()
612            r = d * self.automatic_picking_distance
613            TI = self.warped.transform.compute_inverse()
614            for p in pts.coordinates:
615                pp = vedo.utils.closest(p, self.targets)[1]
616                if vedo.mag(pp - p) < r:
617                    continue
618                q = self.warped.closest_point(p)
619                self.sources.append(TI(q))
620                self.targets.append(p)
621            self.source.pickable(True)
622            self.target.pickable(False)
623            self.update()
624        if evt.keypress == "z" or evt.keypress == "a":
625            dists = self.warped.distance_to(self.target, signed=True)
626            v = np.std(dists) * 2
627            self.warped.cmap(self.cmap_name, dists, vmin=-v, vmax=+v)
628
629            h = vedo.pyplot.histogram(
630                dists,
631                bins=self.nbins,
632                title=" ",
633                xtitle=f"STD = {v/2:.2f}",
634                ytitle="",
635                c=self.cmap_name,
636                xlim=(-v, v),
637                aspect=16/9,
638                axes=dict(
639                    number_of_divisions=5,
640                    text_scale=2,
641                    xtitle_offset=0.075,
642                    xlabel_justify="top-center"),
643            )
644
645            # try to fit a gaussian to the histogram
646            def gauss(x, A, B, sigma):
647                return A + B * np.exp(-x**2 / (2 * sigma**2))
648            try:
649                from scipy.optimize import curve_fit
650                inits = [0, len(dists)/self.nbins*2.5, v/2]
651                popt, _ = curve_fit(gauss, xdata=h.centers, ydata=h.frequencies, p0=inits)
652                x = np.linspace(-v, v, 300)
653                h += vedo.pyplot.plot(x, gauss(x, *popt), like=h, lw=1, lc="k2")
654                h["Axes"]["xtitle"].text(f":sigma = {abs(popt[2]):.3f}", font="VictorMono")
655            except:
656                pass
657
658            h = h.clone2d(pos="bottom-left", size=0.575)
659            h.name = "warped"
660            self.at(2).add(h)
661            self.render()
662
663        if evt.keypress == "q":
664            self.break_interaction()

A Plotter with 3 renderers to show the source, target and warped meshes.

Examples:
MorphPlotter(source, target, **kwargs)
488    def __init__(self, source, target, **kwargs):
489
490        vedo.settings.enable_default_keyboard_callbacks = False
491        vedo.settings.enable_default_mouse_callbacks = False
492
493        kwargs.update({"N": 3})
494        kwargs.update({"sharecam": 0})
495        super().__init__(**kwargs)
496
497        self.source = source.pickable(True)
498        self.target = target.pickable(False)
499        self.clicked = []
500        self.sources = []
501        self.targets = []
502        self.warped = None
503        self.source_labels = None
504        self.target_labels = None
505        self.automatic_picking_distance = 0.075
506        self.cmap_name = "coolwarm"
507        self.nbins = 25
508        self.msg0 = Text2D("Pick a point on the surface",
509                           pos="bottom-center", c='white', bg="blue4", alpha=1, font="Calco")
510        self.msg1 = Text2D(pos="bottom-center", c='white', bg="blue4", alpha=1, font="Calco")
511        self.instructions = Text2D(s=0.7, bg="blue4", alpha=0.1, font="Calco")
512        self.instructions.text(
513            "  Morphological alignment of 3D surfaces\n\n"
514            "Pick a point on the source surface, then\n"
515            "pick the corresponding point on the target \n"
516            "Pick at least 4 point pairs. Press:\n"
517            "- c to clear all landmarks\n"
518            "- d to delete the last landmark pair\n"
519            "- a to auto-pick additional landmarks\n"
520            "- z to compute and show the residuals\n"
521            "- q to quit and proceed"
522        )
523        self.at(0).add_renderer_frame()
524        self.add(source, self.msg0, self.instructions).reset_camera()
525        self.at(1).add_renderer_frame()
526        self.add(Text2D(f"Target: {target.filename[-35:]}", bg="blue4", alpha=0.1, font="Calco"))
527        self.add(self.msg1, target)
528        cam1 = self.camera  # save camera at 1
529        self.at(2).background("k9")
530        self.add(target, Text2D("Morphing Output", font="Calco"))
531        self.camera = cam1  # use the same camera of renderer1
532
533        self.add_renderer_frame()
534
535        self.callid1 = self.add_callback("KeyPress", self.on_keypress)
536        self.callid2 = self.add_callback("LeftButtonPress", self.on_click)
537        self._interactive = True
Arguments:
  • shape : (str, list) shape of the grid of renderers in format (rows, columns). Ignored if N is specified.
  • N : (int) number of desired renderers arranged in a grid automatically.
  • pos : (list) (x,y) position in pixels of top-left corner of the rendering window on the screen
  • size : (str, list) size of the rendering window. If 'auto', guess it based on screensize.
  • screensize : (list) physical size of the monitor screen in pixels
  • bg : (color, str) background color or specify jpg image file name with path
  • bg2 : (color) background color of a gradient towards the top
  • title : (str) window title
  • axes : (int) axis type-1 can be fully customized by passing a dictionary. Check addons.Axes() for the full list of options. Set the type of axes to be shown:
    • 0, no axes
    • 1, draw three gray grid walls
    • 2, show cartesian axes from (0,0,0)
    • 3, show positive range of cartesian axes from (0,0,0)
    • 4, show a triad at bottom left
    • 5, show a cube at bottom left
    • 6, mark the corners of the bounding box
    • 7, draw a 3D ruler at each side of the cartesian axes
    • 8, show the vtkCubeAxesActor object
    • 9, show the bounding box outLine
    • 10, show three circles representing the maximum bounding box
    • 11, show a large grid on the x-y plane
    • 12, show polar axes
    • 13, draw a simple ruler at the bottom of the window
    • 14: draw a camera orientation widget
  • sharecam : (bool) if False each renderer will have an independent camera
  • interactive : (bool) if True will stop after show() to allow interaction with the 3d scene
  • offscreen : (bool) if True will not show the rendering window
  • qt_widget : (QVTKRenderWindowInteractor) render in a Qt-Widget using an QVTKRenderWindowInteractor. See examples qt_windows[1,2,3].py and qt_cutter.py.
camera
3712    @property
3713    def camera(self):
3714        """Return the current active camera."""
3715        if self.renderer:
3716            return self.renderer.GetActiveCamera()

Return the current active camera.

def update(self):
540    def update(self):
541        """Update the rendering window"""
542        source_pts = Points(self.sources).color("purple5").ps(12)
543        target_pts = Points(self.targets).color("purple5").ps(12)
544        source_pts.name = "source_pts"
545        target_pts.name = "target_pts"
546        self.source_labels = source_pts.labels2d("id", c="purple3")
547        self.target_labels = target_pts.labels2d("id", c="purple3")
548        self.source_labels.name = "source_pts"
549        self.target_labels.name = "target_pts"
550        self.at(0).remove("source_pts").add(source_pts, self.source_labels)
551        self.at(1).remove("target_pts").add(target_pts, self.target_labels)
552        self.render()
553
554        if len(self.sources) == len(self.targets) and len(self.sources) > 3:
555            self.warped = self.source.clone().warp(self.sources, self.targets)
556            self.warped.name = "warped"
557            self.at(2).remove("warped").add(self.warped)
558            self.render()

Update the rendering window

def on_click(self, evt):
560    def on_click(self, evt):
561        """Handle mouse click events"""
562        if evt.object == self.source:
563            self.sources.append(evt.picked3d)
564            self.source.pickable(False)
565            self.target.pickable(True)
566            self.msg0.text("--->")
567            self.msg1.text("now pick a target point")
568            self.update()
569        elif evt.object == self.target:
570            self.targets.append(evt.picked3d)
571            self.source.pickable(True)
572            self.target.pickable(False)
573            self.msg0.text("now pick a source point")
574            self.msg1.text("<---")
575            self.update()

Handle mouse click events

def on_keypress(self, evt):
577    def on_keypress(self, evt):
578        """Handle keyboard events"""
579        if evt.keypress == "c":
580            self.sources.clear()
581            self.targets.clear()
582            self.at(0).remove("source_pts")
583            self.at(1).remove("target_pts")
584            self.at(2).remove("warped")
585            self.msg0.text("CLEARED! Pick a point here")
586            self.msg1.text("")
587            self.source.pickable(True)
588            self.target.pickable(False)
589            self.update()
590        if evt.keypress == "w":
591            rep = (self.warped.properties.GetRepresentation() == 1)
592            self.warped.wireframe(not rep)
593            self.render()
594        if evt.keypress == "d":
595            n = min(len(self.sources), len(self.targets))
596            self.sources = self.sources[:n-1]
597            self.targets = self.targets[:n-1]
598            self.msg0.text("Last point deleted! Pick a point here")
599            self.msg1.text("")
600            self.source.pickable(True)
601            self.target.pickable(False)
602            self.update()
603        if evt.keypress == "a":
604            # auto-pick points on the target surface
605            if not self.warped:
606                vedo.printc("At least 4 points are needed.", c="r")
607                return
608            pts = self.target.clone().subsample(self.automatic_picking_distance)
609            if len(self.sources) > len(self.targets):
610                self.sources.pop()
611            d = self.target.diagonal_size()
612            r = d * self.automatic_picking_distance
613            TI = self.warped.transform.compute_inverse()
614            for p in pts.coordinates:
615                pp = vedo.utils.closest(p, self.targets)[1]
616                if vedo.mag(pp - p) < r:
617                    continue
618                q = self.warped.closest_point(p)
619                self.sources.append(TI(q))
620                self.targets.append(p)
621            self.source.pickable(True)
622            self.target.pickable(False)
623            self.update()
624        if evt.keypress == "z" or evt.keypress == "a":
625            dists = self.warped.distance_to(self.target, signed=True)
626            v = np.std(dists) * 2
627            self.warped.cmap(self.cmap_name, dists, vmin=-v, vmax=+v)
628
629            h = vedo.pyplot.histogram(
630                dists,
631                bins=self.nbins,
632                title=" ",
633                xtitle=f"STD = {v/2:.2f}",
634                ytitle="",
635                c=self.cmap_name,
636                xlim=(-v, v),
637                aspect=16/9,
638                axes=dict(
639                    number_of_divisions=5,
640                    text_scale=2,
641                    xtitle_offset=0.075,
642                    xlabel_justify="top-center"),
643            )
644
645            # try to fit a gaussian to the histogram
646            def gauss(x, A, B, sigma):
647                return A + B * np.exp(-x**2 / (2 * sigma**2))
648            try:
649                from scipy.optimize import curve_fit
650                inits = [0, len(dists)/self.nbins*2.5, v/2]
651                popt, _ = curve_fit(gauss, xdata=h.centers, ydata=h.frequencies, p0=inits)
652                x = np.linspace(-v, v, 300)
653                h += vedo.pyplot.plot(x, gauss(x, *popt), like=h, lw=1, lc="k2")
654                h["Axes"]["xtitle"].text(f":sigma = {abs(popt[2]):.3f}", font="VictorMono")
655            except:
656                pass
657
658            h = h.clone2d(pos="bottom-left", size=0.575)
659            h.name = "warped"
660            self.at(2).add(h)
661            self.render()
662
663        if evt.keypress == "q":
664            self.break_interaction()

Handle keyboard events

class SplinePlotter(vedo.plotter.Plotter):
1639class SplinePlotter(Plotter):
1640    """
1641    Interactive drawing of splined curves on meshes.
1642    """
1643
1644    def __init__(self, obj, init_points=(), closed=False, splined=True, mode="auto", **kwargs):
1645        """
1646        Create an interactive application that allows the user to click points and
1647        retrieve the coordinates of such points and optionally a spline or line
1648        (open or closed).
1649        Input object can be a image file name or a 3D mesh.
1650
1651        Arguments:
1652            obj : (Mesh, str)
1653                The input object can be a image file name or a 3D mesh.
1654            init_points : (list)
1655                Set an initial number of points to define a region.
1656            closed : (bool)
1657                Close the spline or line.
1658            splined : (bool)
1659                Join points with a spline or a simple line.
1660            mode : (str)
1661                Set the mode of interaction.
1662            **kwargs : (dict)
1663                keyword arguments to pass to a `vedo.plotter.Plotter` instance.
1664        """
1665        super().__init__(**kwargs)
1666
1667        self.verbose = True
1668        self.splined = splined
1669        self.resolution = None  # spline resolution (None = automatic)
1670        self.closed = closed
1671        self.lcolor = "yellow4"
1672        self.lwidth = 3
1673        self.pcolor = "purple5"
1674        self.psize = 10
1675
1676        self.cpoints = list(init_points)
1677        self.vpoints = None
1678        self.line = None
1679
1680        if isinstance(obj, str):
1681            self.object = vedo.file_io.load(obj)
1682        else:
1683            self.object = obj
1684
1685        self.mode = mode
1686        if self.mode == "auto":
1687            if isinstance(self.object, vedo.Image):
1688                self.mode = "image"
1689                self.parallel_projection(True)
1690            else:
1691                self.mode = "TrackballCamera"
1692
1693        t = (
1694            "Click to add a point\n"
1695            "Right-click to remove it\n"
1696            "Drag mouse to change contrast\n"
1697            "Press c to clear points\n"
1698            "Press q to continue"
1699        )
1700        self.instructions = Text2D(t, pos="bottom-left", c="white", bg="green", font="Calco")
1701
1702        self += [self.object, self.instructions]
1703
1704        self.callid1 = self.add_callback("KeyPress", self._key_press)
1705        self.callid2 = self.add_callback("LeftButtonPress", self._on_left_click)
1706        self.callid3 = self.add_callback("RightButtonPress", self._on_right_click)
1707
1708
1709    def points(self, newpts=None) -> Union["SplinePlotter", np.ndarray]:
1710        """Retrieve the 3D coordinates of the clicked points"""
1711        if newpts is not None:
1712            self.cpoints = newpts
1713            self.update()
1714            return self
1715        return np.array(self.cpoints)
1716
1717    def _on_left_click(self, evt):
1718        if not evt.actor:
1719            return
1720        if evt.actor.name == "points":
1721            # remove clicked point if clicked twice
1722            pid = self.vpoints.closest_point(evt.picked3d, return_point_id=True)
1723            self.cpoints.pop(pid)
1724            self.update()
1725            return
1726        p = evt.picked3d
1727        self.cpoints.append(p)
1728        self.update()
1729        if self.verbose:
1730            vedo.colors.printc("Added point:", precision(p, 4), c="g")
1731
1732    def _on_right_click(self, evt):
1733        if evt.actor and len(self.cpoints) > 0:
1734            self.cpoints.pop()  # pop removes from the list the last pt
1735            self.update()
1736            if self.verbose:
1737                vedo.colors.printc("Deleted last point", c="r")
1738
1739    def update(self):
1740        """Update the plot with the new points"""
1741        self.remove(self.line, self.vpoints)  # remove old points and spline
1742        self.vpoints = Points(self.cpoints).ps(self.psize).c(self.pcolor)
1743        self.vpoints.name = "points"
1744        self.vpoints.pickable(True)  # to allow toggle
1745        minnr = 1
1746        if self.splined:
1747            minnr = 2
1748        if self.lwidth and len(self.cpoints) > minnr:
1749            if self.splined:
1750                try:
1751                    self.line = Spline(self.cpoints, closed=self.closed, res=self.resolution)
1752                except ValueError:
1753                    # if clicking too close splining might fail
1754                    self.cpoints.pop()
1755                    return
1756            else:
1757                self.line = Line(self.cpoints, closed=self.closed)
1758            self.line.c(self.lcolor).lw(self.lwidth).pickable(False)
1759            self.add(self.vpoints, self.line)
1760        else:
1761            self.add(self.vpoints)
1762
1763    def _key_press(self, evt):
1764        if evt.keypress == "c":
1765            self.cpoints = []
1766            self.remove(self.line, self.vpoints).render()
1767            if self.verbose:
1768                vedo.colors.printc("==== Cleared all points ====", c="r", invert=True)
1769
1770    def start(self) -> "SplinePlotter":
1771        """Start the interaction"""
1772        self.update()
1773        self.show(self.object, self.instructions, mode=self.mode)
1774        return self

Interactive drawing of splined curves on meshes.

SplinePlotter( obj, init_points=(), closed=False, splined=True, mode='auto', **kwargs)
1644    def __init__(self, obj, init_points=(), closed=False, splined=True, mode="auto", **kwargs):
1645        """
1646        Create an interactive application that allows the user to click points and
1647        retrieve the coordinates of such points and optionally a spline or line
1648        (open or closed).
1649        Input object can be a image file name or a 3D mesh.
1650
1651        Arguments:
1652            obj : (Mesh, str)
1653                The input object can be a image file name or a 3D mesh.
1654            init_points : (list)
1655                Set an initial number of points to define a region.
1656            closed : (bool)
1657                Close the spline or line.
1658            splined : (bool)
1659                Join points with a spline or a simple line.
1660            mode : (str)
1661                Set the mode of interaction.
1662            **kwargs : (dict)
1663                keyword arguments to pass to a `vedo.plotter.Plotter` instance.
1664        """
1665        super().__init__(**kwargs)
1666
1667        self.verbose = True
1668        self.splined = splined
1669        self.resolution = None  # spline resolution (None = automatic)
1670        self.closed = closed
1671        self.lcolor = "yellow4"
1672        self.lwidth = 3
1673        self.pcolor = "purple5"
1674        self.psize = 10
1675
1676        self.cpoints = list(init_points)
1677        self.vpoints = None
1678        self.line = None
1679
1680        if isinstance(obj, str):
1681            self.object = vedo.file_io.load(obj)
1682        else:
1683            self.object = obj
1684
1685        self.mode = mode
1686        if self.mode == "auto":
1687            if isinstance(self.object, vedo.Image):
1688                self.mode = "image"
1689                self.parallel_projection(True)
1690            else:
1691                self.mode = "TrackballCamera"
1692
1693        t = (
1694            "Click to add a point\n"
1695            "Right-click to remove it\n"
1696            "Drag mouse to change contrast\n"
1697            "Press c to clear points\n"
1698            "Press q to continue"
1699        )
1700        self.instructions = Text2D(t, pos="bottom-left", c="white", bg="green", font="Calco")
1701
1702        self += [self.object, self.instructions]
1703
1704        self.callid1 = self.add_callback("KeyPress", self._key_press)
1705        self.callid2 = self.add_callback("LeftButtonPress", self._on_left_click)
1706        self.callid3 = self.add_callback("RightButtonPress", self._on_right_click)

Create an interactive application that allows the user to click points and retrieve the coordinates of such points and optionally a spline or line (open or closed). Input object can be a image file name or a 3D mesh.

Arguments:
  • obj : (Mesh, str) The input object can be a image file name or a 3D mesh.
  • init_points : (list) Set an initial number of points to define a region.
  • closed : (bool) Close the spline or line.
  • splined : (bool) Join points with a spline or a simple line.
  • mode : (str) Set the mode of interaction.
  • **kwargs : (dict) keyword arguments to pass to a vedo.plotter.Plotter instance.
def points( self, newpts=None) -> Union[SplinePlotter, numpy.ndarray]:
1709    def points(self, newpts=None) -> Union["SplinePlotter", np.ndarray]:
1710        """Retrieve the 3D coordinates of the clicked points"""
1711        if newpts is not None:
1712            self.cpoints = newpts
1713            self.update()
1714            return self
1715        return np.array(self.cpoints)

Retrieve the 3D coordinates of the clicked points

def update(self):
1739    def update(self):
1740        """Update the plot with the new points"""
1741        self.remove(self.line, self.vpoints)  # remove old points and spline
1742        self.vpoints = Points(self.cpoints).ps(self.psize).c(self.pcolor)
1743        self.vpoints.name = "points"
1744        self.vpoints.pickable(True)  # to allow toggle
1745        minnr = 1
1746        if self.splined:
1747            minnr = 2
1748        if self.lwidth and len(self.cpoints) > minnr:
1749            if self.splined:
1750                try:
1751                    self.line = Spline(self.cpoints, closed=self.closed, res=self.resolution)
1752                except ValueError:
1753                    # if clicking too close splining might fail
1754                    self.cpoints.pop()
1755                    return
1756            else:
1757                self.line = Line(self.cpoints, closed=self.closed)
1758            self.line.c(self.lcolor).lw(self.lwidth).pickable(False)
1759            self.add(self.vpoints, self.line)
1760        else:
1761            self.add(self.vpoints)

Update the plot with the new points

def start(self) -> SplinePlotter:
1770    def start(self) -> "SplinePlotter":
1771        """Start the interaction"""
1772        self.update()
1773        self.show(self.object, self.instructions, mode=self.mode)
1774        return self

Start the interaction

class AnimationPlayer(vedo.plotter.Plotter):
2181class AnimationPlayer(vedo.Plotter):
2182    """
2183    A Plotter with play/pause, step forward/backward and slider functionalties.
2184    Useful for inspecting time series.
2185
2186    The user has the responsibility to update all actors in the callback function.
2187
2188    Arguments:
2189        func :  (Callable)
2190            a function that passes an integer as input and updates the scene
2191        irange : (tuple)
2192            the range of the integer input representing the time series index
2193        dt : (float)
2194            the time interval between two calls to `func` in milliseconds
2195        loop : (bool)
2196            whether to loop the animation
2197        c : (list, str)
2198            the color of the play/pause button
2199        bc : (list)
2200            the background color of the play/pause button and the slider
2201        button_size : (int)
2202            the size of the play/pause buttons
2203        button_pos : (float, float)
2204            the position of the play/pause buttons as a fraction of the window size
2205        button_gap : (float)
2206            the gap between the buttons
2207        slider_length : (float)
2208            the length of the slider as a fraction of the window size
2209        slider_pos : (float, float)
2210            the position of the slider as a fraction of the window size
2211        kwargs: (dict)
2212            keyword arguments to be passed to `Plotter`
2213
2214    Examples:
2215        - [aspring2_player.py](https://vedo.embl.es/images/simulations/spring_player.gif)
2216    """
2217
2218    # Original class contributed by @mikaeltulldahl (Mikael Tulldahl)
2219
2220    PLAY_SYMBOL        = "    \u23F5   "
2221    PAUSE_SYMBOL       = "   \u23F8   "
2222    ONE_BACK_SYMBOL    = " \u29CF"
2223    ONE_FORWARD_SYMBOL = "\u29D0 "
2224
2225    def __init__(
2226        self,
2227        func,
2228        irange: tuple,
2229        dt: float = 1.0,
2230        loop: bool = True,
2231        c=("white", "white"),
2232        bc=("green3", "red4"),
2233        button_size=25,
2234        button_pos=(0.5, 0.04),
2235        button_gap=0.055,
2236        slider_length=0.5,
2237        slider_pos=(0.5, 0.055),
2238        **kwargs,
2239    ):
2240        super().__init__(**kwargs)
2241
2242        min_value, max_value = np.array(irange).astype(int)
2243        button_pos = np.array(button_pos)
2244        slider_pos = np.array(slider_pos)
2245
2246        self._func = func
2247
2248        self.value = min_value - 1
2249        self.min_value = min_value
2250        self.max_value = max_value
2251        self.dt = max(dt, 1)
2252        self.is_playing = False
2253        self._loop = loop
2254
2255        self.timer_callback_id = self.add_callback(
2256            "timer", self._handle_timer, enable_picking=False
2257        )
2258        self.timer_id = None
2259
2260        self.play_pause_button = self.add_button(
2261            self.toggle,
2262            pos=button_pos,  # x,y fraction from bottom left corner
2263            states=[self.PLAY_SYMBOL, self.PAUSE_SYMBOL],
2264            font="Kanopus",
2265            size=button_size,
2266            bc=bc,
2267        )
2268        self.button_oneback = self.add_button(
2269            self.onebackward,
2270            pos=(-button_gap, 0) + button_pos,
2271            states=[self.ONE_BACK_SYMBOL],
2272            font="Kanopus",
2273            size=button_size,
2274            c=c,
2275            bc=bc,
2276        )
2277        self.button_oneforward = self.add_button(
2278            self.oneforward,
2279            pos=(button_gap, 0) + button_pos,
2280            states=[self.ONE_FORWARD_SYMBOL],
2281            font="Kanopus",
2282            size=button_size,
2283            bc=bc,
2284        )
2285        d = (1 - slider_length) / 2
2286        self.slider: SliderWidget = self.add_slider(
2287            self._slider_callback,
2288            self.min_value,
2289            self.max_value - 1,
2290            value=self.min_value,
2291            pos=[(d - 0.5, 0) + slider_pos, (0.5 - d, 0) + slider_pos],
2292            show_value=False,
2293            c=bc[0],
2294            alpha=1,
2295        )
2296
2297    def pause(self) -> None:
2298        """Pause the animation."""
2299        self.is_playing = False
2300        if self.timer_id is not None:
2301            self.timer_callback("destroy", self.timer_id)
2302            self.timer_id = None
2303        self.play_pause_button.status(self.PLAY_SYMBOL)
2304
2305    def resume(self) -> None:
2306        """Resume the animation."""
2307        if self.timer_id is not None:
2308            self.timer_callback("destroy", self.timer_id)
2309        self.timer_id = self.timer_callback("create", dt=int(self.dt))
2310        self.is_playing = True
2311        self.play_pause_button.status(self.PAUSE_SYMBOL)
2312
2313    def toggle(self, _obj, _evt) -> None:
2314        """Toggle between play and pause."""
2315        if not self.is_playing:
2316            self.resume()
2317        else:
2318            self.pause()
2319
2320    def oneforward(self, _obj, _evt) -> None:
2321        """Advance the animation by one frame."""
2322        self.pause()
2323        self.set_frame(self.value + 1)
2324
2325    def onebackward(self, _obj, _evt) -> None:
2326        """Go back one frame in the animation."""
2327        self.pause()
2328        self.set_frame(self.value - 1)
2329
2330    def set_frame(self, value: int) -> None:
2331        """Set the current value of the animation."""
2332        if self._loop:
2333            if value < self.min_value:
2334                value = self.max_value - 1
2335            elif value >= self.max_value:
2336                value = self.min_value
2337        else:
2338            if value < self.min_value:
2339                self.pause()
2340                value = self.min_value
2341            elif value >= self.max_value - 1:
2342                value = self.max_value - 1
2343                self.pause()
2344
2345        if self.value != value:
2346            self.value = value
2347            self.slider.value = value
2348            self._func(value)
2349
2350    def _slider_callback(self, widget: SliderWidget, _: str) -> None:
2351        self.pause()
2352        self.set_frame(int(round(widget.value)))
2353
2354    def _handle_timer(self, _evt=None) -> None:
2355        self.set_frame(self.value + 1)
2356
2357    def stop(self) -> "AnimationPlayer":
2358        """
2359        Stop the animation timers, remove buttons and slider.
2360        Behave like a normal `Plotter` after this.
2361        """
2362        # stop timer
2363        if self.timer_id is not None:
2364            self.timer_callback("destroy", self.timer_id)
2365            self.timer_id = None
2366
2367        # remove callbacks
2368        self.remove_callback(self.timer_callback_id)
2369
2370        # remove buttons
2371        self.slider.off()
2372        self.renderer.RemoveActor(self.play_pause_button.actor)
2373        self.renderer.RemoveActor(self.button_oneback.actor)
2374        self.renderer.RemoveActor(self.button_oneforward.actor)
2375        return self

A Plotter with play/pause, step forward/backward and slider functionalties. Useful for inspecting time series.

The user has the responsibility to update all actors in the callback function.

Arguments:
  • func : (Callable) a function that passes an integer as input and updates the scene
  • irange : (tuple) the range of the integer input representing the time series index
  • dt : (float) the time interval between two calls to func in milliseconds
  • loop : (bool) whether to loop the animation
  • c : (list, str) the color of the play/pause button
  • bc : (list) the background color of the play/pause button and the slider
  • button_size : (int) the size of the play/pause buttons
  • button_pos : (float, float) the position of the play/pause buttons as a fraction of the window size
  • button_gap : (float) the gap between the buttons
  • slider_length : (float) the length of the slider as a fraction of the window size
  • slider_pos : (float, float) the position of the slider as a fraction of the window size
  • kwargs: (dict) keyword arguments to be passed to Plotter
Examples:
AnimationPlayer( func, irange: tuple, dt: float = 1.0, loop: bool = True, c=('white', 'white'), bc=('green3', 'red4'), button_size=25, button_pos=(0.5, 0.04), button_gap=0.055, slider_length=0.5, slider_pos=(0.5, 0.055), **kwargs)
2225    def __init__(
2226        self,
2227        func,
2228        irange: tuple,
2229        dt: float = 1.0,
2230        loop: bool = True,
2231        c=("white", "white"),
2232        bc=("green3", "red4"),
2233        button_size=25,
2234        button_pos=(0.5, 0.04),
2235        button_gap=0.055,
2236        slider_length=0.5,
2237        slider_pos=(0.5, 0.055),
2238        **kwargs,
2239    ):
2240        super().__init__(**kwargs)
2241
2242        min_value, max_value = np.array(irange).astype(int)
2243        button_pos = np.array(button_pos)
2244        slider_pos = np.array(slider_pos)
2245
2246        self._func = func
2247
2248        self.value = min_value - 1
2249        self.min_value = min_value
2250        self.max_value = max_value
2251        self.dt = max(dt, 1)
2252        self.is_playing = False
2253        self._loop = loop
2254
2255        self.timer_callback_id = self.add_callback(
2256            "timer", self._handle_timer, enable_picking=False
2257        )
2258        self.timer_id = None
2259
2260        self.play_pause_button = self.add_button(
2261            self.toggle,
2262            pos=button_pos,  # x,y fraction from bottom left corner
2263            states=[self.PLAY_SYMBOL, self.PAUSE_SYMBOL],
2264            font="Kanopus",
2265            size=button_size,
2266            bc=bc,
2267        )
2268        self.button_oneback = self.add_button(
2269            self.onebackward,
2270            pos=(-button_gap, 0) + button_pos,
2271            states=[self.ONE_BACK_SYMBOL],
2272            font="Kanopus",
2273            size=button_size,
2274            c=c,
2275            bc=bc,
2276        )
2277        self.button_oneforward = self.add_button(
2278            self.oneforward,
2279            pos=(button_gap, 0) + button_pos,
2280            states=[self.ONE_FORWARD_SYMBOL],
2281            font="Kanopus",
2282            size=button_size,
2283            bc=bc,
2284        )
2285        d = (1 - slider_length) / 2
2286        self.slider: SliderWidget = self.add_slider(
2287            self._slider_callback,
2288            self.min_value,
2289            self.max_value - 1,
2290            value=self.min_value,
2291            pos=[(d - 0.5, 0) + slider_pos, (0.5 - d, 0) + slider_pos],
2292            show_value=False,
2293            c=bc[0],
2294            alpha=1,
2295        )
Arguments:
  • shape : (str, list) shape of the grid of renderers in format (rows, columns). Ignored if N is specified.
  • N : (int) number of desired renderers arranged in a grid automatically.
  • pos : (list) (x,y) position in pixels of top-left corner of the rendering window on the screen
  • size : (str, list) size of the rendering window. If 'auto', guess it based on screensize.
  • screensize : (list) physical size of the monitor screen in pixels
  • bg : (color, str) background color or specify jpg image file name with path
  • bg2 : (color) background color of a gradient towards the top
  • title : (str) window title
  • axes : (int) axis type-1 can be fully customized by passing a dictionary. Check addons.Axes() for the full list of options. Set the type of axes to be shown:
    • 0, no axes
    • 1, draw three gray grid walls
    • 2, show cartesian axes from (0,0,0)
    • 3, show positive range of cartesian axes from (0,0,0)
    • 4, show a triad at bottom left
    • 5, show a cube at bottom left
    • 6, mark the corners of the bounding box
    • 7, draw a 3D ruler at each side of the cartesian axes
    • 8, show the vtkCubeAxesActor object
    • 9, show the bounding box outLine
    • 10, show three circles representing the maximum bounding box
    • 11, show a large grid on the x-y plane
    • 12, show polar axes
    • 13, draw a simple ruler at the bottom of the window
    • 14: draw a camera orientation widget
  • sharecam : (bool) if False each renderer will have an independent camera
  • interactive : (bool) if True will stop after show() to allow interaction with the 3d scene
  • offscreen : (bool) if True will not show the rendering window
  • qt_widget : (QVTKRenderWindowInteractor) render in a Qt-Widget using an QVTKRenderWindowInteractor. See examples qt_windows[1,2,3].py and qt_cutter.py.
def pause(self) -> None:
2297    def pause(self) -> None:
2298        """Pause the animation."""
2299        self.is_playing = False
2300        if self.timer_id is not None:
2301            self.timer_callback("destroy", self.timer_id)
2302            self.timer_id = None
2303        self.play_pause_button.status(self.PLAY_SYMBOL)

Pause the animation.

def resume(self) -> None:
2305    def resume(self) -> None:
2306        """Resume the animation."""
2307        if self.timer_id is not None:
2308            self.timer_callback("destroy", self.timer_id)
2309        self.timer_id = self.timer_callback("create", dt=int(self.dt))
2310        self.is_playing = True
2311        self.play_pause_button.status(self.PAUSE_SYMBOL)

Resume the animation.

def toggle(self, _obj, _evt) -> None:
2313    def toggle(self, _obj, _evt) -> None:
2314        """Toggle between play and pause."""
2315        if not self.is_playing:
2316            self.resume()
2317        else:
2318            self.pause()

Toggle between play and pause.

def oneforward(self, _obj, _evt) -> None:
2320    def oneforward(self, _obj, _evt) -> None:
2321        """Advance the animation by one frame."""
2322        self.pause()
2323        self.set_frame(self.value + 1)

Advance the animation by one frame.

def onebackward(self, _obj, _evt) -> None:
2325    def onebackward(self, _obj, _evt) -> None:
2326        """Go back one frame in the animation."""
2327        self.pause()
2328        self.set_frame(self.value - 1)

Go back one frame in the animation.

def set_frame(self, value: int) -> None:
2330    def set_frame(self, value: int) -> None:
2331        """Set the current value of the animation."""
2332        if self._loop:
2333            if value < self.min_value:
2334                value = self.max_value - 1
2335            elif value >= self.max_value:
2336                value = self.min_value
2337        else:
2338            if value < self.min_value:
2339                self.pause()
2340                value = self.min_value
2341            elif value >= self.max_value - 1:
2342                value = self.max_value - 1
2343                self.pause()
2344
2345        if self.value != value:
2346            self.value = value
2347            self.slider.value = value
2348            self._func(value)

Set the current value of the animation.

def stop(self) -> AnimationPlayer:
2357    def stop(self) -> "AnimationPlayer":
2358        """
2359        Stop the animation timers, remove buttons and slider.
2360        Behave like a normal `Plotter` after this.
2361        """
2362        # stop timer
2363        if self.timer_id is not None:
2364            self.timer_callback("destroy", self.timer_id)
2365            self.timer_id = None
2366
2367        # remove callbacks
2368        self.remove_callback(self.timer_callback_id)
2369
2370        # remove buttons
2371        self.slider.off()
2372        self.renderer.RemoveActor(self.play_pause_button.actor)
2373        self.renderer.RemoveActor(self.button_oneback.actor)
2374        self.renderer.RemoveActor(self.button_oneforward.actor)
2375        return self

Stop the animation timers, remove buttons and slider. Behave like a normal Plotter after this.