vedo.applications

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

   1#!/usr/bin/env python3
   2# -*- coding: utf-8 -*-
   3import time
   4import os
   5from typing import Callable
   6
   7import numpy as np
   8
   9import vedo
  10from vedo.colors import color_map, get_color
  11from vedo.utils import is_sequence, lin_interpolate, mag, precision
  12from vedo.plotter import Event, Plotter
  13from vedo.pointcloud import fit_plane, Points
  14from vedo.shapes import Line, Ribbon, Spline, Text2D
  15from vedo.pyplot import CornerHistogram
  16from vedo.addons import SliderWidget
  17
  18
  19__docformat__ = "google"
  20
  21__doc__ = """
  22This module contains vedo applications which provide some *ready-to-use* funcionalities
  23
  24<img src="https://vedo.embl.es/images/advanced/app_raycaster.gif" width="500">
  25"""
  26
  27__all__ = [
  28    "Browser",
  29    "IsosurfaceBrowser",
  30    "FreeHandCutPlotter",
  31    "RayCastPlotter",
  32    "Slicer3DPlotter",
  33    "Slicer2DPlotter",
  34    "SplinePlotter",
  35    "AnimationPlayer",
  36    "Clock",
  37]
  38
  39
  40#################################
  41class Slicer3DPlotter(Plotter):
  42    """
  43    Generate a rendering window with slicing planes for the input Volume.
  44    """
  45
  46    def __init__(
  47        self,
  48        volume,
  49        alpha=1,
  50        cmaps=("gist_ncar_r", "hot_r", "bone_r", "jet", "Spectral_r"),
  51        map2cells=False,  # buggy
  52        clamp=True,
  53        use_slider3d=False,
  54        show_histo=True,
  55        show_icon=True,
  56        draggable=False,
  57        pos=(0, 0),
  58        size="auto",
  59        screensize="auto",
  60        title="",
  61        bg="white",
  62        bg2="lightblue",
  63        axes=7,
  64        resetcam=True,
  65        interactive=True,
  66    ):
  67        """
  68        Generate a rendering window with slicing planes for the input Volume.
  69
  70        Arguments:
  71            alpha : (float)
  72                transparency of the slicing planes
  73            cmaps : (list)
  74                list of color maps names to cycle when clicking button
  75            map2cells : (bool)
  76                scalars are mapped to cells, not interpolated
  77            clamp : (bool)
  78                clamp scalar to reduce the effect of tails in color mapping
  79            use_slider3d : (bool)
  80                show sliders attached along the axes
  81            show_histo : (bool)
  82                show histogram on bottom left
  83            show_icon : (bool)
  84                show a small 3D rendering icon of the volume
  85            draggable : (bool)
  86                make the icon draggable
  87
  88        Examples:
  89            - [slicer1.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/slicer1.py)
  90
  91            <img src="https://vedo.embl.es/images/volumetric/slicer1.jpg" width="500">
  92        """
  93        self._cmap_slicer = "gist_ncar_r"
  94
  95        if not title:
  96            if volume.filename:
  97                title = volume.filename
  98            else:
  99                title = "Volume Slicer"
 100
 101        ################################
 102        Plotter.__init__(
 103            self,
 104            pos=pos,
 105            bg=bg,
 106            bg2=bg2,
 107            size=size,
 108            screensize=screensize,
 109            title=title,
 110            interactive=interactive,
 111            axes=axes,
 112        )
 113        ################################
 114        box = volume.box().wireframe().alpha(0.1)
 115
 116        self.show(box, viewup="z", resetcam=resetcam, interactive=False)
 117        if show_icon:
 118            self.add_inset(volume, pos=(0.85, 0.85), size=0.15, c="w", draggable=draggable)
 119
 120        # inits
 121        la, ld = 0.7, 0.3  # ambient, diffuse
 122        dims = volume.dimensions()
 123        data = volume.pointdata[0]
 124        rmin, rmax = volume.imagedata().GetScalarRange()
 125        if clamp:
 126            hdata, edg = np.histogram(data, bins=50)
 127            logdata = np.log(hdata + 1)
 128            # mean  of the logscale plot
 129            meanlog = np.sum(np.multiply(edg[:-1], logdata)) / np.sum(logdata)
 130            rmax = min(rmax, meanlog + (meanlog - rmin) * 0.9)
 131            rmin = max(rmin, meanlog - (rmax - meanlog) * 0.9)
 132            vedo.logger.debug(
 133                "scalar range clamped to range: ("
 134                + precision(rmin, 3)
 135                + ", "
 136                + precision(rmax, 3)
 137                + ")"
 138            )
 139        self._cmap_slicer = cmaps[0]
 140        visibles = [None, None, None]
 141        msh = volume.zslice(int(dims[2] / 2))
 142        msh.alpha(alpha).lighting("", la, ld, 0)
 143        msh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
 144        if map2cells:
 145            msh.mapPointsToCells()
 146        self.renderer.AddActor(msh)
 147        visibles[2] = msh
 148        msh.add_scalarbar(pos=(0.04, 0.0), horizontal=True, font_size=0)
 149
 150        def slider_function_x(widget, event):
 151            i = int(widget.GetRepresentation().GetValue())
 152            msh = volume.xslice(i).alpha(alpha).lighting("", la, ld, 0)
 153            msh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
 154            if map2cells:
 155                msh.mapPointsToCells()
 156            self.renderer.RemoveActor(visibles[0])
 157            if i and i < dims[0]:
 158                self.renderer.AddActor(msh)
 159            visibles[0] = msh
 160
 161        def slider_function_y(widget, event):
 162            i = int(widget.GetRepresentation().GetValue())
 163            msh = volume.yslice(i).alpha(alpha).lighting("", la, ld, 0)
 164            msh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
 165            if map2cells:
 166                msh.mapPointsToCells()
 167            self.renderer.RemoveActor(visibles[1])
 168            if i and i < dims[1]:
 169                self.renderer.AddActor(msh)
 170            visibles[1] = msh
 171
 172        def slider_function_z(widget, event):
 173            i = int(widget.GetRepresentation().GetValue())
 174            msh = volume.zslice(i).alpha(alpha).lighting("", la, ld, 0)
 175            msh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
 176            if map2cells:
 177                msh.mapPointsToCells()
 178            self.renderer.RemoveActor(visibles[2])
 179            if i and i < dims[2]:
 180                self.renderer.AddActor(msh)
 181            visibles[2] = msh
 182
 183        cx, cy, cz, ch = "dr", "dg", "db", (0.3, 0.3, 0.3)
 184        if np.sum(self.renderer.GetBackground()) < 1.5:
 185            cx, cy, cz = "lr", "lg", "lb"
 186            ch = (0.8, 0.8, 0.8)
 187
 188        if not use_slider3d:
 189            self.add_slider(
 190                slider_function_x,
 191                0,
 192                dims[0],
 193                title="X",
 194                title_size=0.5,
 195                pos=[(0.8, 0.12), (0.95, 0.12)],
 196                show_value=False,
 197                c=cx,
 198            )
 199            self.add_slider(
 200                slider_function_y,
 201                0,
 202                dims[1],
 203                title="Y",
 204                title_size=0.5,
 205                pos=[(0.8, 0.08), (0.95, 0.08)],
 206                show_value=False,
 207                c=cy,
 208            )
 209            self.add_slider(
 210                slider_function_z,
 211                0,
 212                dims[2],
 213                title="Z",
 214                title_size=0.6,
 215                value=int(dims[2] / 2),
 216                pos=[(0.8, 0.04), (0.95, 0.04)],
 217                show_value=False,
 218                c=cz,
 219            )
 220        else:  # 3d sliders attached to the axes bounds
 221            bs = box.bounds()
 222            self.add_slider3d(
 223                slider_function_x,
 224                pos1=(bs[0], bs[2], bs[4]),
 225                pos2=(bs[1], bs[2], bs[4]),
 226                xmin=0,
 227                xmax=dims[0],
 228                t=box.diagonal_size() / mag(box.xbounds()) * 0.6,
 229                c=cx,
 230                show_value=False,
 231            )
 232            self.add_slider3d(
 233                slider_function_y,
 234                pos1=(bs[1], bs[2], bs[4]),
 235                pos2=(bs[1], bs[3], bs[4]),
 236                xmin=0,
 237                xmax=dims[1],
 238                t=box.diagonal_size() / mag(box.ybounds()) * 0.6,
 239                c=cy,
 240                show_value=False,
 241            )
 242            self.add_slider3d(
 243                slider_function_z,
 244                pos1=(bs[0], bs[2], bs[4]),
 245                pos2=(bs[0], bs[2], bs[5]),
 246                xmin=0,
 247                xmax=dims[2],
 248                value=int(dims[2] / 2),
 249                t=box.diagonal_size() / mag(box.zbounds()) * 0.6,
 250                c=cz,
 251                show_value=False,
 252            )
 253
 254        #################
 255        def buttonfunc():
 256            bu.switch()
 257            self._cmap_slicer = bu.status()
 258            for mesh in visibles:
 259                if mesh:
 260                    mesh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
 261                    if map2cells:
 262                        mesh.mapPointsToCells()
 263            self.renderer.RemoveActor(mesh.scalarbar)
 264            mesh.add_scalarbar(pos=(0.04, 0.0), horizontal=True)
 265            self.renderer.AddActor(mesh.scalarbar)
 266
 267        bu = self.add_button(
 268            buttonfunc,
 269            pos=(0.27, 0.005),
 270            states=cmaps,
 271            c=["db"] * len(cmaps),
 272            bc=["lb"] * len(cmaps),  # colors of states
 273            size=14,
 274            bold=True,
 275        )
 276
 277        #################
 278        hist = None
 279        if show_histo:
 280            hist = CornerHistogram(
 281                data, s=0.2, bins=25, logscale=1, pos=(0.02, 0.02), c=ch, bg=ch, alpha=0.7
 282            )
 283
 284        self.add([msh, hist])
 285        if interactive:
 286            self.interactive()
 287
 288
 289########################################################################################
 290class Slicer2DPlotter(Plotter):
 291    """
 292    A single slice of a Volume which always faces the camera,
 293    but at the same time can be oriented arbitrarily in space.
 294    """
 295
 296    def __init__(self, volume, levels=(None, None), histo_color="red5", **kwargs):
 297        """
 298        A single slice of a Volume which always faces the camera,
 299        but at the same time can be oriented arbitrarily in space.
 300
 301        Arguments:
 302            levels : (list)
 303                window and color levels
 304            histo_color : (color)
 305                histogram color, use `None` to disable it
 306
 307        <img src="https://vedo.embl.es/images/volumetric/read_volume3.jpg" width="500">
 308        """
 309        if "shape" not in kwargs:
 310            custom_shape = [  # define here the 2 rendering rectangle spaces
 311                dict(bottomleft=(0.0, 0.0), topright=(1, 1), bg="k9"),  # the full window
 312                dict(bottomleft=(0.8, 0.8), topright=(1, 1), bg="k8", bg2="lb"),
 313            ]
 314            kwargs["shape"] = custom_shape
 315
 316        Plotter.__init__(self, **kwargs)
 317
 318        # reuse the same underlying data as in vol
 319        vsl = vedo.volume.VolumeSlice(volume)
 320
 321        # no argument will grab the existing cmap in vol (or use build_lut())
 322        vsl.colorize()
 323
 324        if levels[0] and levels[1]:
 325            vsl.lighting(window=levels[0], level=levels[1])
 326
 327        usage = Text2D(
 328            (
 329                "Left click & drag  :rightarrow modify luminosity and contrast\n"
 330                "SHIFT+Left click   :rightarrow slice image obliquely\n"
 331                "SHIFT+Middle click :rightarrow slice image perpendicularly\n"
 332                "R                  :rightarrow Reset the Window/Color levels\n"
 333                "X                  :rightarrow Reset to sagittal view\n"
 334                "Y                  :rightarrow Reset to coronal view\n"
 335                "Z                  :rightarrow Reset to axial view"
 336            ),
 337            font="Calco",
 338            pos="top-left",
 339            s=0.8,
 340            bg="yellow",
 341            alpha=0.25,
 342        )
 343
 344        hist = None
 345        if histo_color is not None:
 346            # hist = CornerHistogram(
 347            #     volume.pointdata[0],
 348            #     bins=25,
 349            #     logscale=1,
 350            #     pos=(0.02, 0.02),
 351            #     s=0.175,
 352            #     c="dg",
 353            #     bg="k",
 354            #     alpha=1,
 355            # )
 356            hist = vedo.pyplot.histogram(
 357                volume.pointdata[0],
 358                bins=10,
 359                logscale=True,
 360                c=histo_color,
 361                ytitle="log_10 (counts)",
 362                axes=dict(text_scale=1.9),
 363            )
 364            hist = hist.as2d(pos="bottom-left", scale=0.5)
 365
 366        axes = kwargs.pop("axes", 7)
 367        interactive = kwargs.pop("interactive", True)
 368        if axes == 7:
 369            ax = vedo.addons.RulerAxes(vsl, xtitle="x - ", ytitle="y - ", ztitle="z - ")
 370
 371        box = vsl.box().alpha(0.2)
 372        self.at(0).show(vsl, box, ax, usage, hist, mode="image")
 373        self.at(1).show(volume, interactive=interactive)
 374
 375
 376########################################################################
 377class RayCastPlotter(Plotter):
 378    """
 379    Generate Volume rendering using ray casting.
 380    """
 381
 382    def __init__(self, volume, **kwargs):
 383        """
 384        Generate a window for Volume rendering using ray casting.
 385
 386        Returns:
 387            `vedo.Plotter` object.
 388
 389        Examples:
 390            - [app_raycaster.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/app_raycaster.py)
 391
 392            ![](https://vedo.embl.es/images/advanced/app_raycaster.gif)
 393        """
 394
 395        Plotter.__init__(self, **kwargs)
 396
 397        self.alphaslider0 = 0.33
 398        self.alphaslider1 = 0.66
 399        self.alphaslider2 = 1
 400
 401        self.property = volume.GetProperty()
 402        img = volume.imagedata()
 403
 404        if volume.dimensions()[2] < 3:
 405            vedo.logger.error("RayCastPlotter: not enough z slices.")
 406            raise RuntimeError
 407
 408        smin, smax = img.GetScalarRange()
 409        x0alpha = smin + (smax - smin) * 0.25
 410        x1alpha = smin + (smax - smin) * 0.5
 411        x2alpha = smin + (smax - smin) * 1.0
 412
 413        ############################## color map slider
 414        # Create transfer mapping scalar value to color
 415        cmaps = [
 416            "jet",
 417            "viridis",
 418            "bone",
 419            "hot",
 420            "plasma",
 421            "winter",
 422            "cool",
 423            "gist_earth",
 424            "coolwarm",
 425            "tab10",
 426        ]
 427        cols_cmaps = []
 428        for cm in cmaps:
 429            cols = color_map(range(0, 21), cm, 0, 20)  # sample 20 colors
 430            cols_cmaps.append(cols)
 431        Ncols = len(cmaps)
 432        csl = (0.9, 0.9, 0.9)
 433        if sum(get_color(self.renderer.GetBackground())) > 1.5:
 434            csl = (0.1, 0.1, 0.1)
 435
 436        def sliderColorMap(widget, event):
 437            sliderRep = widget.GetRepresentation()
 438            k = int(sliderRep.GetValue())
 439            sliderRep.SetTitleText(cmaps[k])
 440            volume.color(cmaps[k])
 441
 442        w1 = self.add_slider(
 443            sliderColorMap,
 444            0,
 445            Ncols - 1,
 446            value=0,
 447            show_value=0,
 448            title=cmaps[0],
 449            c=csl,
 450            pos=[(0.8, 0.05), (0.965, 0.05)],
 451        )
 452        w1.GetRepresentation().SetTitleHeight(0.018)
 453
 454        ############################## alpha sliders
 455        # Create transfer mapping scalar value to opacity
 456        opacityTransferFunction = self.property.GetScalarOpacity()
 457
 458        def setOTF():
 459            opacityTransferFunction.RemoveAllPoints()
 460            opacityTransferFunction.AddPoint(smin, 0.0)
 461            opacityTransferFunction.AddPoint(smin + (smax - smin) * 0.1, 0.0)
 462            opacityTransferFunction.AddPoint(x0alpha, self.alphaslider0)
 463            opacityTransferFunction.AddPoint(x1alpha, self.alphaslider1)
 464            opacityTransferFunction.AddPoint(x2alpha, self.alphaslider2)
 465
 466        setOTF()
 467
 468        def sliderA0(widget, event):
 469            self.alphaslider0 = widget.GetRepresentation().GetValue()
 470            setOTF()
 471
 472        self.add_slider(
 473            sliderA0,
 474            0,
 475            1,
 476            value=self.alphaslider0,
 477            pos=[(0.84, 0.1), (0.84, 0.26)],
 478            c=csl,
 479            show_value=0,
 480        )
 481
 482        def sliderA1(widget, event):
 483            self.alphaslider1 = widget.GetRepresentation().GetValue()
 484            setOTF()
 485
 486        self.add_slider(
 487            sliderA1,
 488            0,
 489            1,
 490            value=self.alphaslider1,
 491            pos=[(0.89, 0.1), (0.89, 0.26)],
 492            c=csl,
 493            show_value=0,
 494        )
 495
 496        def sliderA2(widget, event):
 497            self.alphaslider2 = widget.GetRepresentation().GetValue()
 498            setOTF()
 499
 500        w2 = self.add_slider(
 501            sliderA2,
 502            0,
 503            1,
 504            value=self.alphaslider2,
 505            pos=[(0.96, 0.1), (0.96, 0.26)],
 506            c=csl,
 507            show_value=0,
 508            title="Opacity levels",
 509        )
 510        w2.GetRepresentation().SetTitleHeight(0.016)
 511
 512        # add a button
 513        def button_func_mode():
 514            s = volume.mode()
 515            snew = (s + 1) % 2
 516            volume.mode(snew)
 517            bum.switch()
 518
 519        bum = self.add_button(
 520            button_func_mode,
 521            pos=(0.7, 0.035),
 522            states=["composite", "max proj."],
 523            c=["bb", "gray"],
 524            bc=["gray", "bb"],  # colors of states
 525            font="",
 526            size=16,
 527            bold=0,
 528            italic=False,
 529        )
 530        bum.status(volume.mode())
 531
 532        # add histogram of scalar
 533        plot = CornerHistogram(
 534            volume,
 535            bins=25,
 536            logscale=1,
 537            c=(0.7, 0.7, 0.7),
 538            bg=(0.7, 0.7, 0.7),
 539            pos=(0.78, 0.065),
 540            lines=True,
 541            dots=False,
 542            nmax=3.1415e06,  # subsample otherwise is too slow
 543        )
 544
 545        plot.GetPosition2Coordinate().SetValue(0.197, 0.20, 0)
 546        plot.GetXAxisActor2D().SetFontFactor(0.7)
 547        plot.GetProperty().SetOpacity(0.5)
 548        self.add([plot, volume])
 549
 550
 551#####################################################################################
 552class IsosurfaceBrowser(Plotter):
 553    """
 554    Generate a Volume isosurfacing controlled by a slider.
 555    """
 556
 557    def __init__(
 558        self,
 559        volume,
 560        isovalue=None,
 561        c=None,
 562        alpha=1,
 563        lego=False,
 564        res=50,
 565        use_gpu=False,
 566        precompute=False,
 567        progress=False,
 568        cmap="hot",
 569        delayed=False,
 570        sliderpos=4,
 571        pos=(0, 0),
 572        size="auto",
 573        screensize="auto",
 574        title="",
 575        bg="white",
 576        bg2=None,
 577        axes=1,
 578        interactive=True,
 579    ):
 580        """
 581        Generate a `vedo.Plotter` for Volume isosurfacing using a slider.
 582
 583        Set `delayed=True` to delay slider update on mouse release.
 584
 585        Set `res` to set the resolution, e.g. the number of desired isosurfaces to be
 586        generated on the fly.
 587
 588        Set `precompute=True` to precompute the isosurfaces (so slider browsing will be smoother).
 589
 590        Examples:
 591            - [app_isobrowser.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/app_isobrowser.py)
 592
 593                ![](https://vedo.embl.es/images/advanced/app_isobrowser.gif)
 594        """
 595
 596        Plotter.__init__(
 597            self,
 598            pos=pos,
 599            bg=bg,
 600            bg2=bg2,
 601            size=size,
 602            screensize=screensize,
 603            title=title,
 604            interactive=interactive,
 605            axes=axes,
 606        )
 607
 608        ### GPU ################################
 609        if use_gpu and hasattr(volume.GetProperty(), "GetIsoSurfaceValues"):
 610
 611            scrange = volume.scalar_range()
 612            delta = scrange[1] - scrange[0]
 613            if not delta:
 614                return
 615
 616            if isovalue is None:
 617                isovalue = delta / 3.0 + scrange[0]
 618
 619            ### isovalue slider callback
 620            def slider_isovalue(widget, event):
 621                value = widget.GetRepresentation().GetValue()
 622                isovals.SetValue(0, value)
 623
 624            isovals = volume.GetProperty().GetIsoSurfaceValues()
 625            isovals.SetValue(0, isovalue)
 626            self.renderer.AddActor(volume.mode(5).alpha(alpha).c(c))
 627
 628            self.add_slider(
 629                slider_isovalue,
 630                scrange[0] + 0.02 * delta,
 631                scrange[1] - 0.02 * delta,
 632                value=isovalue,
 633                pos=sliderpos,
 634                title="scalar value",
 635                show_value=True,
 636                delayed=delayed,
 637            )
 638
 639        ### CPU ################################
 640        else:
 641
 642            self._prev_value = 1e30
 643
 644            scrange = volume.scalar_range()
 645            delta = scrange[1] - scrange[0]
 646            if not delta:
 647                return
 648
 649            if lego:
 650                res = int(res / 2)  # because lego is much slower
 651                slidertitle = ""
 652            else:
 653                slidertitle = "scalar value"
 654
 655            allowed_vals = np.linspace(scrange[0], scrange[1], num=res)
 656
 657            bacts = {}  # cache the meshes so we dont need to recompute
 658            if precompute:
 659                delayed = False  # no need to delay the slider in this case
 660                if progress:
 661                    pb = vedo.ProgressBar(0, len(allowed_vals), delay=1)
 662
 663                for value in allowed_vals:
 664                    value_name = precision(value, 2)
 665                    if lego:
 666                        mesh = volume.legosurface(vmin=value)
 667                        if mesh.ncells:
 668                            mesh.cmap(cmap, vmin=scrange[0], vmax=scrange[1], on="cells")
 669                    else:
 670                        mesh = volume.isosurface(value).color(c).alpha(alpha)
 671                    bacts.update({value_name: mesh})  # store it
 672                    if progress:
 673                        pb.print("isosurfacing volume..")
 674
 675            ### isovalue slider callback
 676            def slider_isovalue(widget, event):
 677
 678                prevact = self.actors[0]
 679                if isinstance(widget, float):
 680                    value = widget
 681                else:
 682                    value = widget.GetRepresentation().GetValue()
 683
 684                # snap to the closest
 685                idx = (np.abs(allowed_vals - value)).argmin()
 686                value = allowed_vals[idx]
 687
 688                if abs(value - self._prev_value) / delta < 0.001:
 689                    return
 690                self._prev_value = value
 691
 692                value_name = precision(value, 2)
 693                if value_name in bacts:  # reusing the already existing mesh
 694                    # print('reusing')
 695                    mesh = bacts[value_name]
 696                else:  # else generate it
 697                    # print('generating', value)
 698                    if lego:
 699                        mesh = volume.legosurface(vmin=value)
 700                        if mesh.ncells:
 701                            mesh.cmap(cmap, vmin=scrange[0], vmax=scrange[1], on="cells")
 702                    else:
 703                        mesh = volume.isosurface(value).color(c).alpha(alpha)
 704                    bacts.update({value_name: mesh})  # store it
 705
 706                self.renderer.RemoveActor(prevact)
 707                self.renderer.AddActor(mesh)
 708                self.actors[0] = mesh
 709
 710            ################################################
 711
 712            if isovalue is None:
 713                isovalue = delta / 3.0 + scrange[0]
 714
 715            self.actors = [None]
 716            slider_isovalue(isovalue, "")  # init call
 717            if lego:
 718                self.actors[0].add_scalarbar(pos=(0.8, 0.12))
 719
 720            self.add_slider(
 721                slider_isovalue,
 722                scrange[0] + 0.02 * delta,
 723                scrange[1] - 0.02 * delta,
 724                value=isovalue,
 725                pos=sliderpos,
 726                title=slidertitle,
 727                show_value=True,
 728                delayed=delayed,
 729            )
 730
 731
 732##############################################################################
 733class Browser(Plotter):
 734    """
 735    Browse a series of vedo objects by using a simple slider.
 736    """
 737    def __init__(
 738        self,
 739        objects=(),
 740        sliderpos=((0.50, 0.07), (0.95, 0.07)),
 741        c=None,  # slider color
 742        prefix="",
 743        font="Calco", # slider font
 744        axes=1,
 745        resetcam=False, # resetcam while using the slider
 746        **kwargs,
 747    ):
 748        """
 749        Browse a series of vedo objects by using a simple slider.
 750
 751        Examples:
 752            ```python
 753            from vedo import load, dataurl
 754            from vedo.applications import Browser
 755            meshes = load(dataurl+'timecourse1d.npy') # python list of Meshes
 756            plt = Browser(meshes, bg='k')             # vedo.Plotter
 757            plt.show(interactive=False, zoom='tight') # show the meshes
 758            plt.play(dt=50)                           # delay in milliseconds
 759            plt.close()
 760            ```
 761
 762        - [morphomatics_tube.py](https://github.com/marcomusy/vedo/tree/master/examples/other/morphomatics_tube.py)
 763        """
 764        Plotter.__init__(self, axes=axes, **kwargs)
 765
 766        if isinstance(objects, str):
 767            objects = vedo.file_io.load(objects)
 768
 769        self += objects
 770
 771        self.slider = None
 772        self.timer_callback_id = None
 773
 774        # define the slider func ##########################
 775        def slider_function(widget=None, event=None):
 776
 777            must_render = False
 778            if isinstance(widget, vedo.plotter.Event):
 779                if self.slider.value < len(self.actors)-1:
 780                    self.slider.value = self.slider.value + 1
 781                else:
 782                    self.slider.value = 0
 783                must_render = True
 784
 785            k = int(self.slider.value)
 786            ak = self.actors[k]
 787            for a in self.actors:
 788                if a == ak:
 789                    a.on()
 790                else:
 791                    a.off()
 792            if resetcam:
 793                self.reset_camera()
 794            tx = str(k)
 795            if ak.filename:
 796                tx = ak.filename.split("/")[-1]
 797                tx = tx.split("\\")[-1]  # windows os
 798            elif ak.name:
 799                tx = ak.name
 800
 801            self.slider.title = prefix + tx
 802
 803            if must_render:
 804                self.render()
 805        ##################################################
 806
 807        self.slider_function = slider_function
 808        self.slider = self.add_slider(
 809            slider_function,
 810            0.5,
 811            len(objects) - 0.5,
 812            pos=sliderpos,
 813            font=font,
 814            c=c,
 815            show_value=False,
 816        )
 817        self.slider.GetRepresentation().SetTitleHeight(0.020)
 818        slider_function(self.slider)  # init call
 819    
 820    def play(self, dt=100):
 821        """Start playing the slides at a given speed."""
 822        self.timer_callback_id = self.add_callback("timer", self.slider_function)
 823        self.timer_callback("start", dt=dt)
 824        self.interactive()
 825
 826
 827#############################################################################################
 828class FreeHandCutPlotter(Plotter):
 829    """A tool to edit meshes interactively."""
 830
 831    # thanks to Jakub Kaminski for the original version of this script
 832    def __init__(
 833        self,
 834        mesh,
 835        splined=True,
 836        font="Bongas",
 837        alpha=0.9,
 838        lw=4,
 839        lc="red5",
 840        pc="red4",
 841        c="green3",
 842        tc="k9",
 843        tol=0.008,
 844        **options,
 845    ):
 846        """
 847        A `vedo.Plotter` derived class which edits polygonal meshes interactively.
 848
 849        Can also be invoked from command line with:
 850
 851        ```bash
 852        vedo --edit https://vedo.embl.es/examples/data/porsche.ply
 853        ```
 854
 855        Usage:
 856            - Left-click and hold to rotate
 857            - Right-click and move to draw line
 858            - Second right-click to stop drawing
 859            - Press "c" to clear points
 860            -       "z/Z" to cut mesh (Z inverts inside-out the selection area)
 861            -       "L" to keep only the largest connected surface
 862            -       "s" to save mesh to file (tag `_edited` is appended to filename)
 863            -       "u" to undo last action
 864            -       "h" for help, "i" for info
 865
 866        Arguments:
 867            mesh : (Mesh, Points)
 868                The input Mesh or pointcloud.
 869            splined : (bool)
 870                join points with a spline or a simple line.
 871            font : (str)
 872                Font name for the instructions.
 873            alpha : (float)
 874                transparency of the instruction message panel.
 875            lw : (str)
 876                selection line width.
 877            lc : (str)
 878                selection line color.
 879            pc : (str)
 880                selection points color.
 881            c : (str)
 882                background color of instructions.
 883            tc : (str)
 884                text color of instructions.
 885            tol : (int)
 886                tolerance of the point proximity.
 887
 888        Examples:
 889            - [cut_freehand.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/cut_freehand.py)
 890
 891                ![](https://vedo.embl.es/images/basic/cutFreeHand.gif)
 892        """
 893
 894        if not isinstance(mesh, Points):
 895            vedo.logger.error("FreeHandCutPlotter input must be Points or Mesh")
 896            raise RuntimeError()
 897
 898        super().__init__(**options)
 899
 900        self.mesh = mesh
 901        self.mesh_prev = mesh
 902        self.splined = splined
 903        self.linecolor = lc
 904        self.linewidth = lw
 905        self.pointcolor = pc
 906        self.color = c
 907        self.alpha = alpha
 908
 909        self.msg = "Right-click and move to draw line\n"
 910        self.msg += "Second right-click to stop drawing\n"
 911        self.msg += "Press L to extract largest surface\n"
 912        self.msg += "        z/Z to cut mesh (s to save)\n"
 913        self.msg += "        c to clear points, u to undo"
 914        self.txt2d = Text2D(self.msg, pos="top-left", font=font, s=0.9)
 915        self.txt2d.c(tc).background(c, alpha).frame()
 916
 917        self.idkeypress = self.add_callback("KeyPress", self._on_keypress)
 918        self.idrightclck = self.add_callback("RightButton", self._on_right_click)
 919        self.idmousemove = self.add_callback("MouseMove", self._on_mouse_move)
 920        self.drawmode = False
 921        self.tol = tol  # tolerance of point distance
 922        self.cpoints = []
 923        self.points = None
 924        self.spline = None
 925        self.jline = None
 926        self.topline = None
 927        self.top_pts = []
 928
 929    def init(self, init_points):
 930        """Set an initial number of points to define a region"""
 931        if isinstance(init_points, Points):
 932            self.cpoints = init_points.points()
 933        else:
 934            self.cpoints = np.array(init_points)
 935        self.points = Points(self.cpoints, r=self.linewidth).c(self.pointcolor).pickable(0)
 936        if self.splined:
 937            self.spline = Spline(self.cpoints, res=len(self.cpoints) * 4)
 938        else:
 939            self.spline = Line(self.cpoints)
 940        self.spline.lw(self.linewidth).c(self.linecolor).pickable(False)
 941        self.jline = Line(self.cpoints[0], self.cpoints[-1], lw=1, c=self.linecolor).pickable(0)
 942        self.add([self.points, self.spline, self.jline]).render()
 943        return self
 944
 945    def _on_right_click(self, evt):
 946        self.drawmode = not self.drawmode  # toggle mode
 947        if self.drawmode:
 948            self.txt2d.background(self.linecolor, self.alpha)
 949        else:
 950            self.txt2d.background(self.color, self.alpha)
 951            if len(self.cpoints) > 2:
 952                self.remove([self.spline, self.jline])
 953                if self.splined:  # show the spline closed
 954                    self.spline = Spline(self.cpoints, closed=True, res=len(self.cpoints) * 4)
 955                else:
 956                    self.spline = Line(self.cpoints, closed=True)
 957                self.spline.lw(self.linewidth).c(self.linecolor).pickable(False)
 958                self.add(self.spline)
 959        self.render()
 960
 961    def _on_mouse_move(self, evt):
 962        if self.drawmode:
 963            cpt = self.compute_world_coordinate(evt.picked2d)  # make this 2d-screen point 3d
 964            if self.cpoints and mag(cpt - self.cpoints[-1]) < self.mesh.diagonal_size() * self.tol:
 965                return  # new point is too close to the last one. skip
 966            self.cpoints.append(cpt)
 967            if len(self.cpoints) > 2:
 968                self.remove([self.points, self.spline, self.jline, self.topline])
 969                self.points = Points(self.cpoints, r=self.linewidth).c(self.pointcolor).pickable(0)
 970                if self.splined:
 971                    self.spline = Spline(self.cpoints, res=len(self.cpoints) * 4)  # not closed here
 972                else:
 973                    self.spline = Line(self.cpoints)
 974
 975                if evt.actor:
 976                    self.top_pts.append(evt.picked3d)
 977                    self.topline = Points(self.top_pts, r=self.linewidth)
 978                    self.topline.c(self.linecolor).pickable(False)
 979
 980                self.spline.lw(self.linewidth).c(self.linecolor).pickable(False)
 981                self.txt2d.background(self.linecolor)
 982                self.jline = Line(self.cpoints[0], self.cpoints[-1], lw=1, c=self.linecolor).pickable(0)
 983                self.add([self.points, self.spline, self.jline, self.topline]).render()
 984
 985    def _on_keypress(self, evt):
 986        if evt.keypress.lower() == "z" and self.spline:  # Cut mesh with a ribbon-like surface
 987            inv = False
 988            if evt.keypress == "Z":
 989                inv = True
 990            self.txt2d.background("red8").text("  ... working ...  ")
 991            self.render()
 992            self.mesh_prev = self.mesh.clone()
 993            tol = self.mesh.diagonal_size() / 2  # size of ribbon (not shown)
 994            pts = self.spline.points()
 995            n = fit_plane(pts, signed=True).normal  # compute normal vector to points
 996            rb = Ribbon(pts - tol * n, pts + tol * n, closed=True)
 997            self.mesh.cut_with_mesh(rb, invert=inv)  # CUT
 998            self.txt2d.text(self.msg)  # put back original message
 999            if self.drawmode:
1000                self._on_right_click(evt)  # toggle mode to normal
1001            else:
1002                self.txt2d.background(self.color, self.alpha)
1003            self.remove([self.spline, self.points, self.jline, self.topline]).render()
1004            self.cpoints, self.points, self.spline = [], None, None
1005            self.top_pts, self.topline = [], None
1006
1007        elif evt.keypress == "L":
1008            self.txt2d.background("red8")
1009            self.txt2d.text(" ... removing smaller ... \n ... parts of the mesh ... ")
1010            self.render()
1011            self.remove(self.mesh)
1012            self.mesh_prev = self.mesh
1013            mcut = self.mesh.extract_largest_region()
1014            mcut.filename = self.mesh.filename  # copy over various properties
1015            mcut.name = self.mesh.name
1016            mcut.scalarbar = self.mesh.scalarbar
1017            mcut.info = self.mesh.info
1018            self.mesh = mcut                            # discard old mesh by overwriting it
1019            self.txt2d.text(self.msg).background(self.color)   # put back original message
1020            self.add(mcut).render()
1021
1022        elif evt.keypress == 'u':                       # Undo last action
1023            if self.drawmode:
1024                self._on_right_click(evt)               # toggle mode to normal
1025            else:
1026                self.txt2d.background(self.color, self.alpha)
1027            self.remove([self.mesh, self.spline, self.jline, self.points, self.topline])
1028            self.mesh = self.mesh_prev
1029            self.cpoints, self.points, self.spline = [], None, None
1030            self.top_pts, self.topline = [], None
1031            self.add(self.mesh).render()
1032
1033        elif evt.keypress in ("c", "Delete"):
1034            # clear all points
1035            self.remove([self.spline, self.points, self.jline, self.topline]).render()
1036            self.cpoints, self.points, self.spline = [], None, None
1037            self.top_pts, self.topline = [], None
1038
1039        elif evt.keypress == "r":  # reset camera and axes
1040            try:
1041                self.remove(self.axes_instances[0])
1042                self.axes_instances[0] = None
1043                self.add_global_axes(axtype=1, c=None, bounds=self.mesh.bounds())
1044                self.renderer.ResetCamera()
1045                self.render()
1046            except:
1047                pass
1048
1049        elif evt.keypress == "s":
1050            if self.mesh.filename:
1051                fname = os.path.basename(self.mesh.filename)
1052                fname, extension = os.path.splitext(fname)
1053                fname = fname.replace("_edited", "")
1054                fname = f"{fname}_edited{extension}"
1055            else:
1056                fname = "mesh_edited.vtk"
1057            self.write(fname)
1058
1059    def write(self, filename="mesh_edited.vtk"):
1060        """Save the resulting mesh to file"""
1061        self.mesh.write(filename)
1062        vedo.logger.info(f"mesh saved to file {filename}")
1063        return self
1064
1065    def start(self, *args, **kwargs):
1066        """Start window interaction (with mouse and keyboard)"""
1067        acts = [self.txt2d, self.mesh, self.points, self.spline, self.jline]
1068        self.show(acts + list(args), **kwargs)
1069        return self
1070
1071
1072########################################################################
1073class SplinePlotter(Plotter):
1074    """
1075    Interactive drawing of splined curves on meshes.
1076    """
1077
1078    def __init__(self, obj, init_points=(), closed=False, splined=True, **kwargs):
1079        """
1080        Create an interactive application that allows the user to click points and
1081        retrieve the coordinates of such points and optionally a spline or line
1082        (open or closed).
1083
1084        Input object can be a image file name or a 3D mesh.
1085        """
1086        super().__init__(**kwargs)
1087
1088        self.mode = "trackball"
1089        self.verbose = True
1090        self.splined = splined
1091        self.resolution = None  # spline resolution (None = automatic)
1092        self.closed = closed
1093        self.lcolor = "yellow4"
1094        self.lwidth = 3
1095        self.pcolor = "purple5"
1096        self.psize = 10
1097
1098        self.cpoints = list(init_points)
1099        self.vpoints = None
1100        self.line = None
1101
1102        if isinstance(obj, str):
1103            self.object = vedo.file_io.load(obj)
1104        else:
1105            self.object = obj
1106
1107        if isinstance(self.object, vedo.Picture):
1108            self.mode = "image"
1109            self.parallel_projection(True)
1110
1111        t = (
1112            "Click to add a point\n"
1113            "Right-click to remove it\n"
1114            "Drag mouse to change contrast\n"
1115            "Press c to clear points\n"
1116            "Press q to continue"
1117        )
1118        self.instructions = Text2D(t, pos="bottom-left", c="white", bg="green", font="Calco")
1119
1120        self += [self.object, self.instructions]
1121
1122        self.callid1 = self.add_callback("KeyPress", self._key_press)
1123        self.callid2 = self.add_callback("LeftButtonPress", self._on_left_click)
1124        self.callid3 = self.add_callback("RightButtonPress", self._on_right_click)
1125
1126    def points(self, newpts=None):
1127        """Retrieve the 3D coordinates of the clicked points"""
1128        if newpts is not None:
1129            self.cpoints = newpts
1130            self._update()
1131            return self
1132        return np.array(self.cpoints)
1133
1134    def _on_left_click(self, evt):
1135        if not evt.actor:
1136            return
1137        if evt.actor.name == "points":
1138            # remove clicked point if clicked twice
1139            pid = self.vpoints.closest_point(evt.picked3d, return_point_id=True)
1140            self.cpoints.pop(pid)
1141            self._update()
1142            return
1143        p = evt.picked3d
1144        self.cpoints.append(p)
1145        self._update()
1146        if self.verbose:
1147            vedo.colors.printc("Added point:", precision(p, 4), c="g")
1148
1149    def _on_right_click(self, evt):
1150        if evt.actor and len(self.cpoints) > 0:
1151            self.cpoints.pop()  # pop removes from the list the last pt
1152            self._update()
1153            if self.verbose:
1154                vedo.colors.printc("Deleted last point", c="r")
1155
1156    def _update(self):
1157        self.remove(self.line, self.vpoints)  # remove old points and spline
1158        self.vpoints = Points(self.cpoints).ps(self.psize).c(self.pcolor)
1159        self.vpoints.name = "points"
1160        self.vpoints.pickable(True)  # to allow toggle
1161        minnr = 1
1162        if self.splined:
1163            minnr = 2
1164        if self.lwidth and len(self.cpoints) > minnr:
1165            if self.splined:
1166                try:
1167                    self.line = Spline(self.cpoints, closed=self.closed, res=self.resolution)
1168                except ValueError:
1169                    # if clicking too close splining might fail
1170                    self.cpoints.pop()
1171                    return
1172            else:
1173                self.line = Line(self.cpoints, closed=self.closed)
1174            self.line.c(self.lcolor).lw(self.lwidth).pickable(False)
1175            self.add(self.vpoints, self.line)
1176        else:
1177            self.add(self.vpoints)
1178
1179    def _key_press(self, evt):
1180        if evt.keypress == "c":
1181            self.cpoints = []
1182            self.remove(self.line, self.vpoints).render()
1183            if self.verbose:
1184                vedo.colors.printc("==== Cleared all points ====", c="r", invert=True)
1185
1186    def start(self):
1187        """Start the interaction"""
1188        self.show(self.object, self.instructions, mode=self.mode)
1189        return self
1190
1191
1192########################################################################
1193class Animation(Plotter):
1194    """
1195    A `Plotter` derived class that allows to animate simultaneously various objects
1196    by specifying event times and durations of different visual effects.
1197
1198    Arguments:
1199        total_duration : (float)
1200            expand or shrink the total duration of video to this value
1201        time_resolution : (float)
1202            in seconds, save a frame at this rate
1203        show_progressbar : (bool)
1204            whether to show a progress bar or not
1205        video_filename : (str)
1206            output file name of the video
1207        video_fps : (int)
1208            desired value of the nr of frames per second
1209
1210    .. warning:: this is still an experimental feature at the moment.
1211    """
1212
1213    def __init__(
1214        self,
1215        total_duration=None,
1216        time_resolution=0.02,
1217        show_progressbar=True,
1218        video_filename="animation.mp4",
1219        video_fps=12,
1220    ):
1221        Plotter.__init__(self)
1222        self.resetcam = True
1223
1224        self.events = []
1225        self.time_resolution = time_resolution
1226        self.total_duration = total_duration
1227        self.show_progressbar = show_progressbar
1228        self.video_filename = video_filename
1229        self.video_fps = video_fps
1230        self.bookingMode = True
1231        self._inputvalues = []
1232        self._performers = []
1233        self._lastT = None
1234        self._lastDuration = None
1235        self._lastActs = None
1236        self.eps = 0.00001
1237
1238    def _parse(self, objs, t, duration):
1239        if t is None:
1240            if self._lastT:
1241                t = self._lastT
1242            else:
1243                t = 0.0
1244        if duration is None:
1245            if self._lastDuration:
1246                duration = self._lastDuration
1247            else:
1248                duration = 0.0
1249        if objs is None:
1250            if self._lastActs:
1251                objs = self._lastActs
1252            else:
1253                vedo.logger.error("Need to specify actors!")
1254                raise RuntimeError
1255
1256        objs2 = objs
1257
1258        if is_sequence(objs):
1259            objs2 = objs
1260        else:
1261            objs2 = [objs]
1262
1263        # quantize time steps and duration
1264        t = int(t / self.time_resolution + 0.5) * self.time_resolution
1265        nsteps = int(duration / self.time_resolution + 0.5)
1266        duration = nsteps * self.time_resolution
1267
1268        rng = np.linspace(t, t + duration, nsteps + 1)
1269
1270        self._lastT = t
1271        self._lastDuration = duration
1272        self._lastActs = objs2
1273
1274        for a in objs2:
1275            if a not in self.actors:
1276                self.actors.append(a)
1277
1278        return objs2, t, duration, rng
1279
1280    def switch_on(self, acts=None, t=None):
1281        """Switch on the input list of meshes."""
1282        return self.fade_in(acts, t, 0)
1283
1284    def switch_off(self, acts=None, t=None):
1285        """Switch off the input list of meshes."""
1286        return self.fade_out(acts, t, 0)
1287
1288    def fade_in(self, acts=None, t=None, duration=None):
1289        """Gradually switch on the input list of meshes by increasing opacity."""
1290        if self.bookingMode:
1291            acts, t, duration, rng = self._parse(acts, t, duration)
1292            for tt in rng:
1293                alpha = lin_interpolate(tt, [t, t + duration], [0, 1])
1294                self.events.append((tt, self.fade_in, acts, alpha))
1295        else:
1296            for a in self._performers:
1297                if hasattr(a, "alpha"):
1298                    if a.alpha() >= self._inputvalues:
1299                        continue
1300                    a.alpha(self._inputvalues)
1301        return self
1302
1303    def fade_out(self, acts=None, t=None, duration=None):
1304        """Gradually switch off the input list of meshes by increasing transparency."""
1305        if self.bookingMode:
1306            acts, t, duration, rng = self._parse(acts, t, duration)
1307            for tt in rng:
1308                alpha = lin_interpolate(tt, [t, t + duration], [1, 0])
1309                self.events.append((tt, self.fade_out, acts, alpha))
1310        else:
1311            for a in self._performers:
1312                if a.alpha() <= self._inputvalues:
1313                    continue
1314                a.alpha(self._inputvalues)
1315        return self
1316
1317    def change_alpha_between(self, alpha1, alpha2, acts=None, t=None, duration=None):
1318        """Gradually change transparency for the input list of meshes."""
1319        if self.bookingMode:
1320            acts, t, duration, rng = self._parse(acts, t, duration)
1321            for tt in rng:
1322                alpha = lin_interpolate(tt, [t, t + duration], [alpha1, alpha2])
1323                self.events.append((tt, self.fade_out, acts, alpha))
1324        else:
1325            for a in self._performers:
1326                a.alpha(self._inputvalues)
1327        return self
1328
1329    def change_color(self, c, acts=None, t=None, duration=None):
1330        """Gradually change color for the input list of meshes."""
1331        if self.bookingMode:
1332            acts, t, duration, rng = self._parse(acts, t, duration)
1333
1334            col2 = get_color(c)
1335            for tt in rng:
1336                inputvalues = []
1337                for a in acts:
1338                    col1 = a.color()
1339                    r = lin_interpolate(tt, [t, t + duration], [col1[0], col2[0]])
1340                    g = lin_interpolate(tt, [t, t + duration], [col1[1], col2[1]])
1341                    b = lin_interpolate(tt, [t, t + duration], [col1[2], col2[2]])
1342                    inputvalues.append((r, g, b))
1343                self.events.append((tt, self.change_color, acts, inputvalues))
1344        else:
1345            for i, a in enumerate(self._performers):
1346                a.color(self._inputvalues[i])
1347        return self
1348
1349    def change_backcolor(self, c, acts=None, t=None, duration=None):
1350        """Gradually change backface color for the input list of meshes.
1351        An initial backface color should be set in advance."""
1352        if self.bookingMode:
1353            acts, t, duration, rng = self._parse(acts, t, duration)
1354
1355            col2 = get_color(c)
1356            for tt in rng:
1357                inputvalues = []
1358                for a in acts:
1359                    if a.GetBackfaceProperty():
1360                        col1 = a.backColor()
1361                        r = lin_interpolate(tt, [t, t + duration], [col1[0], col2[0]])
1362                        g = lin_interpolate(tt, [t, t + duration], [col1[1], col2[1]])
1363                        b = lin_interpolate(tt, [t, t + duration], [col1[2], col2[2]])
1364                        inputvalues.append((r, g, b))
1365                    else:
1366                        inputvalues.append(None)
1367                self.events.append((tt, self.change_backcolor, acts, inputvalues))
1368        else:
1369            for i, a in enumerate(self._performers):
1370                a.backColor(self._inputvalues[i])
1371        return self
1372
1373    def change_to_wireframe(self, acts=None, t=None):
1374        """Switch representation to wireframe for the input list of meshes at time `t`."""
1375        if self.bookingMode:
1376            acts, t, _, _ = self._parse(acts, t, None)
1377            self.events.append((t, self.change_to_wireframe, acts, True))
1378        else:
1379            for a in self._performers:
1380                a.wireframe(self._inputvalues)
1381        return self
1382
1383    def change_to_surface(self, acts=None, t=None):
1384        """Switch representation to surface for the input list of meshes at time `t`."""
1385        if self.bookingMode:
1386            acts, t, _, _ = self._parse(acts, t, None)
1387            self.events.append((t, self.change_to_surface, acts, False))
1388        else:
1389            for a in self._performers:
1390                a.wireframe(self._inputvalues)
1391        return self
1392
1393    def change_line_width(self, lw, acts=None, t=None, duration=None):
1394        """Gradually change line width of the mesh edges for the input list of meshes."""
1395        if self.bookingMode:
1396            acts, t, duration, rng = self._parse(acts, t, duration)
1397            for tt in rng:
1398                inputvalues = []
1399                for a in acts:
1400                    newlw = lin_interpolate(tt, [t, t + duration], [a.lw(), lw])
1401                    inputvalues.append(newlw)
1402                self.events.append((tt, self.change_line_width, acts, inputvalues))
1403        else:
1404            for i, a in enumerate(self._performers):
1405                a.lw(self._inputvalues[i])
1406        return self
1407
1408    def change_line_color(self, c, acts=None, t=None, duration=None):
1409        """Gradually change line color of the mesh edges for the input list of meshes."""
1410        if self.bookingMode:
1411            acts, t, duration, rng = self._parse(acts, t, duration)
1412            col2 = get_color(c)
1413            for tt in rng:
1414                inputvalues = []
1415                for a in acts:
1416                    col1 = a.linecolor()
1417                    r = lin_interpolate(tt, [t, t + duration], [col1[0], col2[0]])
1418                    g = lin_interpolate(tt, [t, t + duration], [col1[1], col2[1]])
1419                    b = lin_interpolate(tt, [t, t + duration], [col1[2], col2[2]])
1420                    inputvalues.append((r, g, b))
1421                self.events.append((tt, self.change_line_color, acts, inputvalues))
1422        else:
1423            for i, a in enumerate(self._performers):
1424                a.linecolor(self._inputvalues[i])
1425        return self
1426
1427    def change_lighting(self, style, acts=None, t=None, duration=None):
1428        """Gradually change the lighting style for the input list of meshes.
1429
1430        Allowed styles are: [metallic, plastic, shiny, glossy, default].
1431        """
1432        if self.bookingMode:
1433            acts, t, duration, rng = self._parse(acts, t, duration)
1434
1435            c = (1,1,0.99)
1436            if   style=='metallic': pars = [0.1, 0.3, 1.0, 10, c]
1437            elif style=='plastic' : pars = [0.3, 0.4, 0.3,  5, c]
1438            elif style=='shiny'   : pars = [0.2, 0.6, 0.8, 50, c]
1439            elif style=='glossy'  : pars = [0.1, 0.7, 0.9, 90, c]
1440            elif style=='default' : pars = [0.1, 1.0, 0.05, 5, c]
1441            else:
1442                vedo.logger.error(f"Unknown lighting style {style}")
1443
1444            for tt in rng:
1445                inputvalues = []
1446                for a in acts:
1447                    pr = a.GetProperty()
1448                    aa = pr.GetAmbient()
1449                    ad = pr.GetDiffuse()
1450                    asp = pr.GetSpecular()
1451                    aspp = pr.GetSpecularPower()
1452                    naa  = lin_interpolate(tt, [t,t+duration], [aa,  pars[0]])
1453                    nad  = lin_interpolate(tt, [t,t+duration], [ad,  pars[1]])
1454                    nasp = lin_interpolate(tt, [t,t+duration], [asp, pars[2]])
1455                    naspp= lin_interpolate(tt, [t,t+duration], [aspp,pars[3]])
1456                    inputvalues.append((naa, nad, nasp, naspp))
1457                self.events.append((tt, self.change_lighting, acts, inputvalues))
1458        else:
1459            for i, a in enumerate(self._performers):
1460                pr = a.GetProperty()
1461                vals = self._inputvalues[i]
1462                pr.SetAmbient(vals[0])
1463                pr.SetDiffuse(vals[1])
1464                pr.SetSpecular(vals[2])
1465                pr.SetSpecularPower(vals[3])
1466        return self
1467
1468    def move(self, act=None, pt=(0, 0, 0), t=None, duration=None, style="linear"):
1469        """Smoothly change the position of a specific object to a new point in space."""
1470        if self.bookingMode:
1471            acts, t, duration, rng = self._parse(act, t, duration)
1472            if len(acts) != 1:
1473                vedo.logger.error("in move(), can move only one object.")
1474            cpos = acts[0].pos()
1475            pt = np.array(pt)
1476            dv = (pt - cpos) / len(rng)
1477            for j, tt in enumerate(rng):
1478                i = j + 1
1479                if "quad" in style:
1480                    x = i / len(rng)
1481                    y = x * x
1482                    self.events.append((tt, self.move, acts, cpos + dv * i * y))
1483                else:
1484                    self.events.append((tt, self.move, acts, cpos + dv * i))
1485        else:
1486            self._performers[0].pos(self._inputvalues)
1487        return self
1488
1489    def rotate(self, act=None, axis=(1, 0, 0), angle=0, t=None, duration=None):
1490        """Smoothly rotate a specific object by a specified angle and axis."""
1491        if self.bookingMode:
1492            acts, t, duration, rng = self._parse(act, t, duration)
1493            if len(acts) != 1:
1494                vedo.logger.error("in rotate(), can move only one object.")
1495            for tt in rng:
1496                ang = angle / len(rng)
1497                self.events.append((tt, self.rotate, acts, (axis, ang)))
1498        else:
1499            ax = self._inputvalues[0]
1500            if ax == "x":
1501                self._performers[0].rotate_x(self._inputvalues[1])
1502            elif ax == "y":
1503                self._performers[0].rotate_y(self._inputvalues[1])
1504            elif ax == "z":
1505                self._performers[0].rotate_z(self._inputvalues[1])
1506        return self
1507
1508    def scale(self, acts=None, factor=1, t=None, duration=None):
1509        """Smoothly scale a specific object to a specified scale factor."""
1510        if self.bookingMode:
1511            acts, t, duration, rng = self._parse(acts, t, duration)
1512            for tt in rng:
1513                fac = lin_interpolate(tt, [t, t + duration], [1, factor])
1514                self.events.append((tt, self.scale, acts, fac))
1515        else:
1516            for a in self._performers:
1517                a.scale(self._inputvalues)
1518        return self
1519
1520    def mesh_erode(self, act=None, corner=6, t=None, duration=None):
1521        """Erode a mesh by removing cells that are close to one of the 8 corners
1522        of the bounding box.
1523        """
1524        if self.bookingMode:
1525            acts, t, duration, rng = self._parse(act, t, duration)
1526            if len(acts) != 1:
1527                vedo.logger.error("in meshErode(), can erode only one object.")
1528            diag = acts[0].diagonal_size()
1529            x0, x1, y0, y1, z0, z1 = acts[0].GetBounds()
1530            corners = [
1531                (x0, y0, z0),
1532                (x1, y0, z0),
1533                (x1, y1, z0),
1534                (x0, y1, z0),
1535                (x0, y0, z1),
1536                (x1, y0, z1),
1537                (x1, y1, z1),
1538                (x0, y1, z1),
1539            ]
1540            pcl = acts[0].closest_point(corners[corner])
1541            dmin = np.linalg.norm(pcl - corners[corner])
1542            for tt in rng:
1543                d = lin_interpolate(tt, [t, t + duration], [dmin, diag * 1.01])
1544                if d > 0:
1545                    ids = acts[0].closest_point(corners[corner], radius=d, return_point_id=True)
1546                    if len(ids) <= acts[0].npoints:
1547                        self.events.append((tt, self.mesh_erode, acts, ids))
1548        return self
1549
1550    def play(self):
1551        """Play the internal list of events and save a video."""
1552
1553        self.events = sorted(self.events, key=lambda x: x[0])
1554        self.bookingMode = False
1555
1556        if self.show_progressbar:
1557            pb = vedo.ProgressBar(0, len(self.events), c="g")
1558
1559        if self.total_duration is None:
1560            self.total_duration = self.events[-1][0] - self.events[0][0]
1561
1562        if self.video_filename:
1563            vd = vedo.Video(self.video_filename, fps=self.video_fps, duration=self.total_duration)
1564
1565        ttlast = 0
1566        for e in self.events:
1567
1568            tt, action, self._performers, self._inputvalues = e
1569            action(0, 0)
1570
1571            dt = tt - ttlast
1572            if dt > self.eps:
1573                self.show(interactive=False, resetcam=self.resetcam)
1574                if self.video_filename:
1575                    vd.add_frame()
1576
1577                if dt > self.time_resolution + self.eps:
1578                    if self.video_filename:
1579                        vd.pause(dt)
1580
1581            ttlast = tt
1582
1583            if self.show_progressbar:
1584                pb.print("t=" + str(int(tt * 100) / 100) + "s,  " + action.__name__)
1585
1586        self.show(interactive=False, resetcam=self.resetcam)
1587        if self.video_filename:
1588            vd.add_frame()
1589            vd.close()
1590
1591        self.show(interactive=True, resetcam=self.resetcam)
1592        self.bookingMode = True
1593
1594
1595########################################################################
1596class AnimationPlayer(vedo.Plotter):
1597    """
1598    A Plotter with play/pause, step forward/backward and slider functionalties.
1599    Useful for inspecting time series. 
1600
1601    The user has the responsibility to update all actors in the callback function. 
1602    Pay attention to that the idx can both increment and decrement,
1603    as well as make large jumps.
1604    
1605    Arguments:
1606        func :  (Callable)
1607            a function that passes an integer as input and updates the scene
1608        irange : (tuple)
1609            the range of the integer input representing the time series index
1610        dt : (float)
1611            the time interval between two calls to `func` in milliseconds
1612        loop : (bool)
1613            whether to loop the animation
1614        c : (list, str)
1615            the color of the play/pause button
1616        bc : (list)
1617            the background color of the play/pause button and the slider
1618        button_size : (int)
1619            the size of the play/pause buttons
1620        button_pos : (float, float)
1621            the position of the play/pause buttons as a fraction of the window size
1622        button_gap : (float)
1623            the gap between the buttons
1624        slider_length : (float)
1625            the length of the slider as a fraction of the window size
1626        slider_pos : (float, float)
1627            the position of the slider as a fraction of the window size
1628        kwargs: (dict)
1629            keyword arguments to be passed to `Plotter`
1630
1631    Examples:
1632        - [aspring2_player.py](https://github.com/marcomusy/vedo/tree/master/examples/simulations/aspring2_player.py)
1633    """
1634    # Original class contributed by @mikaeltulldahl (Mikael Tulldahl)
1635
1636    PLAY_SYMBOL        = "    \u23F5   "
1637    PAUSE_SYMBOL       = "   \u23F8   "
1638    ONE_BACK_SYMBOL    = " \u29CF"
1639    ONE_FORWARD_SYMBOL = "\u29D0 "
1640
1641    def __init__(
1642        self,
1643        func: Callable[[int],None],
1644        irange: tuple,
1645        dt: float = 1.0,
1646        loop: bool = True,
1647        c=("white", "white"),
1648        bc=("green3","red4"),
1649        button_size=25,
1650        button_pos=(0.5,0.08),
1651        button_gap=0.055,
1652        slider_length=0.5,
1653        slider_pos=(0.5,0.055),
1654        **kwargs,
1655    ):
1656        super().__init__(**kwargs)
1657
1658        min_value, max_value = np.array(irange).astype(int)
1659        button_pos = np.array(button_pos)
1660        slider_pos = np.array(slider_pos)
1661
1662        self._func = func
1663
1664        self.value = min_value-1
1665        self.min_value = min_value
1666        self.max_value = max_value
1667        self.dt = max(dt, 1)
1668        self.is_playing = False
1669        self._loop = loop
1670
1671        self.timer_callback_id = self.add_callback("timer", self._handle_timer)
1672        self.timer_id = None
1673
1674        self.play_pause_button = self.add_button(
1675            self.toggle,
1676            pos=button_pos,  # x,y fraction from bottom left corner
1677            states=[self.PLAY_SYMBOL, self.PAUSE_SYMBOL],
1678            font="Kanopus",
1679            size=button_size,
1680            bc=bc,
1681        )
1682        self.button_oneback = self.add_button(
1683            self.onebackward,
1684            pos=(-button_gap, 0) + button_pos,
1685            states=[self.ONE_BACK_SYMBOL],
1686            font="Kanopus",
1687            size=button_size,
1688            c=c,
1689            bc=bc,
1690        )
1691        self.button_oneforward = self.add_button(
1692            self.oneforward,
1693            pos=(button_gap, 0) + button_pos,
1694            states=[self.ONE_FORWARD_SYMBOL],
1695            font="Kanopus",
1696            size=button_size,
1697            bc=bc,
1698        )
1699        d = (1-slider_length)/2
1700        self.slider: SliderWidget = self.add_slider(
1701            self._slider_callback,
1702            self.min_value,
1703            self.max_value - 1,
1704            value=self.min_value,
1705            pos=[(d-0.5, 0)+slider_pos, (0.5-d, 0)+slider_pos],
1706            show_value=False,
1707            c=bc[0],
1708            alpha=1,
1709        )
1710
1711    def pause(self) -> None:
1712        """Pause the animation."""
1713        self.is_playing = False
1714        if self.timer_id is not None:
1715            self.timer_callback("destroy", self.timer_id)
1716            self.timer_id = None
1717        self.play_pause_button.status(self.PLAY_SYMBOL)
1718
1719    def resume(self) -> None:
1720        """Resume the animation."""
1721        if self.timer_id is not None:
1722            self.timer_callback("destroy", self.timer_id)
1723        self.timer_id = self.timer_callback("create", dt=int(self.dt))
1724        self.is_playing = True
1725        self.play_pause_button.status(self.PAUSE_SYMBOL)
1726
1727    def toggle(self) -> None:
1728        """Toggle between play and pause."""
1729        if not self.is_playing:
1730            self.resume()
1731        else:
1732            self.pause()
1733
1734    def oneforward(self) -> None:
1735        """Advance the animation by one frame."""
1736        self.pause()
1737        self.set_frame(self.value + 1)
1738
1739    def onebackward(self) -> None:
1740        """Go back one frame in the animation."""
1741        self.pause()
1742        self.set_frame(self.value - 1)
1743
1744    def set_frame(self, value: int) -> None:
1745        """Set the current value of the animation."""
1746        if self._loop:
1747            if value < self.min_value:
1748                value = self.max_value - 1
1749            elif value >= self.max_value:
1750                value = self.min_value
1751        else:
1752            if value < self.min_value:
1753                self.pause()
1754                value = self.min_value
1755            elif value >= self.max_value - 1:
1756                value = self.max_value - 1
1757                self.pause()
1758
1759        if self.value != value:
1760            self.value = value
1761            self.slider.value = value
1762            self._func(value)
1763
1764    def _slider_callback(self, widget: SliderWidget, _: str) -> None:
1765        self.pause()
1766        self.set_frame(int(round(widget.value)))
1767
1768    def _handle_timer(self, _: Event = None) -> None:
1769        self.set_frame(self.value + 1)
1770
1771    def stop(self) -> "AnimationPlayer":
1772        """
1773        Stop the animation timers, remove buttons and slider.
1774        Behave like a normal `Plotter` after this.
1775        """
1776        # stop timer
1777        if self.timer_id is not None:
1778            self.timer_callback("destroy", self.timer_id)
1779            self.timer_id = None
1780
1781        # remove callbacks
1782        self.remove_callback(self.timer_callback_id)
1783
1784        # remove buttons
1785        self.slider.off()
1786        self.renderer.RemoveActor(self.play_pause_button.actor)
1787        self.renderer.RemoveActor(self.button_oneback.actor)
1788        self.renderer.RemoveActor(self.button_oneforward.actor)
1789        return self
1790
1791
1792########################################################################
1793class Clock(vedo.Assembly):
1794    """Clock animation."""
1795
1796    def __init__(self, h=None, m=None, s=None, font="Quikhand", title="", c="k"):
1797        """
1798        Create a clock with current time or user provided time.
1799
1800        Arguments:
1801            h : (int)
1802                hours in range [0,23]
1803            m : (int)
1804                minutes in range [0,59]
1805            s : (int)
1806                seconds in range [0,59]
1807            font : (str)
1808                font type
1809            title : (str)
1810                some extra text to show on the clock
1811            c : (str)
1812                color of the numbers
1813
1814        Example:
1815            ```python
1816            import time
1817            from vedo import show
1818            from vedo.applications import Clock
1819            clock = Clock()
1820            plt = show(clock, interactive=False)
1821            for i in range(10):
1822                time.sleep(1)
1823                clock.update()
1824                plt.render()
1825            plt.close()
1826            ```
1827            ![](https://vedo.embl.es/images/feats/clock.png)
1828        """
1829        self.elapsed = 0
1830        self._start = time.time()
1831
1832        wd = ""
1833        if h is None and m is None:
1834            t = time.localtime()
1835            h = t.tm_hour
1836            m = t.tm_min
1837            s = t.tm_sec
1838            if not title:
1839                d = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
1840                wd = f"{d[t.tm_wday]} {t.tm_mday}/{t.tm_mon}/{t.tm_year} "
1841
1842        h = int(h) % 24
1843        m = int(m) % 60
1844        t = (h * 60 + m) / 12 / 60
1845
1846        alpha = 2 * np.pi * t + np.pi / 2
1847        beta = 12 * 2 * np.pi * t + np.pi / 2
1848
1849        x1, y1 = np.cos(alpha), np.sin(alpha)
1850        x2, y2 = np.cos(beta), np.sin(beta)
1851        if s is not None:
1852            s = int(s) % 60
1853            gamma = s * 2 * np.pi / 60 + np.pi / 2
1854            x3, y3 = np.cos(gamma), np.sin(gamma)
1855
1856        ore = Line([0, 0], [x1, y1], lw=14, c="red4").scale(0.5).mirror()
1857        minu = Line([0, 0], [x2, y2], lw=7, c="blue3").scale(0.75).mirror()
1858        secs = None
1859        if s is not None:
1860            secs = Line([0, 0], [x3, y3], lw=1, c="k").scale(0.95).mirror()
1861            secs.z(0.003)
1862        back1 = vedo.shapes.Circle(res=180, c="k5")
1863        back2 = vedo.shapes.Circle(res=12).mirror().scale(0.84).rotate_z(-360 / 12)
1864        labels = back2.labels(range(1, 13), justify="center", font=font, c=c, scale=0.14)
1865        txt = vedo.shapes.Text3D(wd + title, font="VictorMono", justify="top-center", s=0.07, c=c)
1866        txt.pos(0, -0.25, 0.001)
1867        labels.z(0.001)
1868        minu.z(0.002)
1869        vedo.Assembly.__init__(self, [back1, labels, ore, minu, secs, txt])
1870        self.name = "Clock"
1871
1872    def update(self, h=None, m=None, s=None):
1873        """Update clock with current or user time."""
1874        parts = self.unpack()
1875        self.elapsed = time.time() - self._start
1876
1877        if h is None and m is None:
1878            t = time.localtime()
1879            h = t.tm_hour
1880            m = t.tm_min
1881            s = t.tm_sec
1882
1883        h = int(h) % 24
1884        m = int(m) % 60
1885        t = (h * 60 + m) / 12 / 60
1886
1887        alpha = 2 * np.pi * t + np.pi / 2
1888        beta = 12 * 2 * np.pi * t + np.pi / 2
1889
1890        x1, y1 = np.cos(alpha), np.sin(alpha)
1891        x2, y2 = np.cos(beta), np.sin(beta)
1892        if s is not None:
1893            s = int(s) % 60
1894            gamma = s * 2 * np.pi / 60 + np.pi / 2
1895            x3, y3 = np.cos(gamma), np.sin(gamma)
1896
1897        pts2 = parts[2].points()
1898        pts2[1] = [-x1 * 0.5, y1 * 0.5, 0.001]
1899        parts[2].points(pts2)
1900
1901        pts3 = parts[3].points()
1902        pts3[1] = [-x2 * 0.75, y2 * 0.75, 0.002]
1903        parts[3].points(pts3)
1904
1905        if s is not None:
1906            pts4 = parts[4].points()
1907            pts4[1] = [-x3 * 0.95, y3 * 0.95, 0.003]
1908            parts[4].points(pts4)
1909
1910        return self
class Browser(vedo.plotter.Plotter):
734class Browser(Plotter):
735    """
736    Browse a series of vedo objects by using a simple slider.
737    """
738    def __init__(
739        self,
740        objects=(),
741        sliderpos=((0.50, 0.07), (0.95, 0.07)),
742        c=None,  # slider color
743        prefix="",
744        font="Calco", # slider font
745        axes=1,
746        resetcam=False, # resetcam while using the slider
747        **kwargs,
748    ):
749        """
750        Browse a series of vedo objects by using a simple slider.
751
752        Examples:
753            ```python
754            from vedo import load, dataurl
755            from vedo.applications import Browser
756            meshes = load(dataurl+'timecourse1d.npy') # python list of Meshes
757            plt = Browser(meshes, bg='k')             # vedo.Plotter
758            plt.show(interactive=False, zoom='tight') # show the meshes
759            plt.play(dt=50)                           # delay in milliseconds
760            plt.close()
761            ```
762
763        - [morphomatics_tube.py](https://github.com/marcomusy/vedo/tree/master/examples/other/morphomatics_tube.py)
764        """
765        Plotter.__init__(self, axes=axes, **kwargs)
766
767        if isinstance(objects, str):
768            objects = vedo.file_io.load(objects)
769
770        self += objects
771
772        self.slider = None
773        self.timer_callback_id = None
774
775        # define the slider func ##########################
776        def slider_function(widget=None, event=None):
777
778            must_render = False
779            if isinstance(widget, vedo.plotter.Event):
780                if self.slider.value < len(self.actors)-1:
781                    self.slider.value = self.slider.value + 1
782                else:
783                    self.slider.value = 0
784                must_render = True
785
786            k = int(self.slider.value)
787            ak = self.actors[k]
788            for a in self.actors:
789                if a == ak:
790                    a.on()
791                else:
792                    a.off()
793            if resetcam:
794                self.reset_camera()
795            tx = str(k)
796            if ak.filename:
797                tx = ak.filename.split("/")[-1]
798                tx = tx.split("\\")[-1]  # windows os
799            elif ak.name:
800                tx = ak.name
801
802            self.slider.title = prefix + tx
803
804            if must_render:
805                self.render()
806        ##################################################
807
808        self.slider_function = slider_function
809        self.slider = self.add_slider(
810            slider_function,
811            0.5,
812            len(objects) - 0.5,
813            pos=sliderpos,
814            font=font,
815            c=c,
816            show_value=False,
817        )
818        self.slider.GetRepresentation().SetTitleHeight(0.020)
819        slider_function(self.slider)  # init call
820    
821    def play(self, dt=100):
822        """Start playing the slides at a given speed."""
823        self.timer_callback_id = self.add_callback("timer", self.slider_function)
824        self.timer_callback("start", dt=dt)
825        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, prefix='', font='Calco', axes=1, resetcam=False, **kwargs)
738    def __init__(
739        self,
740        objects=(),
741        sliderpos=((0.50, 0.07), (0.95, 0.07)),
742        c=None,  # slider color
743        prefix="",
744        font="Calco", # slider font
745        axes=1,
746        resetcam=False, # resetcam while using the slider
747        **kwargs,
748    ):
749        """
750        Browse a series of vedo objects by using a simple slider.
751
752        Examples:
753            ```python
754            from vedo import load, dataurl
755            from vedo.applications import Browser
756            meshes = load(dataurl+'timecourse1d.npy') # python list of Meshes
757            plt = Browser(meshes, bg='k')             # vedo.Plotter
758            plt.show(interactive=False, zoom='tight') # show the meshes
759            plt.play(dt=50)                           # delay in milliseconds
760            plt.close()
761            ```
762
763        - [morphomatics_tube.py](https://github.com/marcomusy/vedo/tree/master/examples/other/morphomatics_tube.py)
764        """
765        Plotter.__init__(self, axes=axes, **kwargs)
766
767        if isinstance(objects, str):
768            objects = vedo.file_io.load(objects)
769
770        self += objects
771
772        self.slider = None
773        self.timer_callback_id = None
774
775        # define the slider func ##########################
776        def slider_function(widget=None, event=None):
777
778            must_render = False
779            if isinstance(widget, vedo.plotter.Event):
780                if self.slider.value < len(self.actors)-1:
781                    self.slider.value = self.slider.value + 1
782                else:
783                    self.slider.value = 0
784                must_render = True
785
786            k = int(self.slider.value)
787            ak = self.actors[k]
788            for a in self.actors:
789                if a == ak:
790                    a.on()
791                else:
792                    a.off()
793            if resetcam:
794                self.reset_camera()
795            tx = str(k)
796            if ak.filename:
797                tx = ak.filename.split("/")[-1]
798                tx = tx.split("\\")[-1]  # windows os
799            elif ak.name:
800                tx = ak.name
801
802            self.slider.title = prefix + tx
803
804            if must_render:
805                self.render()
806        ##################################################
807
808        self.slider_function = slider_function
809        self.slider = self.add_slider(
810            slider_function,
811            0.5,
812            len(objects) - 0.5,
813            pos=sliderpos,
814            font=font,
815            c=c,
816            show_value=False,
817        )
818        self.slider.GetRepresentation().SetTitleHeight(0.020)
819        slider_function(self.slider)  # init call

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

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):
821    def play(self, dt=100):
822        """Start playing the slides at a given speed."""
823        self.timer_callback_id = self.add_callback("timer", self.slider_function)
824        self.timer_callback("start", dt=dt)
825        self.interactive()

Start playing the slides at a given speed.

class IsosurfaceBrowser(vedo.plotter.Plotter):
553class IsosurfaceBrowser(Plotter):
554    """
555    Generate a Volume isosurfacing controlled by a slider.
556    """
557
558    def __init__(
559        self,
560        volume,
561        isovalue=None,
562        c=None,
563        alpha=1,
564        lego=False,
565        res=50,
566        use_gpu=False,
567        precompute=False,
568        progress=False,
569        cmap="hot",
570        delayed=False,
571        sliderpos=4,
572        pos=(0, 0),
573        size="auto",
574        screensize="auto",
575        title="",
576        bg="white",
577        bg2=None,
578        axes=1,
579        interactive=True,
580    ):
581        """
582        Generate a `vedo.Plotter` for Volume isosurfacing using a slider.
583
584        Set `delayed=True` to delay slider update on mouse release.
585
586        Set `res` to set the resolution, e.g. the number of desired isosurfaces to be
587        generated on the fly.
588
589        Set `precompute=True` to precompute the isosurfaces (so slider browsing will be smoother).
590
591        Examples:
592            - [app_isobrowser.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/app_isobrowser.py)
593
594                ![](https://vedo.embl.es/images/advanced/app_isobrowser.gif)
595        """
596
597        Plotter.__init__(
598            self,
599            pos=pos,
600            bg=bg,
601            bg2=bg2,
602            size=size,
603            screensize=screensize,
604            title=title,
605            interactive=interactive,
606            axes=axes,
607        )
608
609        ### GPU ################################
610        if use_gpu and hasattr(volume.GetProperty(), "GetIsoSurfaceValues"):
611
612            scrange = volume.scalar_range()
613            delta = scrange[1] - scrange[0]
614            if not delta:
615                return
616
617            if isovalue is None:
618                isovalue = delta / 3.0 + scrange[0]
619
620            ### isovalue slider callback
621            def slider_isovalue(widget, event):
622                value = widget.GetRepresentation().GetValue()
623                isovals.SetValue(0, value)
624
625            isovals = volume.GetProperty().GetIsoSurfaceValues()
626            isovals.SetValue(0, isovalue)
627            self.renderer.AddActor(volume.mode(5).alpha(alpha).c(c))
628
629            self.add_slider(
630                slider_isovalue,
631                scrange[0] + 0.02 * delta,
632                scrange[1] - 0.02 * delta,
633                value=isovalue,
634                pos=sliderpos,
635                title="scalar value",
636                show_value=True,
637                delayed=delayed,
638            )
639
640        ### CPU ################################
641        else:
642
643            self._prev_value = 1e30
644
645            scrange = volume.scalar_range()
646            delta = scrange[1] - scrange[0]
647            if not delta:
648                return
649
650            if lego:
651                res = int(res / 2)  # because lego is much slower
652                slidertitle = ""
653            else:
654                slidertitle = "scalar value"
655
656            allowed_vals = np.linspace(scrange[0], scrange[1], num=res)
657
658            bacts = {}  # cache the meshes so we dont need to recompute
659            if precompute:
660                delayed = False  # no need to delay the slider in this case
661                if progress:
662                    pb = vedo.ProgressBar(0, len(allowed_vals), delay=1)
663
664                for value in allowed_vals:
665                    value_name = precision(value, 2)
666                    if lego:
667                        mesh = volume.legosurface(vmin=value)
668                        if mesh.ncells:
669                            mesh.cmap(cmap, vmin=scrange[0], vmax=scrange[1], on="cells")
670                    else:
671                        mesh = volume.isosurface(value).color(c).alpha(alpha)
672                    bacts.update({value_name: mesh})  # store it
673                    if progress:
674                        pb.print("isosurfacing volume..")
675
676            ### isovalue slider callback
677            def slider_isovalue(widget, event):
678
679                prevact = self.actors[0]
680                if isinstance(widget, float):
681                    value = widget
682                else:
683                    value = widget.GetRepresentation().GetValue()
684
685                # snap to the closest
686                idx = (np.abs(allowed_vals - value)).argmin()
687                value = allowed_vals[idx]
688
689                if abs(value - self._prev_value) / delta < 0.001:
690                    return
691                self._prev_value = value
692
693                value_name = precision(value, 2)
694                if value_name in bacts:  # reusing the already existing mesh
695                    # print('reusing')
696                    mesh = bacts[value_name]
697                else:  # else generate it
698                    # print('generating', value)
699                    if lego:
700                        mesh = volume.legosurface(vmin=value)
701                        if mesh.ncells:
702                            mesh.cmap(cmap, vmin=scrange[0], vmax=scrange[1], on="cells")
703                    else:
704                        mesh = volume.isosurface(value).color(c).alpha(alpha)
705                    bacts.update({value_name: mesh})  # store it
706
707                self.renderer.RemoveActor(prevact)
708                self.renderer.AddActor(mesh)
709                self.actors[0] = mesh
710
711            ################################################
712
713            if isovalue is None:
714                isovalue = delta / 3.0 + scrange[0]
715
716            self.actors = [None]
717            slider_isovalue(isovalue, "")  # init call
718            if lego:
719                self.actors[0].add_scalarbar(pos=(0.8, 0.12))
720
721            self.add_slider(
722                slider_isovalue,
723                scrange[0] + 0.02 * delta,
724                scrange[1] - 0.02 * delta,
725                value=isovalue,
726                pos=sliderpos,
727                title=slidertitle,
728                show_value=True,
729                delayed=delayed,
730            )

Generate a Volume isosurfacing controlled by a slider.

IsosurfaceBrowser( volume, isovalue=None, c=None, alpha=1, lego=False, res=50, use_gpu=False, precompute=False, progress=False, cmap='hot', delayed=False, sliderpos=4, pos=(0, 0), size='auto', screensize='auto', title='', bg='white', bg2=None, axes=1, interactive=True)
558    def __init__(
559        self,
560        volume,
561        isovalue=None,
562        c=None,
563        alpha=1,
564        lego=False,
565        res=50,
566        use_gpu=False,
567        precompute=False,
568        progress=False,
569        cmap="hot",
570        delayed=False,
571        sliderpos=4,
572        pos=(0, 0),
573        size="auto",
574        screensize="auto",
575        title="",
576        bg="white",
577        bg2=None,
578        axes=1,
579        interactive=True,
580    ):
581        """
582        Generate a `vedo.Plotter` for Volume isosurfacing using a slider.
583
584        Set `delayed=True` to delay slider update on mouse release.
585
586        Set `res` to set the resolution, e.g. the number of desired isosurfaces to be
587        generated on the fly.
588
589        Set `precompute=True` to precompute the isosurfaces (so slider browsing will be smoother).
590
591        Examples:
592            - [app_isobrowser.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/app_isobrowser.py)
593
594                ![](https://vedo.embl.es/images/advanced/app_isobrowser.gif)
595        """
596
597        Plotter.__init__(
598            self,
599            pos=pos,
600            bg=bg,
601            bg2=bg2,
602            size=size,
603            screensize=screensize,
604            title=title,
605            interactive=interactive,
606            axes=axes,
607        )
608
609        ### GPU ################################
610        if use_gpu and hasattr(volume.GetProperty(), "GetIsoSurfaceValues"):
611
612            scrange = volume.scalar_range()
613            delta = scrange[1] - scrange[0]
614            if not delta:
615                return
616
617            if isovalue is None:
618                isovalue = delta / 3.0 + scrange[0]
619
620            ### isovalue slider callback
621            def slider_isovalue(widget, event):
622                value = widget.GetRepresentation().GetValue()
623                isovals.SetValue(0, value)
624
625            isovals = volume.GetProperty().GetIsoSurfaceValues()
626            isovals.SetValue(0, isovalue)
627            self.renderer.AddActor(volume.mode(5).alpha(alpha).c(c))
628
629            self.add_slider(
630                slider_isovalue,
631                scrange[0] + 0.02 * delta,
632                scrange[1] - 0.02 * delta,
633                value=isovalue,
634                pos=sliderpos,
635                title="scalar value",
636                show_value=True,
637                delayed=delayed,
638            )
639
640        ### CPU ################################
641        else:
642
643            self._prev_value = 1e30
644
645            scrange = volume.scalar_range()
646            delta = scrange[1] - scrange[0]
647            if not delta:
648                return
649
650            if lego:
651                res = int(res / 2)  # because lego is much slower
652                slidertitle = ""
653            else:
654                slidertitle = "scalar value"
655
656            allowed_vals = np.linspace(scrange[0], scrange[1], num=res)
657
658            bacts = {}  # cache the meshes so we dont need to recompute
659            if precompute:
660                delayed = False  # no need to delay the slider in this case
661                if progress:
662                    pb = vedo.ProgressBar(0, len(allowed_vals), delay=1)
663
664                for value in allowed_vals:
665                    value_name = precision(value, 2)
666                    if lego:
667                        mesh = volume.legosurface(vmin=value)
668                        if mesh.ncells:
669                            mesh.cmap(cmap, vmin=scrange[0], vmax=scrange[1], on="cells")
670                    else:
671                        mesh = volume.isosurface(value).color(c).alpha(alpha)
672                    bacts.update({value_name: mesh})  # store it
673                    if progress:
674                        pb.print("isosurfacing volume..")
675
676            ### isovalue slider callback
677            def slider_isovalue(widget, event):
678
679                prevact = self.actors[0]
680                if isinstance(widget, float):
681                    value = widget
682                else:
683                    value = widget.GetRepresentation().GetValue()
684
685                # snap to the closest
686                idx = (np.abs(allowed_vals - value)).argmin()
687                value = allowed_vals[idx]
688
689                if abs(value - self._prev_value) / delta < 0.001:
690                    return
691                self._prev_value = value
692
693                value_name = precision(value, 2)
694                if value_name in bacts:  # reusing the already existing mesh
695                    # print('reusing')
696                    mesh = bacts[value_name]
697                else:  # else generate it
698                    # print('generating', value)
699                    if lego:
700                        mesh = volume.legosurface(vmin=value)
701                        if mesh.ncells:
702                            mesh.cmap(cmap, vmin=scrange[0], vmax=scrange[1], on="cells")
703                    else:
704                        mesh = volume.isosurface(value).color(c).alpha(alpha)
705                    bacts.update({value_name: mesh})  # store it
706
707                self.renderer.RemoveActor(prevact)
708                self.renderer.AddActor(mesh)
709                self.actors[0] = mesh
710
711            ################################################
712
713            if isovalue is None:
714                isovalue = delta / 3.0 + scrange[0]
715
716            self.actors = [None]
717            slider_isovalue(isovalue, "")  # init call
718            if lego:
719                self.actors[0].add_scalarbar(pos=(0.8, 0.12))
720
721            self.add_slider(
722                slider_isovalue,
723                scrange[0] + 0.02 * delta,
724                scrange[1] - 0.02 * delta,
725                value=isovalue,
726                pos=sliderpos,
727                title=slidertitle,
728                show_value=True,
729                delayed=delayed,
730            )

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

Set delayed=True to delay slider update on mouse release.

Set res to set the resolution, e.g. the number of desired isosurfaces to be generated on the fly.

Set precompute=True to precompute the isosurfaces (so slider browsing will be smoother).

Examples:
class FreeHandCutPlotter(vedo.plotter.Plotter):
 829class FreeHandCutPlotter(Plotter):
 830    """A tool to edit meshes interactively."""
 831
 832    # thanks to Jakub Kaminski for the original version of this script
 833    def __init__(
 834        self,
 835        mesh,
 836        splined=True,
 837        font="Bongas",
 838        alpha=0.9,
 839        lw=4,
 840        lc="red5",
 841        pc="red4",
 842        c="green3",
 843        tc="k9",
 844        tol=0.008,
 845        **options,
 846    ):
 847        """
 848        A `vedo.Plotter` derived class which edits polygonal meshes interactively.
 849
 850        Can also be invoked from command line with:
 851
 852        ```bash
 853        vedo --edit https://vedo.embl.es/examples/data/porsche.ply
 854        ```
 855
 856        Usage:
 857            - Left-click and hold to rotate
 858            - Right-click and move to draw line
 859            - Second right-click to stop drawing
 860            - Press "c" to clear points
 861            -       "z/Z" to cut mesh (Z inverts inside-out the selection area)
 862            -       "L" to keep only the largest connected surface
 863            -       "s" to save mesh to file (tag `_edited` is appended to filename)
 864            -       "u" to undo last action
 865            -       "h" for help, "i" for info
 866
 867        Arguments:
 868            mesh : (Mesh, Points)
 869                The input Mesh or pointcloud.
 870            splined : (bool)
 871                join points with a spline or a simple line.
 872            font : (str)
 873                Font name for the instructions.
 874            alpha : (float)
 875                transparency of the instruction message panel.
 876            lw : (str)
 877                selection line width.
 878            lc : (str)
 879                selection line color.
 880            pc : (str)
 881                selection points color.
 882            c : (str)
 883                background color of instructions.
 884            tc : (str)
 885                text color of instructions.
 886            tol : (int)
 887                tolerance of the point proximity.
 888
 889        Examples:
 890            - [cut_freehand.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/cut_freehand.py)
 891
 892                ![](https://vedo.embl.es/images/basic/cutFreeHand.gif)
 893        """
 894
 895        if not isinstance(mesh, Points):
 896            vedo.logger.error("FreeHandCutPlotter input must be Points or Mesh")
 897            raise RuntimeError()
 898
 899        super().__init__(**options)
 900
 901        self.mesh = mesh
 902        self.mesh_prev = mesh
 903        self.splined = splined
 904        self.linecolor = lc
 905        self.linewidth = lw
 906        self.pointcolor = pc
 907        self.color = c
 908        self.alpha = alpha
 909
 910        self.msg = "Right-click and move to draw line\n"
 911        self.msg += "Second right-click to stop drawing\n"
 912        self.msg += "Press L to extract largest surface\n"
 913        self.msg += "        z/Z to cut mesh (s to save)\n"
 914        self.msg += "        c to clear points, u to undo"
 915        self.txt2d = Text2D(self.msg, pos="top-left", font=font, s=0.9)
 916        self.txt2d.c(tc).background(c, alpha).frame()
 917
 918        self.idkeypress = self.add_callback("KeyPress", self._on_keypress)
 919        self.idrightclck = self.add_callback("RightButton", self._on_right_click)
 920        self.idmousemove = self.add_callback("MouseMove", self._on_mouse_move)
 921        self.drawmode = False
 922        self.tol = tol  # tolerance of point distance
 923        self.cpoints = []
 924        self.points = None
 925        self.spline = None
 926        self.jline = None
 927        self.topline = None
 928        self.top_pts = []
 929
 930    def init(self, init_points):
 931        """Set an initial number of points to define a region"""
 932        if isinstance(init_points, Points):
 933            self.cpoints = init_points.points()
 934        else:
 935            self.cpoints = np.array(init_points)
 936        self.points = Points(self.cpoints, r=self.linewidth).c(self.pointcolor).pickable(0)
 937        if self.splined:
 938            self.spline = Spline(self.cpoints, res=len(self.cpoints) * 4)
 939        else:
 940            self.spline = Line(self.cpoints)
 941        self.spline.lw(self.linewidth).c(self.linecolor).pickable(False)
 942        self.jline = Line(self.cpoints[0], self.cpoints[-1], lw=1, c=self.linecolor).pickable(0)
 943        self.add([self.points, self.spline, self.jline]).render()
 944        return self
 945
 946    def _on_right_click(self, evt):
 947        self.drawmode = not self.drawmode  # toggle mode
 948        if self.drawmode:
 949            self.txt2d.background(self.linecolor, self.alpha)
 950        else:
 951            self.txt2d.background(self.color, self.alpha)
 952            if len(self.cpoints) > 2:
 953                self.remove([self.spline, self.jline])
 954                if self.splined:  # show the spline closed
 955                    self.spline = Spline(self.cpoints, closed=True, res=len(self.cpoints) * 4)
 956                else:
 957                    self.spline = Line(self.cpoints, closed=True)
 958                self.spline.lw(self.linewidth).c(self.linecolor).pickable(False)
 959                self.add(self.spline)
 960        self.render()
 961
 962    def _on_mouse_move(self, evt):
 963        if self.drawmode:
 964            cpt = self.compute_world_coordinate(evt.picked2d)  # make this 2d-screen point 3d
 965            if self.cpoints and mag(cpt - self.cpoints[-1]) < self.mesh.diagonal_size() * self.tol:
 966                return  # new point is too close to the last one. skip
 967            self.cpoints.append(cpt)
 968            if len(self.cpoints) > 2:
 969                self.remove([self.points, self.spline, self.jline, self.topline])
 970                self.points = Points(self.cpoints, r=self.linewidth).c(self.pointcolor).pickable(0)
 971                if self.splined:
 972                    self.spline = Spline(self.cpoints, res=len(self.cpoints) * 4)  # not closed here
 973                else:
 974                    self.spline = Line(self.cpoints)
 975
 976                if evt.actor:
 977                    self.top_pts.append(evt.picked3d)
 978                    self.topline = Points(self.top_pts, r=self.linewidth)
 979                    self.topline.c(self.linecolor).pickable(False)
 980
 981                self.spline.lw(self.linewidth).c(self.linecolor).pickable(False)
 982                self.txt2d.background(self.linecolor)
 983                self.jline = Line(self.cpoints[0], self.cpoints[-1], lw=1, c=self.linecolor).pickable(0)
 984                self.add([self.points, self.spline, self.jline, self.topline]).render()
 985
 986    def _on_keypress(self, evt):
 987        if evt.keypress.lower() == "z" and self.spline:  # Cut mesh with a ribbon-like surface
 988            inv = False
 989            if evt.keypress == "Z":
 990                inv = True
 991            self.txt2d.background("red8").text("  ... working ...  ")
 992            self.render()
 993            self.mesh_prev = self.mesh.clone()
 994            tol = self.mesh.diagonal_size() / 2  # size of ribbon (not shown)
 995            pts = self.spline.points()
 996            n = fit_plane(pts, signed=True).normal  # compute normal vector to points
 997            rb = Ribbon(pts - tol * n, pts + tol * n, closed=True)
 998            self.mesh.cut_with_mesh(rb, invert=inv)  # CUT
 999            self.txt2d.text(self.msg)  # put back original message
1000            if self.drawmode:
1001                self._on_right_click(evt)  # toggle mode to normal
1002            else:
1003                self.txt2d.background(self.color, self.alpha)
1004            self.remove([self.spline, self.points, self.jline, self.topline]).render()
1005            self.cpoints, self.points, self.spline = [], None, None
1006            self.top_pts, self.topline = [], None
1007
1008        elif evt.keypress == "L":
1009            self.txt2d.background("red8")
1010            self.txt2d.text(" ... removing smaller ... \n ... parts of the mesh ... ")
1011            self.render()
1012            self.remove(self.mesh)
1013            self.mesh_prev = self.mesh
1014            mcut = self.mesh.extract_largest_region()
1015            mcut.filename = self.mesh.filename  # copy over various properties
1016            mcut.name = self.mesh.name
1017            mcut.scalarbar = self.mesh.scalarbar
1018            mcut.info = self.mesh.info
1019            self.mesh = mcut                            # discard old mesh by overwriting it
1020            self.txt2d.text(self.msg).background(self.color)   # put back original message
1021            self.add(mcut).render()
1022
1023        elif evt.keypress == 'u':                       # Undo last action
1024            if self.drawmode:
1025                self._on_right_click(evt)               # toggle mode to normal
1026            else:
1027                self.txt2d.background(self.color, self.alpha)
1028            self.remove([self.mesh, self.spline, self.jline, self.points, self.topline])
1029            self.mesh = self.mesh_prev
1030            self.cpoints, self.points, self.spline = [], None, None
1031            self.top_pts, self.topline = [], None
1032            self.add(self.mesh).render()
1033
1034        elif evt.keypress in ("c", "Delete"):
1035            # clear all points
1036            self.remove([self.spline, self.points, self.jline, self.topline]).render()
1037            self.cpoints, self.points, self.spline = [], None, None
1038            self.top_pts, self.topline = [], None
1039
1040        elif evt.keypress == "r":  # reset camera and axes
1041            try:
1042                self.remove(self.axes_instances[0])
1043                self.axes_instances[0] = None
1044                self.add_global_axes(axtype=1, c=None, bounds=self.mesh.bounds())
1045                self.renderer.ResetCamera()
1046                self.render()
1047            except:
1048                pass
1049
1050        elif evt.keypress == "s":
1051            if self.mesh.filename:
1052                fname = os.path.basename(self.mesh.filename)
1053                fname, extension = os.path.splitext(fname)
1054                fname = fname.replace("_edited", "")
1055                fname = f"{fname}_edited{extension}"
1056            else:
1057                fname = "mesh_edited.vtk"
1058            self.write(fname)
1059
1060    def write(self, filename="mesh_edited.vtk"):
1061        """Save the resulting mesh to file"""
1062        self.mesh.write(filename)
1063        vedo.logger.info(f"mesh saved to file {filename}")
1064        return self
1065
1066    def start(self, *args, **kwargs):
1067        """Start window interaction (with mouse and keyboard)"""
1068        acts = [self.txt2d, self.mesh, self.points, self.spline, self.jline]
1069        self.show(acts + list(args), **kwargs)
1070        return self

A tool to edit meshes interactively.

FreeHandCutPlotter( mesh, splined=True, font='Bongas', alpha=0.9, lw=4, lc='red5', pc='red4', c='green3', tc='k9', tol=0.008, **options)
833    def __init__(
834        self,
835        mesh,
836        splined=True,
837        font="Bongas",
838        alpha=0.9,
839        lw=4,
840        lc="red5",
841        pc="red4",
842        c="green3",
843        tc="k9",
844        tol=0.008,
845        **options,
846    ):
847        """
848        A `vedo.Plotter` derived class which edits polygonal meshes interactively.
849
850        Can also be invoked from command line with:
851
852        ```bash
853        vedo --edit https://vedo.embl.es/examples/data/porsche.ply
854        ```
855
856        Usage:
857            - Left-click and hold to rotate
858            - Right-click and move to draw line
859            - Second right-click to stop drawing
860            - Press "c" to clear points
861            -       "z/Z" to cut mesh (Z inverts inside-out the selection area)
862            -       "L" to keep only the largest connected surface
863            -       "s" to save mesh to file (tag `_edited` is appended to filename)
864            -       "u" to undo last action
865            -       "h" for help, "i" for info
866
867        Arguments:
868            mesh : (Mesh, Points)
869                The input Mesh or pointcloud.
870            splined : (bool)
871                join points with a spline or a simple line.
872            font : (str)
873                Font name for the instructions.
874            alpha : (float)
875                transparency of the instruction message panel.
876            lw : (str)
877                selection line width.
878            lc : (str)
879                selection line color.
880            pc : (str)
881                selection points color.
882            c : (str)
883                background color of instructions.
884            tc : (str)
885                text color of instructions.
886            tol : (int)
887                tolerance of the point proximity.
888
889        Examples:
890            - [cut_freehand.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/cut_freehand.py)
891
892                ![](https://vedo.embl.es/images/basic/cutFreeHand.gif)
893        """
894
895        if not isinstance(mesh, Points):
896            vedo.logger.error("FreeHandCutPlotter input must be Points or Mesh")
897            raise RuntimeError()
898
899        super().__init__(**options)
900
901        self.mesh = mesh
902        self.mesh_prev = mesh
903        self.splined = splined
904        self.linecolor = lc
905        self.linewidth = lw
906        self.pointcolor = pc
907        self.color = c
908        self.alpha = alpha
909
910        self.msg = "Right-click and move to draw line\n"
911        self.msg += "Second right-click to stop drawing\n"
912        self.msg += "Press L to extract largest surface\n"
913        self.msg += "        z/Z to cut mesh (s to save)\n"
914        self.msg += "        c to clear points, u to undo"
915        self.txt2d = Text2D(self.msg, pos="top-left", font=font, s=0.9)
916        self.txt2d.c(tc).background(c, alpha).frame()
917
918        self.idkeypress = self.add_callback("KeyPress", self._on_keypress)
919        self.idrightclck = self.add_callback("RightButton", self._on_right_click)
920        self.idmousemove = self.add_callback("MouseMove", self._on_mouse_move)
921        self.drawmode = False
922        self.tol = tol  # tolerance of point distance
923        self.cpoints = []
924        self.points = None
925        self.spline = None
926        self.jline = None
927        self.topline = None
928        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.
Examples:
def init(self, init_points):
930    def init(self, init_points):
931        """Set an initial number of points to define a region"""
932        if isinstance(init_points, Points):
933            self.cpoints = init_points.points()
934        else:
935            self.cpoints = np.array(init_points)
936        self.points = Points(self.cpoints, r=self.linewidth).c(self.pointcolor).pickable(0)
937        if self.splined:
938            self.spline = Spline(self.cpoints, res=len(self.cpoints) * 4)
939        else:
940            self.spline = Line(self.cpoints)
941        self.spline.lw(self.linewidth).c(self.linecolor).pickable(False)
942        self.jline = Line(self.cpoints[0], self.cpoints[-1], lw=1, c=self.linecolor).pickable(0)
943        self.add([self.points, self.spline, self.jline]).render()
944        return self

Set an initial number of points to define a region

def write(self, filename='mesh_edited.vtk'):
1060    def write(self, filename="mesh_edited.vtk"):
1061        """Save the resulting mesh to file"""
1062        self.mesh.write(filename)
1063        vedo.logger.info(f"mesh saved to file {filename}")
1064        return self

Save the resulting mesh to file

def start(self, *args, **kwargs):
1066    def start(self, *args, **kwargs):
1067        """Start window interaction (with mouse and keyboard)"""
1068        acts = [self.txt2d, self.mesh, self.points, self.spline, self.jline]
1069        self.show(acts + list(args), **kwargs)
1070        return self

Start window interaction (with mouse and keyboard)

class RayCastPlotter(vedo.plotter.Plotter):
378class RayCastPlotter(Plotter):
379    """
380    Generate Volume rendering using ray casting.
381    """
382
383    def __init__(self, volume, **kwargs):
384        """
385        Generate a window for Volume rendering using ray casting.
386
387        Returns:
388            `vedo.Plotter` object.
389
390        Examples:
391            - [app_raycaster.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/app_raycaster.py)
392
393            ![](https://vedo.embl.es/images/advanced/app_raycaster.gif)
394        """
395
396        Plotter.__init__(self, **kwargs)
397
398        self.alphaslider0 = 0.33
399        self.alphaslider1 = 0.66
400        self.alphaslider2 = 1
401
402        self.property = volume.GetProperty()
403        img = volume.imagedata()
404
405        if volume.dimensions()[2] < 3:
406            vedo.logger.error("RayCastPlotter: not enough z slices.")
407            raise RuntimeError
408
409        smin, smax = img.GetScalarRange()
410        x0alpha = smin + (smax - smin) * 0.25
411        x1alpha = smin + (smax - smin) * 0.5
412        x2alpha = smin + (smax - smin) * 1.0
413
414        ############################## color map slider
415        # Create transfer mapping scalar value to color
416        cmaps = [
417            "jet",
418            "viridis",
419            "bone",
420            "hot",
421            "plasma",
422            "winter",
423            "cool",
424            "gist_earth",
425            "coolwarm",
426            "tab10",
427        ]
428        cols_cmaps = []
429        for cm in cmaps:
430            cols = color_map(range(0, 21), cm, 0, 20)  # sample 20 colors
431            cols_cmaps.append(cols)
432        Ncols = len(cmaps)
433        csl = (0.9, 0.9, 0.9)
434        if sum(get_color(self.renderer.GetBackground())) > 1.5:
435            csl = (0.1, 0.1, 0.1)
436
437        def sliderColorMap(widget, event):
438            sliderRep = widget.GetRepresentation()
439            k = int(sliderRep.GetValue())
440            sliderRep.SetTitleText(cmaps[k])
441            volume.color(cmaps[k])
442
443        w1 = self.add_slider(
444            sliderColorMap,
445            0,
446            Ncols - 1,
447            value=0,
448            show_value=0,
449            title=cmaps[0],
450            c=csl,
451            pos=[(0.8, 0.05), (0.965, 0.05)],
452        )
453        w1.GetRepresentation().SetTitleHeight(0.018)
454
455        ############################## alpha sliders
456        # Create transfer mapping scalar value to opacity
457        opacityTransferFunction = self.property.GetScalarOpacity()
458
459        def setOTF():
460            opacityTransferFunction.RemoveAllPoints()
461            opacityTransferFunction.AddPoint(smin, 0.0)
462            opacityTransferFunction.AddPoint(smin + (smax - smin) * 0.1, 0.0)
463            opacityTransferFunction.AddPoint(x0alpha, self.alphaslider0)
464            opacityTransferFunction.AddPoint(x1alpha, self.alphaslider1)
465            opacityTransferFunction.AddPoint(x2alpha, self.alphaslider2)
466
467        setOTF()
468
469        def sliderA0(widget, event):
470            self.alphaslider0 = widget.GetRepresentation().GetValue()
471            setOTF()
472
473        self.add_slider(
474            sliderA0,
475            0,
476            1,
477            value=self.alphaslider0,
478            pos=[(0.84, 0.1), (0.84, 0.26)],
479            c=csl,
480            show_value=0,
481        )
482
483        def sliderA1(widget, event):
484            self.alphaslider1 = widget.GetRepresentation().GetValue()
485            setOTF()
486
487        self.add_slider(
488            sliderA1,
489            0,
490            1,
491            value=self.alphaslider1,
492            pos=[(0.89, 0.1), (0.89, 0.26)],
493            c=csl,
494            show_value=0,
495        )
496
497        def sliderA2(widget, event):
498            self.alphaslider2 = widget.GetRepresentation().GetValue()
499            setOTF()
500
501        w2 = self.add_slider(
502            sliderA2,
503            0,
504            1,
505            value=self.alphaslider2,
506            pos=[(0.96, 0.1), (0.96, 0.26)],
507            c=csl,
508            show_value=0,
509            title="Opacity levels",
510        )
511        w2.GetRepresentation().SetTitleHeight(0.016)
512
513        # add a button
514        def button_func_mode():
515            s = volume.mode()
516            snew = (s + 1) % 2
517            volume.mode(snew)
518            bum.switch()
519
520        bum = self.add_button(
521            button_func_mode,
522            pos=(0.7, 0.035),
523            states=["composite", "max proj."],
524            c=["bb", "gray"],
525            bc=["gray", "bb"],  # colors of states
526            font="",
527            size=16,
528            bold=0,
529            italic=False,
530        )
531        bum.status(volume.mode())
532
533        # add histogram of scalar
534        plot = CornerHistogram(
535            volume,
536            bins=25,
537            logscale=1,
538            c=(0.7, 0.7, 0.7),
539            bg=(0.7, 0.7, 0.7),
540            pos=(0.78, 0.065),
541            lines=True,
542            dots=False,
543            nmax=3.1415e06,  # subsample otherwise is too slow
544        )
545
546        plot.GetPosition2Coordinate().SetValue(0.197, 0.20, 0)
547        plot.GetXAxisActor2D().SetFontFactor(0.7)
548        plot.GetProperty().SetOpacity(0.5)
549        self.add([plot, volume])

Generate Volume rendering using ray casting.

RayCastPlotter(volume, **kwargs)
383    def __init__(self, volume, **kwargs):
384        """
385        Generate a window for Volume rendering using ray casting.
386
387        Returns:
388            `vedo.Plotter` object.
389
390        Examples:
391            - [app_raycaster.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/app_raycaster.py)
392
393            ![](https://vedo.embl.es/images/advanced/app_raycaster.gif)
394        """
395
396        Plotter.__init__(self, **kwargs)
397
398        self.alphaslider0 = 0.33
399        self.alphaslider1 = 0.66
400        self.alphaslider2 = 1
401
402        self.property = volume.GetProperty()
403        img = volume.imagedata()
404
405        if volume.dimensions()[2] < 3:
406            vedo.logger.error("RayCastPlotter: not enough z slices.")
407            raise RuntimeError
408
409        smin, smax = img.GetScalarRange()
410        x0alpha = smin + (smax - smin) * 0.25
411        x1alpha = smin + (smax - smin) * 0.5
412        x2alpha = smin + (smax - smin) * 1.0
413
414        ############################## color map slider
415        # Create transfer mapping scalar value to color
416        cmaps = [
417            "jet",
418            "viridis",
419            "bone",
420            "hot",
421            "plasma",
422            "winter",
423            "cool",
424            "gist_earth",
425            "coolwarm",
426            "tab10",
427        ]
428        cols_cmaps = []
429        for cm in cmaps:
430            cols = color_map(range(0, 21), cm, 0, 20)  # sample 20 colors
431            cols_cmaps.append(cols)
432        Ncols = len(cmaps)
433        csl = (0.9, 0.9, 0.9)
434        if sum(get_color(self.renderer.GetBackground())) > 1.5:
435            csl = (0.1, 0.1, 0.1)
436
437        def sliderColorMap(widget, event):
438            sliderRep = widget.GetRepresentation()
439            k = int(sliderRep.GetValue())
440            sliderRep.SetTitleText(cmaps[k])
441            volume.color(cmaps[k])
442
443        w1 = self.add_slider(
444            sliderColorMap,
445            0,
446            Ncols - 1,
447            value=0,
448            show_value=0,
449            title=cmaps[0],
450            c=csl,
451            pos=[(0.8, 0.05), (0.965, 0.05)],
452        )
453        w1.GetRepresentation().SetTitleHeight(0.018)
454
455        ############################## alpha sliders
456        # Create transfer mapping scalar value to opacity
457        opacityTransferFunction = self.property.GetScalarOpacity()
458
459        def setOTF():
460            opacityTransferFunction.RemoveAllPoints()
461            opacityTransferFunction.AddPoint(smin, 0.0)
462            opacityTransferFunction.AddPoint(smin + (smax - smin) * 0.1, 0.0)
463            opacityTransferFunction.AddPoint(x0alpha, self.alphaslider0)
464            opacityTransferFunction.AddPoint(x1alpha, self.alphaslider1)
465            opacityTransferFunction.AddPoint(x2alpha, self.alphaslider2)
466
467        setOTF()
468
469        def sliderA0(widget, event):
470            self.alphaslider0 = widget.GetRepresentation().GetValue()
471            setOTF()
472
473        self.add_slider(
474            sliderA0,
475            0,
476            1,
477            value=self.alphaslider0,
478            pos=[(0.84, 0.1), (0.84, 0.26)],
479            c=csl,
480            show_value=0,
481        )
482
483        def sliderA1(widget, event):
484            self.alphaslider1 = widget.GetRepresentation().GetValue()
485            setOTF()
486
487        self.add_slider(
488            sliderA1,
489            0,
490            1,
491            value=self.alphaslider1,
492            pos=[(0.89, 0.1), (0.89, 0.26)],
493            c=csl,
494            show_value=0,
495        )
496
497        def sliderA2(widget, event):
498            self.alphaslider2 = widget.GetRepresentation().GetValue()
499            setOTF()
500
501        w2 = self.add_slider(
502            sliderA2,
503            0,
504            1,
505            value=self.alphaslider2,
506            pos=[(0.96, 0.1), (0.96, 0.26)],
507            c=csl,
508            show_value=0,
509            title="Opacity levels",
510        )
511        w2.GetRepresentation().SetTitleHeight(0.016)
512
513        # add a button
514        def button_func_mode():
515            s = volume.mode()
516            snew = (s + 1) % 2
517            volume.mode(snew)
518            bum.switch()
519
520        bum = self.add_button(
521            button_func_mode,
522            pos=(0.7, 0.035),
523            states=["composite", "max proj."],
524            c=["bb", "gray"],
525            bc=["gray", "bb"],  # colors of states
526            font="",
527            size=16,
528            bold=0,
529            italic=False,
530        )
531        bum.status(volume.mode())
532
533        # add histogram of scalar
534        plot = CornerHistogram(
535            volume,
536            bins=25,
537            logscale=1,
538            c=(0.7, 0.7, 0.7),
539            bg=(0.7, 0.7, 0.7),
540            pos=(0.78, 0.065),
541            lines=True,
542            dots=False,
543            nmax=3.1415e06,  # subsample otherwise is too slow
544        )
545
546        plot.GetPosition2Coordinate().SetValue(0.197, 0.20, 0)
547        plot.GetXAxisActor2D().SetFontFactor(0.7)
548        plot.GetProperty().SetOpacity(0.5)
549        self.add([plot, volume])

Generate a window for Volume rendering using ray casting.

Returns:

vedo.Plotter object.

Examples:

class Slicer3DPlotter(vedo.plotter.Plotter):
 42class Slicer3DPlotter(Plotter):
 43    """
 44    Generate a rendering window with slicing planes for the input Volume.
 45    """
 46
 47    def __init__(
 48        self,
 49        volume,
 50        alpha=1,
 51        cmaps=("gist_ncar_r", "hot_r", "bone_r", "jet", "Spectral_r"),
 52        map2cells=False,  # buggy
 53        clamp=True,
 54        use_slider3d=False,
 55        show_histo=True,
 56        show_icon=True,
 57        draggable=False,
 58        pos=(0, 0),
 59        size="auto",
 60        screensize="auto",
 61        title="",
 62        bg="white",
 63        bg2="lightblue",
 64        axes=7,
 65        resetcam=True,
 66        interactive=True,
 67    ):
 68        """
 69        Generate a rendering window with slicing planes for the input Volume.
 70
 71        Arguments:
 72            alpha : (float)
 73                transparency of the slicing planes
 74            cmaps : (list)
 75                list of color maps names to cycle when clicking button
 76            map2cells : (bool)
 77                scalars are mapped to cells, not interpolated
 78            clamp : (bool)
 79                clamp scalar to reduce the effect of tails in color mapping
 80            use_slider3d : (bool)
 81                show sliders attached along the axes
 82            show_histo : (bool)
 83                show histogram on bottom left
 84            show_icon : (bool)
 85                show a small 3D rendering icon of the volume
 86            draggable : (bool)
 87                make the icon draggable
 88
 89        Examples:
 90            - [slicer1.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/slicer1.py)
 91
 92            <img src="https://vedo.embl.es/images/volumetric/slicer1.jpg" width="500">
 93        """
 94        self._cmap_slicer = "gist_ncar_r"
 95
 96        if not title:
 97            if volume.filename:
 98                title = volume.filename
 99            else:
100                title = "Volume Slicer"
101
102        ################################
103        Plotter.__init__(
104            self,
105            pos=pos,
106            bg=bg,
107            bg2=bg2,
108            size=size,
109            screensize=screensize,
110            title=title,
111            interactive=interactive,
112            axes=axes,
113        )
114        ################################
115        box = volume.box().wireframe().alpha(0.1)
116
117        self.show(box, viewup="z", resetcam=resetcam, interactive=False)
118        if show_icon:
119            self.add_inset(volume, pos=(0.85, 0.85), size=0.15, c="w", draggable=draggable)
120
121        # inits
122        la, ld = 0.7, 0.3  # ambient, diffuse
123        dims = volume.dimensions()
124        data = volume.pointdata[0]
125        rmin, rmax = volume.imagedata().GetScalarRange()
126        if clamp:
127            hdata, edg = np.histogram(data, bins=50)
128            logdata = np.log(hdata + 1)
129            # mean  of the logscale plot
130            meanlog = np.sum(np.multiply(edg[:-1], logdata)) / np.sum(logdata)
131            rmax = min(rmax, meanlog + (meanlog - rmin) * 0.9)
132            rmin = max(rmin, meanlog - (rmax - meanlog) * 0.9)
133            vedo.logger.debug(
134                "scalar range clamped to range: ("
135                + precision(rmin, 3)
136                + ", "
137                + precision(rmax, 3)
138                + ")"
139            )
140        self._cmap_slicer = cmaps[0]
141        visibles = [None, None, None]
142        msh = volume.zslice(int(dims[2] / 2))
143        msh.alpha(alpha).lighting("", la, ld, 0)
144        msh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
145        if map2cells:
146            msh.mapPointsToCells()
147        self.renderer.AddActor(msh)
148        visibles[2] = msh
149        msh.add_scalarbar(pos=(0.04, 0.0), horizontal=True, font_size=0)
150
151        def slider_function_x(widget, event):
152            i = int(widget.GetRepresentation().GetValue())
153            msh = volume.xslice(i).alpha(alpha).lighting("", la, ld, 0)
154            msh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
155            if map2cells:
156                msh.mapPointsToCells()
157            self.renderer.RemoveActor(visibles[0])
158            if i and i < dims[0]:
159                self.renderer.AddActor(msh)
160            visibles[0] = msh
161
162        def slider_function_y(widget, event):
163            i = int(widget.GetRepresentation().GetValue())
164            msh = volume.yslice(i).alpha(alpha).lighting("", la, ld, 0)
165            msh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
166            if map2cells:
167                msh.mapPointsToCells()
168            self.renderer.RemoveActor(visibles[1])
169            if i and i < dims[1]:
170                self.renderer.AddActor(msh)
171            visibles[1] = msh
172
173        def slider_function_z(widget, event):
174            i = int(widget.GetRepresentation().GetValue())
175            msh = volume.zslice(i).alpha(alpha).lighting("", la, ld, 0)
176            msh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
177            if map2cells:
178                msh.mapPointsToCells()
179            self.renderer.RemoveActor(visibles[2])
180            if i and i < dims[2]:
181                self.renderer.AddActor(msh)
182            visibles[2] = msh
183
184        cx, cy, cz, ch = "dr", "dg", "db", (0.3, 0.3, 0.3)
185        if np.sum(self.renderer.GetBackground()) < 1.5:
186            cx, cy, cz = "lr", "lg", "lb"
187            ch = (0.8, 0.8, 0.8)
188
189        if not use_slider3d:
190            self.add_slider(
191                slider_function_x,
192                0,
193                dims[0],
194                title="X",
195                title_size=0.5,
196                pos=[(0.8, 0.12), (0.95, 0.12)],
197                show_value=False,
198                c=cx,
199            )
200            self.add_slider(
201                slider_function_y,
202                0,
203                dims[1],
204                title="Y",
205                title_size=0.5,
206                pos=[(0.8, 0.08), (0.95, 0.08)],
207                show_value=False,
208                c=cy,
209            )
210            self.add_slider(
211                slider_function_z,
212                0,
213                dims[2],
214                title="Z",
215                title_size=0.6,
216                value=int(dims[2] / 2),
217                pos=[(0.8, 0.04), (0.95, 0.04)],
218                show_value=False,
219                c=cz,
220            )
221        else:  # 3d sliders attached to the axes bounds
222            bs = box.bounds()
223            self.add_slider3d(
224                slider_function_x,
225                pos1=(bs[0], bs[2], bs[4]),
226                pos2=(bs[1], bs[2], bs[4]),
227                xmin=0,
228                xmax=dims[0],
229                t=box.diagonal_size() / mag(box.xbounds()) * 0.6,
230                c=cx,
231                show_value=False,
232            )
233            self.add_slider3d(
234                slider_function_y,
235                pos1=(bs[1], bs[2], bs[4]),
236                pos2=(bs[1], bs[3], bs[4]),
237                xmin=0,
238                xmax=dims[1],
239                t=box.diagonal_size() / mag(box.ybounds()) * 0.6,
240                c=cy,
241                show_value=False,
242            )
243            self.add_slider3d(
244                slider_function_z,
245                pos1=(bs[0], bs[2], bs[4]),
246                pos2=(bs[0], bs[2], bs[5]),
247                xmin=0,
248                xmax=dims[2],
249                value=int(dims[2] / 2),
250                t=box.diagonal_size() / mag(box.zbounds()) * 0.6,
251                c=cz,
252                show_value=False,
253            )
254
255        #################
256        def buttonfunc():
257            bu.switch()
258            self._cmap_slicer = bu.status()
259            for mesh in visibles:
260                if mesh:
261                    mesh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
262                    if map2cells:
263                        mesh.mapPointsToCells()
264            self.renderer.RemoveActor(mesh.scalarbar)
265            mesh.add_scalarbar(pos=(0.04, 0.0), horizontal=True)
266            self.renderer.AddActor(mesh.scalarbar)
267
268        bu = self.add_button(
269            buttonfunc,
270            pos=(0.27, 0.005),
271            states=cmaps,
272            c=["db"] * len(cmaps),
273            bc=["lb"] * len(cmaps),  # colors of states
274            size=14,
275            bold=True,
276        )
277
278        #################
279        hist = None
280        if show_histo:
281            hist = CornerHistogram(
282                data, s=0.2, bins=25, logscale=1, pos=(0.02, 0.02), c=ch, bg=ch, alpha=0.7
283            )
284
285        self.add([msh, hist])
286        if interactive:
287            self.interactive()

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

Slicer3DPlotter( volume, alpha=1, cmaps=('gist_ncar_r', 'hot_r', 'bone_r', 'jet', 'Spectral_r'), map2cells=False, clamp=True, use_slider3d=False, show_histo=True, show_icon=True, draggable=False, pos=(0, 0), size='auto', screensize='auto', title='', bg='white', bg2='lightblue', axes=7, resetcam=True, interactive=True)
 47    def __init__(
 48        self,
 49        volume,
 50        alpha=1,
 51        cmaps=("gist_ncar_r", "hot_r", "bone_r", "jet", "Spectral_r"),
 52        map2cells=False,  # buggy
 53        clamp=True,
 54        use_slider3d=False,
 55        show_histo=True,
 56        show_icon=True,
 57        draggable=False,
 58        pos=(0, 0),
 59        size="auto",
 60        screensize="auto",
 61        title="",
 62        bg="white",
 63        bg2="lightblue",
 64        axes=7,
 65        resetcam=True,
 66        interactive=True,
 67    ):
 68        """
 69        Generate a rendering window with slicing planes for the input Volume.
 70
 71        Arguments:
 72            alpha : (float)
 73                transparency of the slicing planes
 74            cmaps : (list)
 75                list of color maps names to cycle when clicking button
 76            map2cells : (bool)
 77                scalars are mapped to cells, not interpolated
 78            clamp : (bool)
 79                clamp scalar to reduce the effect of tails in color mapping
 80            use_slider3d : (bool)
 81                show sliders attached along the axes
 82            show_histo : (bool)
 83                show histogram on bottom left
 84            show_icon : (bool)
 85                show a small 3D rendering icon of the volume
 86            draggable : (bool)
 87                make the icon draggable
 88
 89        Examples:
 90            - [slicer1.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/slicer1.py)
 91
 92            <img src="https://vedo.embl.es/images/volumetric/slicer1.jpg" width="500">
 93        """
 94        self._cmap_slicer = "gist_ncar_r"
 95
 96        if not title:
 97            if volume.filename:
 98                title = volume.filename
 99            else:
100                title = "Volume Slicer"
101
102        ################################
103        Plotter.__init__(
104            self,
105            pos=pos,
106            bg=bg,
107            bg2=bg2,
108            size=size,
109            screensize=screensize,
110            title=title,
111            interactive=interactive,
112            axes=axes,
113        )
114        ################################
115        box = volume.box().wireframe().alpha(0.1)
116
117        self.show(box, viewup="z", resetcam=resetcam, interactive=False)
118        if show_icon:
119            self.add_inset(volume, pos=(0.85, 0.85), size=0.15, c="w", draggable=draggable)
120
121        # inits
122        la, ld = 0.7, 0.3  # ambient, diffuse
123        dims = volume.dimensions()
124        data = volume.pointdata[0]
125        rmin, rmax = volume.imagedata().GetScalarRange()
126        if clamp:
127            hdata, edg = np.histogram(data, bins=50)
128            logdata = np.log(hdata + 1)
129            # mean  of the logscale plot
130            meanlog = np.sum(np.multiply(edg[:-1], logdata)) / np.sum(logdata)
131            rmax = min(rmax, meanlog + (meanlog - rmin) * 0.9)
132            rmin = max(rmin, meanlog - (rmax - meanlog) * 0.9)
133            vedo.logger.debug(
134                "scalar range clamped to range: ("
135                + precision(rmin, 3)
136                + ", "
137                + precision(rmax, 3)
138                + ")"
139            )
140        self._cmap_slicer = cmaps[0]
141        visibles = [None, None, None]
142        msh = volume.zslice(int(dims[2] / 2))
143        msh.alpha(alpha).lighting("", la, ld, 0)
144        msh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
145        if map2cells:
146            msh.mapPointsToCells()
147        self.renderer.AddActor(msh)
148        visibles[2] = msh
149        msh.add_scalarbar(pos=(0.04, 0.0), horizontal=True, font_size=0)
150
151        def slider_function_x(widget, event):
152            i = int(widget.GetRepresentation().GetValue())
153            msh = volume.xslice(i).alpha(alpha).lighting("", la, ld, 0)
154            msh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
155            if map2cells:
156                msh.mapPointsToCells()
157            self.renderer.RemoveActor(visibles[0])
158            if i and i < dims[0]:
159                self.renderer.AddActor(msh)
160            visibles[0] = msh
161
162        def slider_function_y(widget, event):
163            i = int(widget.GetRepresentation().GetValue())
164            msh = volume.yslice(i).alpha(alpha).lighting("", la, ld, 0)
165            msh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
166            if map2cells:
167                msh.mapPointsToCells()
168            self.renderer.RemoveActor(visibles[1])
169            if i and i < dims[1]:
170                self.renderer.AddActor(msh)
171            visibles[1] = msh
172
173        def slider_function_z(widget, event):
174            i = int(widget.GetRepresentation().GetValue())
175            msh = volume.zslice(i).alpha(alpha).lighting("", la, ld, 0)
176            msh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
177            if map2cells:
178                msh.mapPointsToCells()
179            self.renderer.RemoveActor(visibles[2])
180            if i and i < dims[2]:
181                self.renderer.AddActor(msh)
182            visibles[2] = msh
183
184        cx, cy, cz, ch = "dr", "dg", "db", (0.3, 0.3, 0.3)
185        if np.sum(self.renderer.GetBackground()) < 1.5:
186            cx, cy, cz = "lr", "lg", "lb"
187            ch = (0.8, 0.8, 0.8)
188
189        if not use_slider3d:
190            self.add_slider(
191                slider_function_x,
192                0,
193                dims[0],
194                title="X",
195                title_size=0.5,
196                pos=[(0.8, 0.12), (0.95, 0.12)],
197                show_value=False,
198                c=cx,
199            )
200            self.add_slider(
201                slider_function_y,
202                0,
203                dims[1],
204                title="Y",
205                title_size=0.5,
206                pos=[(0.8, 0.08), (0.95, 0.08)],
207                show_value=False,
208                c=cy,
209            )
210            self.add_slider(
211                slider_function_z,
212                0,
213                dims[2],
214                title="Z",
215                title_size=0.6,
216                value=int(dims[2] / 2),
217                pos=[(0.8, 0.04), (0.95, 0.04)],
218                show_value=False,
219                c=cz,
220            )
221        else:  # 3d sliders attached to the axes bounds
222            bs = box.bounds()
223            self.add_slider3d(
224                slider_function_x,
225                pos1=(bs[0], bs[2], bs[4]),
226                pos2=(bs[1], bs[2], bs[4]),
227                xmin=0,
228                xmax=dims[0],
229                t=box.diagonal_size() / mag(box.xbounds()) * 0.6,
230                c=cx,
231                show_value=False,
232            )
233            self.add_slider3d(
234                slider_function_y,
235                pos1=(bs[1], bs[2], bs[4]),
236                pos2=(bs[1], bs[3], bs[4]),
237                xmin=0,
238                xmax=dims[1],
239                t=box.diagonal_size() / mag(box.ybounds()) * 0.6,
240                c=cy,
241                show_value=False,
242            )
243            self.add_slider3d(
244                slider_function_z,
245                pos1=(bs[0], bs[2], bs[4]),
246                pos2=(bs[0], bs[2], bs[5]),
247                xmin=0,
248                xmax=dims[2],
249                value=int(dims[2] / 2),
250                t=box.diagonal_size() / mag(box.zbounds()) * 0.6,
251                c=cz,
252                show_value=False,
253            )
254
255        #################
256        def buttonfunc():
257            bu.switch()
258            self._cmap_slicer = bu.status()
259            for mesh in visibles:
260                if mesh:
261                    mesh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
262                    if map2cells:
263                        mesh.mapPointsToCells()
264            self.renderer.RemoveActor(mesh.scalarbar)
265            mesh.add_scalarbar(pos=(0.04, 0.0), horizontal=True)
266            self.renderer.AddActor(mesh.scalarbar)
267
268        bu = self.add_button(
269            buttonfunc,
270            pos=(0.27, 0.005),
271            states=cmaps,
272            c=["db"] * len(cmaps),
273            bc=["lb"] * len(cmaps),  # colors of states
274            size=14,
275            bold=True,
276        )
277
278        #################
279        hist = None
280        if show_histo:
281            hist = CornerHistogram(
282                data, s=0.2, bins=25, logscale=1, pos=(0.02, 0.02), c=ch, bg=ch, alpha=0.7
283            )
284
285        self.add([msh, hist])
286        if interactive:
287            self.interactive()

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

Arguments:
  • alpha : (float) transparency of the slicing planes
  • cmaps : (list) list of color maps names to cycle when clicking button
  • map2cells : (bool) scalars are mapped to cells, not interpolated
  • clamp : (bool) clamp scalar 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 icon draggable
Examples:

class Slicer2DPlotter(vedo.plotter.Plotter):
291class Slicer2DPlotter(Plotter):
292    """
293    A single slice of a Volume which always faces the camera,
294    but at the same time can be oriented arbitrarily in space.
295    """
296
297    def __init__(self, volume, levels=(None, None), histo_color="red5", **kwargs):
298        """
299        A single slice of a Volume which always faces the camera,
300        but at the same time can be oriented arbitrarily in space.
301
302        Arguments:
303            levels : (list)
304                window and color levels
305            histo_color : (color)
306                histogram color, use `None` to disable it
307
308        <img src="https://vedo.embl.es/images/volumetric/read_volume3.jpg" width="500">
309        """
310        if "shape" not in kwargs:
311            custom_shape = [  # define here the 2 rendering rectangle spaces
312                dict(bottomleft=(0.0, 0.0), topright=(1, 1), bg="k9"),  # the full window
313                dict(bottomleft=(0.8, 0.8), topright=(1, 1), bg="k8", bg2="lb"),
314            ]
315            kwargs["shape"] = custom_shape
316
317        Plotter.__init__(self, **kwargs)
318
319        # reuse the same underlying data as in vol
320        vsl = vedo.volume.VolumeSlice(volume)
321
322        # no argument will grab the existing cmap in vol (or use build_lut())
323        vsl.colorize()
324
325        if levels[0] and levels[1]:
326            vsl.lighting(window=levels[0], level=levels[1])
327
328        usage = Text2D(
329            (
330                "Left click & drag  :rightarrow modify luminosity and contrast\n"
331                "SHIFT+Left click   :rightarrow slice image obliquely\n"
332                "SHIFT+Middle click :rightarrow slice image perpendicularly\n"
333                "R                  :rightarrow Reset the Window/Color levels\n"
334                "X                  :rightarrow Reset to sagittal view\n"
335                "Y                  :rightarrow Reset to coronal view\n"
336                "Z                  :rightarrow Reset to axial view"
337            ),
338            font="Calco",
339            pos="top-left",
340            s=0.8,
341            bg="yellow",
342            alpha=0.25,
343        )
344
345        hist = None
346        if histo_color is not None:
347            # hist = CornerHistogram(
348            #     volume.pointdata[0],
349            #     bins=25,
350            #     logscale=1,
351            #     pos=(0.02, 0.02),
352            #     s=0.175,
353            #     c="dg",
354            #     bg="k",
355            #     alpha=1,
356            # )
357            hist = vedo.pyplot.histogram(
358                volume.pointdata[0],
359                bins=10,
360                logscale=True,
361                c=histo_color,
362                ytitle="log_10 (counts)",
363                axes=dict(text_scale=1.9),
364            )
365            hist = hist.as2d(pos="bottom-left", scale=0.5)
366
367        axes = kwargs.pop("axes", 7)
368        interactive = kwargs.pop("interactive", True)
369        if axes == 7:
370            ax = vedo.addons.RulerAxes(vsl, xtitle="x - ", ytitle="y - ", ztitle="z - ")
371
372        box = vsl.box().alpha(0.2)
373        self.at(0).show(vsl, box, ax, usage, hist, mode="image")
374        self.at(1).show(volume, interactive=interactive)

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

Slicer2DPlotter(volume, levels=(None, None), histo_color='red5', **kwargs)
297    def __init__(self, volume, levels=(None, None), histo_color="red5", **kwargs):
298        """
299        A single slice of a Volume which always faces the camera,
300        but at the same time can be oriented arbitrarily in space.
301
302        Arguments:
303            levels : (list)
304                window and color levels
305            histo_color : (color)
306                histogram color, use `None` to disable it
307
308        <img src="https://vedo.embl.es/images/volumetric/read_volume3.jpg" width="500">
309        """
310        if "shape" not in kwargs:
311            custom_shape = [  # define here the 2 rendering rectangle spaces
312                dict(bottomleft=(0.0, 0.0), topright=(1, 1), bg="k9"),  # the full window
313                dict(bottomleft=(0.8, 0.8), topright=(1, 1), bg="k8", bg2="lb"),
314            ]
315            kwargs["shape"] = custom_shape
316
317        Plotter.__init__(self, **kwargs)
318
319        # reuse the same underlying data as in vol
320        vsl = vedo.volume.VolumeSlice(volume)
321
322        # no argument will grab the existing cmap in vol (or use build_lut())
323        vsl.colorize()
324
325        if levels[0] and levels[1]:
326            vsl.lighting(window=levels[0], level=levels[1])
327
328        usage = Text2D(
329            (
330                "Left click & drag  :rightarrow modify luminosity and contrast\n"
331                "SHIFT+Left click   :rightarrow slice image obliquely\n"
332                "SHIFT+Middle click :rightarrow slice image perpendicularly\n"
333                "R                  :rightarrow Reset the Window/Color levels\n"
334                "X                  :rightarrow Reset to sagittal view\n"
335                "Y                  :rightarrow Reset to coronal view\n"
336                "Z                  :rightarrow Reset to axial view"
337            ),
338            font="Calco",
339            pos="top-left",
340            s=0.8,
341            bg="yellow",
342            alpha=0.25,
343        )
344
345        hist = None
346        if histo_color is not None:
347            # hist = CornerHistogram(
348            #     volume.pointdata[0],
349            #     bins=25,
350            #     logscale=1,
351            #     pos=(0.02, 0.02),
352            #     s=0.175,
353            #     c="dg",
354            #     bg="k",
355            #     alpha=1,
356            # )
357            hist = vedo.pyplot.histogram(
358                volume.pointdata[0],
359                bins=10,
360                logscale=True,
361                c=histo_color,
362                ytitle="log_10 (counts)",
363                axes=dict(text_scale=1.9),
364            )
365            hist = hist.as2d(pos="bottom-left", scale=0.5)
366
367        axes = kwargs.pop("axes", 7)
368        interactive = kwargs.pop("interactive", True)
369        if axes == 7:
370            ax = vedo.addons.RulerAxes(vsl, xtitle="x - ", ytitle="y - ", ztitle="z - ")
371
372        box = vsl.box().alpha(0.2)
373        self.at(0).show(vsl, box, ax, usage, hist, mode="image")
374        self.at(1).show(volume, interactive=interactive)

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

Arguments:
  • levels : (list) window and color levels
  • histo_color : (color) histogram color, use None to disable it

class SplinePlotter(vedo.plotter.Plotter):
1074class SplinePlotter(Plotter):
1075    """
1076    Interactive drawing of splined curves on meshes.
1077    """
1078
1079    def __init__(self, obj, init_points=(), closed=False, splined=True, **kwargs):
1080        """
1081        Create an interactive application that allows the user to click points and
1082        retrieve the coordinates of such points and optionally a spline or line
1083        (open or closed).
1084
1085        Input object can be a image file name or a 3D mesh.
1086        """
1087        super().__init__(**kwargs)
1088
1089        self.mode = "trackball"
1090        self.verbose = True
1091        self.splined = splined
1092        self.resolution = None  # spline resolution (None = automatic)
1093        self.closed = closed
1094        self.lcolor = "yellow4"
1095        self.lwidth = 3
1096        self.pcolor = "purple5"
1097        self.psize = 10
1098
1099        self.cpoints = list(init_points)
1100        self.vpoints = None
1101        self.line = None
1102
1103        if isinstance(obj, str):
1104            self.object = vedo.file_io.load(obj)
1105        else:
1106            self.object = obj
1107
1108        if isinstance(self.object, vedo.Picture):
1109            self.mode = "image"
1110            self.parallel_projection(True)
1111
1112        t = (
1113            "Click to add a point\n"
1114            "Right-click to remove it\n"
1115            "Drag mouse to change contrast\n"
1116            "Press c to clear points\n"
1117            "Press q to continue"
1118        )
1119        self.instructions = Text2D(t, pos="bottom-left", c="white", bg="green", font="Calco")
1120
1121        self += [self.object, self.instructions]
1122
1123        self.callid1 = self.add_callback("KeyPress", self._key_press)
1124        self.callid2 = self.add_callback("LeftButtonPress", self._on_left_click)
1125        self.callid3 = self.add_callback("RightButtonPress", self._on_right_click)
1126
1127    def points(self, newpts=None):
1128        """Retrieve the 3D coordinates of the clicked points"""
1129        if newpts is not None:
1130            self.cpoints = newpts
1131            self._update()
1132            return self
1133        return np.array(self.cpoints)
1134
1135    def _on_left_click(self, evt):
1136        if not evt.actor:
1137            return
1138        if evt.actor.name == "points":
1139            # remove clicked point if clicked twice
1140            pid = self.vpoints.closest_point(evt.picked3d, return_point_id=True)
1141            self.cpoints.pop(pid)
1142            self._update()
1143            return
1144        p = evt.picked3d
1145        self.cpoints.append(p)
1146        self._update()
1147        if self.verbose:
1148            vedo.colors.printc("Added point:", precision(p, 4), c="g")
1149
1150    def _on_right_click(self, evt):
1151        if evt.actor and len(self.cpoints) > 0:
1152            self.cpoints.pop()  # pop removes from the list the last pt
1153            self._update()
1154            if self.verbose:
1155                vedo.colors.printc("Deleted last point", c="r")
1156
1157    def _update(self):
1158        self.remove(self.line, self.vpoints)  # remove old points and spline
1159        self.vpoints = Points(self.cpoints).ps(self.psize).c(self.pcolor)
1160        self.vpoints.name = "points"
1161        self.vpoints.pickable(True)  # to allow toggle
1162        minnr = 1
1163        if self.splined:
1164            minnr = 2
1165        if self.lwidth and len(self.cpoints) > minnr:
1166            if self.splined:
1167                try:
1168                    self.line = Spline(self.cpoints, closed=self.closed, res=self.resolution)
1169                except ValueError:
1170                    # if clicking too close splining might fail
1171                    self.cpoints.pop()
1172                    return
1173            else:
1174                self.line = Line(self.cpoints, closed=self.closed)
1175            self.line.c(self.lcolor).lw(self.lwidth).pickable(False)
1176            self.add(self.vpoints, self.line)
1177        else:
1178            self.add(self.vpoints)
1179
1180    def _key_press(self, evt):
1181        if evt.keypress == "c":
1182            self.cpoints = []
1183            self.remove(self.line, self.vpoints).render()
1184            if self.verbose:
1185                vedo.colors.printc("==== Cleared all points ====", c="r", invert=True)
1186
1187    def start(self):
1188        """Start the interaction"""
1189        self.show(self.object, self.instructions, mode=self.mode)
1190        return self

Interactive drawing of splined curves on meshes.

SplinePlotter(obj, init_points=(), closed=False, splined=True, **kwargs)
1079    def __init__(self, obj, init_points=(), closed=False, splined=True, **kwargs):
1080        """
1081        Create an interactive application that allows the user to click points and
1082        retrieve the coordinates of such points and optionally a spline or line
1083        (open or closed).
1084
1085        Input object can be a image file name or a 3D mesh.
1086        """
1087        super().__init__(**kwargs)
1088
1089        self.mode = "trackball"
1090        self.verbose = True
1091        self.splined = splined
1092        self.resolution = None  # spline resolution (None = automatic)
1093        self.closed = closed
1094        self.lcolor = "yellow4"
1095        self.lwidth = 3
1096        self.pcolor = "purple5"
1097        self.psize = 10
1098
1099        self.cpoints = list(init_points)
1100        self.vpoints = None
1101        self.line = None
1102
1103        if isinstance(obj, str):
1104            self.object = vedo.file_io.load(obj)
1105        else:
1106            self.object = obj
1107
1108        if isinstance(self.object, vedo.Picture):
1109            self.mode = "image"
1110            self.parallel_projection(True)
1111
1112        t = (
1113            "Click to add a point\n"
1114            "Right-click to remove it\n"
1115            "Drag mouse to change contrast\n"
1116            "Press c to clear points\n"
1117            "Press q to continue"
1118        )
1119        self.instructions = Text2D(t, pos="bottom-left", c="white", bg="green", font="Calco")
1120
1121        self += [self.object, self.instructions]
1122
1123        self.callid1 = self.add_callback("KeyPress", self._key_press)
1124        self.callid2 = self.add_callback("LeftButtonPress", self._on_left_click)
1125        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.

def points(self, newpts=None):
1127    def points(self, newpts=None):
1128        """Retrieve the 3D coordinates of the clicked points"""
1129        if newpts is not None:
1130            self.cpoints = newpts
1131            self._update()
1132            return self
1133        return np.array(self.cpoints)

Retrieve the 3D coordinates of the clicked points

def start(self):
1187    def start(self):
1188        """Start the interaction"""
1189        self.show(self.object, self.instructions, mode=self.mode)
1190        return self

Start the interaction

class AnimationPlayer(vedo.plotter.Plotter):
1597class AnimationPlayer(vedo.Plotter):
1598    """
1599    A Plotter with play/pause, step forward/backward and slider functionalties.
1600    Useful for inspecting time series. 
1601
1602    The user has the responsibility to update all actors in the callback function. 
1603    Pay attention to that the idx can both increment and decrement,
1604    as well as make large jumps.
1605    
1606    Arguments:
1607        func :  (Callable)
1608            a function that passes an integer as input and updates the scene
1609        irange : (tuple)
1610            the range of the integer input representing the time series index
1611        dt : (float)
1612            the time interval between two calls to `func` in milliseconds
1613        loop : (bool)
1614            whether to loop the animation
1615        c : (list, str)
1616            the color of the play/pause button
1617        bc : (list)
1618            the background color of the play/pause button and the slider
1619        button_size : (int)
1620            the size of the play/pause buttons
1621        button_pos : (float, float)
1622            the position of the play/pause buttons as a fraction of the window size
1623        button_gap : (float)
1624            the gap between the buttons
1625        slider_length : (float)
1626            the length of the slider as a fraction of the window size
1627        slider_pos : (float, float)
1628            the position of the slider as a fraction of the window size
1629        kwargs: (dict)
1630            keyword arguments to be passed to `Plotter`
1631
1632    Examples:
1633        - [aspring2_player.py](https://github.com/marcomusy/vedo/tree/master/examples/simulations/aspring2_player.py)
1634    """
1635    # Original class contributed by @mikaeltulldahl (Mikael Tulldahl)
1636
1637    PLAY_SYMBOL        = "    \u23F5   "
1638    PAUSE_SYMBOL       = "   \u23F8   "
1639    ONE_BACK_SYMBOL    = " \u29CF"
1640    ONE_FORWARD_SYMBOL = "\u29D0 "
1641
1642    def __init__(
1643        self,
1644        func: Callable[[int],None],
1645        irange: tuple,
1646        dt: float = 1.0,
1647        loop: bool = True,
1648        c=("white", "white"),
1649        bc=("green3","red4"),
1650        button_size=25,
1651        button_pos=(0.5,0.08),
1652        button_gap=0.055,
1653        slider_length=0.5,
1654        slider_pos=(0.5,0.055),
1655        **kwargs,
1656    ):
1657        super().__init__(**kwargs)
1658
1659        min_value, max_value = np.array(irange).astype(int)
1660        button_pos = np.array(button_pos)
1661        slider_pos = np.array(slider_pos)
1662
1663        self._func = func
1664
1665        self.value = min_value-1
1666        self.min_value = min_value
1667        self.max_value = max_value
1668        self.dt = max(dt, 1)
1669        self.is_playing = False
1670        self._loop = loop
1671
1672        self.timer_callback_id = self.add_callback("timer", self._handle_timer)
1673        self.timer_id = None
1674
1675        self.play_pause_button = self.add_button(
1676            self.toggle,
1677            pos=button_pos,  # x,y fraction from bottom left corner
1678            states=[self.PLAY_SYMBOL, self.PAUSE_SYMBOL],
1679            font="Kanopus",
1680            size=button_size,
1681            bc=bc,
1682        )
1683        self.button_oneback = self.add_button(
1684            self.onebackward,
1685            pos=(-button_gap, 0) + button_pos,
1686            states=[self.ONE_BACK_SYMBOL],
1687            font="Kanopus",
1688            size=button_size,
1689            c=c,
1690            bc=bc,
1691        )
1692        self.button_oneforward = self.add_button(
1693            self.oneforward,
1694            pos=(button_gap, 0) + button_pos,
1695            states=[self.ONE_FORWARD_SYMBOL],
1696            font="Kanopus",
1697            size=button_size,
1698            bc=bc,
1699        )
1700        d = (1-slider_length)/2
1701        self.slider: SliderWidget = self.add_slider(
1702            self._slider_callback,
1703            self.min_value,
1704            self.max_value - 1,
1705            value=self.min_value,
1706            pos=[(d-0.5, 0)+slider_pos, (0.5-d, 0)+slider_pos],
1707            show_value=False,
1708            c=bc[0],
1709            alpha=1,
1710        )
1711
1712    def pause(self) -> None:
1713        """Pause the animation."""
1714        self.is_playing = False
1715        if self.timer_id is not None:
1716            self.timer_callback("destroy", self.timer_id)
1717            self.timer_id = None
1718        self.play_pause_button.status(self.PLAY_SYMBOL)
1719
1720    def resume(self) -> None:
1721        """Resume the animation."""
1722        if self.timer_id is not None:
1723            self.timer_callback("destroy", self.timer_id)
1724        self.timer_id = self.timer_callback("create", dt=int(self.dt))
1725        self.is_playing = True
1726        self.play_pause_button.status(self.PAUSE_SYMBOL)
1727
1728    def toggle(self) -> None:
1729        """Toggle between play and pause."""
1730        if not self.is_playing:
1731            self.resume()
1732        else:
1733            self.pause()
1734
1735    def oneforward(self) -> None:
1736        """Advance the animation by one frame."""
1737        self.pause()
1738        self.set_frame(self.value + 1)
1739
1740    def onebackward(self) -> None:
1741        """Go back one frame in the animation."""
1742        self.pause()
1743        self.set_frame(self.value - 1)
1744
1745    def set_frame(self, value: int) -> None:
1746        """Set the current value of the animation."""
1747        if self._loop:
1748            if value < self.min_value:
1749                value = self.max_value - 1
1750            elif value >= self.max_value:
1751                value = self.min_value
1752        else:
1753            if value < self.min_value:
1754                self.pause()
1755                value = self.min_value
1756            elif value >= self.max_value - 1:
1757                value = self.max_value - 1
1758                self.pause()
1759
1760        if self.value != value:
1761            self.value = value
1762            self.slider.value = value
1763            self._func(value)
1764
1765    def _slider_callback(self, widget: SliderWidget, _: str) -> None:
1766        self.pause()
1767        self.set_frame(int(round(widget.value)))
1768
1769    def _handle_timer(self, _: Event = None) -> None:
1770        self.set_frame(self.value + 1)
1771
1772    def stop(self) -> "AnimationPlayer":
1773        """
1774        Stop the animation timers, remove buttons and slider.
1775        Behave like a normal `Plotter` after this.
1776        """
1777        # stop timer
1778        if self.timer_id is not None:
1779            self.timer_callback("destroy", self.timer_id)
1780            self.timer_id = None
1781
1782        # remove callbacks
1783        self.remove_callback(self.timer_callback_id)
1784
1785        # remove buttons
1786        self.slider.off()
1787        self.renderer.RemoveActor(self.play_pause_button.actor)
1788        self.renderer.RemoveActor(self.button_oneback.actor)
1789        self.renderer.RemoveActor(self.button_oneforward.actor)
1790        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. Pay attention to that the idx can both increment and decrement, as well as make large jumps.

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: Callable[[int], NoneType], irange: tuple, dt: float = 1.0, loop: bool = True, c=('white', 'white'), bc=('green3', 'red4'), button_size=25, button_pos=(0.5, 0.08), button_gap=0.055, slider_length=0.5, slider_pos=(0.5, 0.055), **kwargs)
1642    def __init__(
1643        self,
1644        func: Callable[[int],None],
1645        irange: tuple,
1646        dt: float = 1.0,
1647        loop: bool = True,
1648        c=("white", "white"),
1649        bc=("green3","red4"),
1650        button_size=25,
1651        button_pos=(0.5,0.08),
1652        button_gap=0.055,
1653        slider_length=0.5,
1654        slider_pos=(0.5,0.055),
1655        **kwargs,
1656    ):
1657        super().__init__(**kwargs)
1658
1659        min_value, max_value = np.array(irange).astype(int)
1660        button_pos = np.array(button_pos)
1661        slider_pos = np.array(slider_pos)
1662
1663        self._func = func
1664
1665        self.value = min_value-1
1666        self.min_value = min_value
1667        self.max_value = max_value
1668        self.dt = max(dt, 1)
1669        self.is_playing = False
1670        self._loop = loop
1671
1672        self.timer_callback_id = self.add_callback("timer", self._handle_timer)
1673        self.timer_id = None
1674
1675        self.play_pause_button = self.add_button(
1676            self.toggle,
1677            pos=button_pos,  # x,y fraction from bottom left corner
1678            states=[self.PLAY_SYMBOL, self.PAUSE_SYMBOL],
1679            font="Kanopus",
1680            size=button_size,
1681            bc=bc,
1682        )
1683        self.button_oneback = self.add_button(
1684            self.onebackward,
1685            pos=(-button_gap, 0) + button_pos,
1686            states=[self.ONE_BACK_SYMBOL],
1687            font="Kanopus",
1688            size=button_size,
1689            c=c,
1690            bc=bc,
1691        )
1692        self.button_oneforward = self.add_button(
1693            self.oneforward,
1694            pos=(button_gap, 0) + button_pos,
1695            states=[self.ONE_FORWARD_SYMBOL],
1696            font="Kanopus",
1697            size=button_size,
1698            bc=bc,
1699        )
1700        d = (1-slider_length)/2
1701        self.slider: SliderWidget = self.add_slider(
1702            self._slider_callback,
1703            self.min_value,
1704            self.max_value - 1,
1705            value=self.min_value,
1706            pos=[(d-0.5, 0)+slider_pos, (0.5-d, 0)+slider_pos],
1707            show_value=False,
1708            c=bc[0],
1709            alpha=1,
1710        )
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
  • axes : (int)

    Note that Axes type-1 can be fully customized by passing a dictionary axes=dict(). Check out vedo.addons.Axes() for the available options.

    • 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 VTK CubeAxesActor 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 (use with zoom=8)
    • 12, show polar axes.
    • 13, draw a simple ruler at the bottom of the window
  • sharecam : (bool) if False each renderer will have an independent vtkCamera
  • interactive : (bool) if True will stop after show() to allow interaction w/ window
  • offscreen : (bool) if True will not show the rendering window
  • qt_widget : (QVTKRenderWindowInteractor) render in a Qt-Widget using an QVTKRenderWindowInteractor. Overrides offscreen to True. Overrides interactive to False. See examples qt_windows1.py and qt_windows2.py
def pause(self) -> None:
1712    def pause(self) -> None:
1713        """Pause the animation."""
1714        self.is_playing = False
1715        if self.timer_id is not None:
1716            self.timer_callback("destroy", self.timer_id)
1717            self.timer_id = None
1718        self.play_pause_button.status(self.PLAY_SYMBOL)

Pause the animation.

def resume(self) -> None:
1720    def resume(self) -> None:
1721        """Resume the animation."""
1722        if self.timer_id is not None:
1723            self.timer_callback("destroy", self.timer_id)
1724        self.timer_id = self.timer_callback("create", dt=int(self.dt))
1725        self.is_playing = True
1726        self.play_pause_button.status(self.PAUSE_SYMBOL)

Resume the animation.

def toggle(self) -> None:
1728    def toggle(self) -> None:
1729        """Toggle between play and pause."""
1730        if not self.is_playing:
1731            self.resume()
1732        else:
1733            self.pause()

Toggle between play and pause.

def oneforward(self) -> None:
1735    def oneforward(self) -> None:
1736        """Advance the animation by one frame."""
1737        self.pause()
1738        self.set_frame(self.value + 1)

Advance the animation by one frame.

def onebackward(self) -> None:
1740    def onebackward(self) -> None:
1741        """Go back one frame in the animation."""
1742        self.pause()
1743        self.set_frame(self.value - 1)

Go back one frame in the animation.

def set_frame(self, value: int) -> None:
1745    def set_frame(self, value: int) -> None:
1746        """Set the current value of the animation."""
1747        if self._loop:
1748            if value < self.min_value:
1749                value = self.max_value - 1
1750            elif value >= self.max_value:
1751                value = self.min_value
1752        else:
1753            if value < self.min_value:
1754                self.pause()
1755                value = self.min_value
1756            elif value >= self.max_value - 1:
1757                value = self.max_value - 1
1758                self.pause()
1759
1760        if self.value != value:
1761            self.value = value
1762            self.slider.value = value
1763            self._func(value)

Set the current value of the animation.

def stop(self) -> vedo.applications.AnimationPlayer:
1772    def stop(self) -> "AnimationPlayer":
1773        """
1774        Stop the animation timers, remove buttons and slider.
1775        Behave like a normal `Plotter` after this.
1776        """
1777        # stop timer
1778        if self.timer_id is not None:
1779            self.timer_callback("destroy", self.timer_id)
1780            self.timer_id = None
1781
1782        # remove callbacks
1783        self.remove_callback(self.timer_callback_id)
1784
1785        # remove buttons
1786        self.slider.off()
1787        self.renderer.RemoveActor(self.play_pause_button.actor)
1788        self.renderer.RemoveActor(self.button_oneback.actor)
1789        self.renderer.RemoveActor(self.button_oneforward.actor)
1790        return self

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

class Clock(vedo.assembly.Assembly):
1794class Clock(vedo.Assembly):
1795    """Clock animation."""
1796
1797    def __init__(self, h=None, m=None, s=None, font="Quikhand", title="", c="k"):
1798        """
1799        Create a clock with current time or user provided time.
1800
1801        Arguments:
1802            h : (int)
1803                hours in range [0,23]
1804            m : (int)
1805                minutes in range [0,59]
1806            s : (int)
1807                seconds in range [0,59]
1808            font : (str)
1809                font type
1810            title : (str)
1811                some extra text to show on the clock
1812            c : (str)
1813                color of the numbers
1814
1815        Example:
1816            ```python
1817            import time
1818            from vedo import show
1819            from vedo.applications import Clock
1820            clock = Clock()
1821            plt = show(clock, interactive=False)
1822            for i in range(10):
1823                time.sleep(1)
1824                clock.update()
1825                plt.render()
1826            plt.close()
1827            ```
1828            ![](https://vedo.embl.es/images/feats/clock.png)
1829        """
1830        self.elapsed = 0
1831        self._start = time.time()
1832
1833        wd = ""
1834        if h is None and m is None:
1835            t = time.localtime()
1836            h = t.tm_hour
1837            m = t.tm_min
1838            s = t.tm_sec
1839            if not title:
1840                d = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
1841                wd = f"{d[t.tm_wday]} {t.tm_mday}/{t.tm_mon}/{t.tm_year} "
1842
1843        h = int(h) % 24
1844        m = int(m) % 60
1845        t = (h * 60 + m) / 12 / 60
1846
1847        alpha = 2 * np.pi * t + np.pi / 2
1848        beta = 12 * 2 * np.pi * t + np.pi / 2
1849
1850        x1, y1 = np.cos(alpha), np.sin(alpha)
1851        x2, y2 = np.cos(beta), np.sin(beta)
1852        if s is not None:
1853            s = int(s) % 60
1854            gamma = s * 2 * np.pi / 60 + np.pi / 2
1855            x3, y3 = np.cos(gamma), np.sin(gamma)
1856
1857        ore = Line([0, 0], [x1, y1], lw=14, c="red4").scale(0.5).mirror()
1858        minu = Line([0, 0], [x2, y2], lw=7, c="blue3").scale(0.75).mirror()
1859        secs = None
1860        if s is not None:
1861            secs = Line([0, 0], [x3, y3], lw=1, c="k").scale(0.95).mirror()
1862            secs.z(0.003)
1863        back1 = vedo.shapes.Circle(res=180, c="k5")
1864        back2 = vedo.shapes.Circle(res=12).mirror().scale(0.84).rotate_z(-360 / 12)
1865        labels = back2.labels(range(1, 13), justify="center", font=font, c=c, scale=0.14)
1866        txt = vedo.shapes.Text3D(wd + title, font="VictorMono", justify="top-center", s=0.07, c=c)
1867        txt.pos(0, -0.25, 0.001)
1868        labels.z(0.001)
1869        minu.z(0.002)
1870        vedo.Assembly.__init__(self, [back1, labels, ore, minu, secs, txt])
1871        self.name = "Clock"
1872
1873    def update(self, h=None, m=None, s=None):
1874        """Update clock with current or user time."""
1875        parts = self.unpack()
1876        self.elapsed = time.time() - self._start
1877
1878        if h is None and m is None:
1879            t = time.localtime()
1880            h = t.tm_hour
1881            m = t.tm_min
1882            s = t.tm_sec
1883
1884        h = int(h) % 24
1885        m = int(m) % 60
1886        t = (h * 60 + m) / 12 / 60
1887
1888        alpha = 2 * np.pi * t + np.pi / 2
1889        beta = 12 * 2 * np.pi * t + np.pi / 2
1890
1891        x1, y1 = np.cos(alpha), np.sin(alpha)
1892        x2, y2 = np.cos(beta), np.sin(beta)
1893        if s is not None:
1894            s = int(s) % 60
1895            gamma = s * 2 * np.pi / 60 + np.pi / 2
1896            x3, y3 = np.cos(gamma), np.sin(gamma)
1897
1898        pts2 = parts[2].points()
1899        pts2[1] = [-x1 * 0.5, y1 * 0.5, 0.001]
1900        parts[2].points(pts2)
1901
1902        pts3 = parts[3].points()
1903        pts3[1] = [-x2 * 0.75, y2 * 0.75, 0.002]
1904        parts[3].points(pts3)
1905
1906        if s is not None:
1907            pts4 = parts[4].points()
1908            pts4[1] = [-x3 * 0.95, y3 * 0.95, 0.003]
1909            parts[4].points(pts4)
1910
1911        return self

Clock animation.

Clock(h=None, m=None, s=None, font='Quikhand', title='', c='k')
1797    def __init__(self, h=None, m=None, s=None, font="Quikhand", title="", c="k"):
1798        """
1799        Create a clock with current time or user provided time.
1800
1801        Arguments:
1802            h : (int)
1803                hours in range [0,23]
1804            m : (int)
1805                minutes in range [0,59]
1806            s : (int)
1807                seconds in range [0,59]
1808            font : (str)
1809                font type
1810            title : (str)
1811                some extra text to show on the clock
1812            c : (str)
1813                color of the numbers
1814
1815        Example:
1816            ```python
1817            import time
1818            from vedo import show
1819            from vedo.applications import Clock
1820            clock = Clock()
1821            plt = show(clock, interactive=False)
1822            for i in range(10):
1823                time.sleep(1)
1824                clock.update()
1825                plt.render()
1826            plt.close()
1827            ```
1828            ![](https://vedo.embl.es/images/feats/clock.png)
1829        """
1830        self.elapsed = 0
1831        self._start = time.time()
1832
1833        wd = ""
1834        if h is None and m is None:
1835            t = time.localtime()
1836            h = t.tm_hour
1837            m = t.tm_min
1838            s = t.tm_sec
1839            if not title:
1840                d = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
1841                wd = f"{d[t.tm_wday]} {t.tm_mday}/{t.tm_mon}/{t.tm_year} "
1842
1843        h = int(h) % 24
1844        m = int(m) % 60
1845        t = (h * 60 + m) / 12 / 60
1846
1847        alpha = 2 * np.pi * t + np.pi / 2
1848        beta = 12 * 2 * np.pi * t + np.pi / 2
1849
1850        x1, y1 = np.cos(alpha), np.sin(alpha)
1851        x2, y2 = np.cos(beta), np.sin(beta)
1852        if s is not None:
1853            s = int(s) % 60
1854            gamma = s * 2 * np.pi / 60 + np.pi / 2
1855            x3, y3 = np.cos(gamma), np.sin(gamma)
1856
1857        ore = Line([0, 0], [x1, y1], lw=14, c="red4").scale(0.5).mirror()
1858        minu = Line([0, 0], [x2, y2], lw=7, c="blue3").scale(0.75).mirror()
1859        secs = None
1860        if s is not None:
1861            secs = Line([0, 0], [x3, y3], lw=1, c="k").scale(0.95).mirror()
1862            secs.z(0.003)
1863        back1 = vedo.shapes.Circle(res=180, c="k5")
1864        back2 = vedo.shapes.Circle(res=12).mirror().scale(0.84).rotate_z(-360 / 12)
1865        labels = back2.labels(range(1, 13), justify="center", font=font, c=c, scale=0.14)
1866        txt = vedo.shapes.Text3D(wd + title, font="VictorMono", justify="top-center", s=0.07, c=c)
1867        txt.pos(0, -0.25, 0.001)
1868        labels.z(0.001)
1869        minu.z(0.002)
1870        vedo.Assembly.__init__(self, [back1, labels, ore, minu, secs, txt])
1871        self.name = "Clock"

Create a clock with current time or user provided time.

Arguments:
  • h : (int) hours in range [0,23]
  • m : (int) minutes in range [0,59]
  • s : (int) seconds in range [0,59]
  • font : (str) font type
  • title : (str) some extra text to show on the clock
  • c : (str) color of the numbers
Example:
import time
from vedo import show
from vedo.applications import Clock
clock = Clock()
plt = show(clock, interactive=False)
for i in range(10):
    time.sleep(1)
    clock.update()
    plt.render()
plt.close()

def update(self, h=None, m=None, s=None):
1873    def update(self, h=None, m=None, s=None):
1874        """Update clock with current or user time."""
1875        parts = self.unpack()
1876        self.elapsed = time.time() - self._start
1877
1878        if h is None and m is None:
1879            t = time.localtime()
1880            h = t.tm_hour
1881            m = t.tm_min
1882            s = t.tm_sec
1883
1884        h = int(h) % 24
1885        m = int(m) % 60
1886        t = (h * 60 + m) / 12 / 60
1887
1888        alpha = 2 * np.pi * t + np.pi / 2
1889        beta = 12 * 2 * np.pi * t + np.pi / 2
1890
1891        x1, y1 = np.cos(alpha), np.sin(alpha)
1892        x2, y2 = np.cos(beta), np.sin(beta)
1893        if s is not None:
1894            s = int(s) % 60
1895            gamma = s * 2 * np.pi / 60 + np.pi / 2
1896            x3, y3 = np.cos(gamma), np.sin(gamma)
1897
1898        pts2 = parts[2].points()
1899        pts2[1] = [-x1 * 0.5, y1 * 0.5, 0.001]
1900        parts[2].points(pts2)
1901
1902        pts3 = parts[3].points()
1903        pts3[1] = [-x2 * 0.75, y2 * 0.75, 0.002]
1904        parts[3].points(pts3)
1905
1906        if s is not None:
1907            pts4 = parts[4].points()
1908            pts4[1] = [-x3 * 0.95, y3 * 0.95, 0.003]
1909            parts[4].points(pts4)
1910
1911        return self

Update clock with current or user time.