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

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

Browser( objects=(), sliderpos=((0.55, 0.07), (0.96, 0.07)), c=None, prefix='', pos=(0, 0), size='auto', screensize='auto', title='Browser', bg='white', bg2=None, axes=4, resetcam=False, interactive=True)
718    def __init__(
719            self,
720            objects=(),
721            sliderpos=((0.55, 0.07),(0.96, 0.07)),
722            c=None,  # slider color
723            prefix="",
724            pos=(0, 0),
725            size="auto",
726            screensize="auto",
727            title="Browser",
728            bg="white",
729            bg2=None,
730            axes=4,
731            resetcam=False,
732            interactive=True,
733        ):
734        """
735        Browse a serie of vedo objects by using a simple slider.
736
737        Examples:
738            ```python
739            import vedo
740            from vedo.applications import Browser
741            meshes = vedo.load("data/2*0.vtk") # a python list of Meshes
742            plt = Browser(meshes, resetcam=1, axes=4) # a vedo.Plotter
743            plt.show().close()
744            ```
745
746        - [morphomatics_tube.py](https://github.com/marcomusy/vedo/tree/master/examples/other/morphomatics_tube.py)
747        """
748        Plotter.__init__(
749            self,
750            pos=pos,
751            size=size,
752            screensize=screensize,
753            title=title,
754            bg=bg,
755            bg2=bg2,
756            axes=axes,
757            interactive=interactive,
758        )
759
760        self += objects
761
762        self.slider = None
763
764        # define the slider
765        def sliderfunc(widget, event=None):
766            k = int(widget.GetRepresentation().GetValue())
767            ak = self.actors[k]
768            for a in self.actors:
769                if a == ak:
770                    a.on()
771                else:
772                    a.off()
773            if resetcam:
774                self.reset_camera()
775            tx = str(k)
776            if ak.filename:
777                tx = ak.filename.split("/")[-1]
778                tx = tx.split("\\")[-1]  # windows os
779            elif ak.name:
780                tx = ak.name
781            widget.GetRepresentation().SetTitleText(prefix + tx)
782
783        self.slider = self.add_slider(
784            sliderfunc,
785            0.5,
786            len(objects) - 0.5,
787            pos=sliderpos,
788            font="courier",
789            c=c,
790            show_value=False,
791        )
792        self.slider.GetRepresentation().SetTitleHeight(0.020)
793        sliderfunc(self.slider)  # init call

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

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

Generate a Volume isosurfacing controlled by a slider.

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

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

Set an initial number of points to define a region

def write(self, filename='mesh_edited.vtk'):
1028    def write(self, filename="mesh_edited.vtk"):
1029        """Save the resulting mesh to file"""
1030        self.mesh.write(filename)
1031        vedo.logger.info(f"mesh saved to file {filename}")
1032        return self

Save the resulting mesh to file

def start(self, *args, **kwargs):
1034    def start(self, *args, **kwargs):
1035        """Start window interaction (with mouse and keyboard)"""
1036        acts = [self.txt2d, self.mesh, self.points, self.spline, self.jline]
1037        self.show(acts + list(args), **kwargs)
1038        return self

Start window interaction (with mouse and keyboard)

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

Generate Volume rendering using ray casting.

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

Generate a window for Volume rendering using ray casting.

Returns:

vedo.Plotter object.

Examples:

class Slicer3DPlotter(vedo.plotter.Plotter):
 45class Slicer3DPlotter(Plotter):
 46    """
 47    Generate a rendering window with slicing planes for the input Volume.
 48    """
 49    def __init__(
 50            self,
 51            volume,
 52            alpha=1,
 53            cmaps=('gist_ncar_r', "hot_r", "bone_r", "jet", "Spectral_r"),
 54            map2cells=False,  # buggy
 55            clamp=True,
 56            use_slider3d=False,
 57            show_histo=True,
 58            show_icon=True,
 59            draggable=False,
 60            pos=(0, 0),
 61            size="auto",
 62            screensize="auto",
 63            title="",
 64            bg="white",
 65            bg2="lightblue",
 66            axes=7,
 67            resetcam=True,
 68            interactive=True,
 69        ):
 70        """
 71        Generate a rendering window with slicing planes for the input Volume.
 72     
 73        Arguments:
 74            alpha : (float)
 75                transparency of the slicing planes
 76            cmaps : (list)
 77                list of color maps names to cycle when clicking button
 78            map2cells : (bool)
 79                scalars are mapped to cells, not interpolated
 80            clamp : (bool)
 81                clamp scalar to reduce the effect of tails in color mapping
 82            use_slider3d : (bool)
 83                show sliders attached along the axes
 84            show_histo : (bool)
 85                show histogram on bottom left
 86            show_icon : (bool)
 87                show a small 3D rendering icon of the volume
 88            draggable : (bool)
 89                make the icon draggable
 90
 91        Examples:
 92            - [slicer1.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/slicer1.py)
 93
 94            <img src="https://vedo.embl.es/images/volumetric/slicer1.jpg" width="500">
 95        """
 96        self._cmap_slicer= 'gist_ncar_r'
 97
 98        if not title:
 99            if volume.filename:
100                title = volume.filename
101            else:
102                title = "Volume Slicer"
103
104        ################################
105        Plotter.__init__(
106            self,
107            pos=pos,
108            bg=bg,
109            bg2=bg2,
110            size=size,
111            screensize=screensize,
112            title=title,
113            interactive=interactive,
114            axes=axes,
115        )
116        ################################
117        box = volume.box().wireframe().alpha(0.1)
118
119        self.show(box, viewup="z", resetcam=resetcam, interactive=False)
120        if show_icon:
121            self.add_inset(
122                volume,
123                pos=(0.85, 0.85),
124                size=0.15, c="w",
125                draggable=draggable,
126            )
127
128        # inits
129        la, ld = 0.7, 0.3  # ambient, diffuse
130        dims = volume.dimensions()
131        data = volume.pointdata[0]
132        rmin, rmax = volume.imagedata().GetScalarRange()
133        if clamp:
134            hdata, edg = np.histogram(data, bins=50)
135            logdata = np.log(hdata + 1)
136            # mean  of the logscale plot
137            meanlog = np.sum(np.multiply(edg[:-1], logdata)) / np.sum(logdata)
138            rmax = min(rmax, meanlog + (meanlog - rmin) * 0.9)
139            rmin = max(rmin, meanlog - (rmax - meanlog) * 0.9)
140            vedo.logger.debug(
141                "scalar range clamped to range: ("
142                + precision(rmin, 3)
143                + ", "
144                + precision(rmax, 3)
145                + ")"
146            )
147        self._cmap_slicer = cmaps[0]
148        visibles = [None, None, None]
149        msh = volume.zslice(int(dims[2] / 2))
150        msh.alpha(alpha).lighting("", la, ld, 0)
151        msh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
152        if map2cells:
153            msh.mapPointsToCells()
154        self.renderer.AddActor(msh)
155        visibles[2] = msh
156        msh.add_scalarbar(pos=(0.04, 0.0), horizontal=True, font_size=0)
157
158        def sliderfunc_x(widget, event):
159            i = int(widget.GetRepresentation().GetValue())
160            msh = volume.xslice(i).alpha(alpha).lighting("", la, ld, 0)
161            msh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
162            if map2cells:
163                msh.mapPointsToCells()
164            self.renderer.RemoveActor(visibles[0])
165            if i and i < dims[0]:
166                self.renderer.AddActor(msh)
167            visibles[0] = msh
168
169        def sliderfunc_y(widget, event):
170            i = int(widget.GetRepresentation().GetValue())
171            msh = volume.yslice(i).alpha(alpha).lighting("", la, ld, 0)
172            msh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
173            if map2cells:
174                msh.mapPointsToCells()
175            self.renderer.RemoveActor(visibles[1])
176            if i and i < dims[1]:
177                self.renderer.AddActor(msh)
178            visibles[1] = msh
179
180        def sliderfunc_z(widget, event):
181            i = int(widget.GetRepresentation().GetValue())
182            msh = volume.zslice(i).alpha(alpha).lighting("", la, ld, 0)
183            msh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
184            if map2cells:
185                msh.mapPointsToCells()
186            self.renderer.RemoveActor(visibles[2])
187            if i and i < dims[2]:
188                self.renderer.AddActor(msh)
189            visibles[2] = msh
190
191        cx, cy, cz, ch = "dr", "dg", "db", (0.3, 0.3, 0.3)
192        if np.sum(self.renderer.GetBackground()) < 1.5:
193            cx, cy, cz = "lr", "lg", "lb"
194            ch = (0.8, 0.8, 0.8)
195
196        if not use_slider3d:
197            self.add_slider(
198                sliderfunc_x,
199                0,
200                dims[0],
201                title="X",
202                title_size=0.5,
203                pos=[(0.8, 0.12), (0.95, 0.12)],
204                show_value=False,
205                c=cx,
206            )
207            self.add_slider(
208                sliderfunc_y,
209                0,
210                dims[1],
211                title="Y",
212                title_size=0.5,
213                pos=[(0.8, 0.08), (0.95, 0.08)],
214                show_value=False,
215                c=cy,
216            )
217            self.add_slider(
218                sliderfunc_z,
219                0,
220                dims[2],
221                title="Z",
222                title_size=0.6,
223                value=int(dims[2] / 2),
224                pos=[(0.8, 0.04), (0.95, 0.04)],
225                show_value=False,
226                c=cz,
227            )
228        else:  # 3d sliders attached to the axes bounds
229            bs = box.bounds()
230            self.add_slider3d(
231                sliderfunc_x,
232                pos1=(bs[0], bs[2], bs[4]),
233                pos2=(bs[1], bs[2], bs[4]),
234                xmin=0,
235                xmax=dims[0],
236                t=box.diagonal_size() / mag(box.xbounds()) * 0.6,
237                c=cx,
238                show_value=False,
239            )
240            self.add_slider3d(
241                sliderfunc_y,
242                pos1=(bs[1], bs[2], bs[4]),
243                pos2=(bs[1], bs[3], bs[4]),
244                xmin=0,
245                xmax=dims[1],
246                t=box.diagonal_size() / mag(box.ybounds()) * 0.6,
247                c=cy,
248                show_value=False,
249            )
250            self.add_slider3d(
251                sliderfunc_z,
252                pos1=(bs[0], bs[2], bs[4]),
253                pos2=(bs[0], bs[2], bs[5]),
254                xmin=0,
255                xmax=dims[2],
256                value=int(dims[2] / 2),
257                t=box.diagonal_size() / mag(box.zbounds()) * 0.6,
258                c=cz,
259                show_value=False,
260            )
261
262        #################
263        def buttonfunc():
264            bu.switch()
265            self._cmap_slicer = bu.status()
266            for mesh in visibles:
267                if mesh:
268                    mesh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
269                    if map2cells:
270                        mesh.mapPointsToCells()
271            self.renderer.RemoveActor(mesh.scalarbar)
272            mesh.add_scalarbar(pos=(0.04, 0.0), horizontal=True)
273            self.renderer.AddActor(mesh.scalarbar)
274
275        bu = self.add_button(
276            buttonfunc,
277            pos=(0.27, 0.005),
278            states=cmaps,
279            c=["db"] * len(cmaps),
280            bc=["lb"] * len(cmaps),  # colors of states
281            size=14,
282            bold=True,
283        )
284
285        #################
286        hist = None
287        if show_histo:
288            hist = CornerHistogram(
289                data,
290                s=0.2,
291                bins=25,
292                logscale=1,
293                pos=(0.02, 0.02),
294                c=ch,
295                bg=ch,
296                alpha=0.7,
297            )
298
299        self.add([msh, hist], resetcam=False)
300        if interactive:
301            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)
 49    def __init__(
 50            self,
 51            volume,
 52            alpha=1,
 53            cmaps=('gist_ncar_r', "hot_r", "bone_r", "jet", "Spectral_r"),
 54            map2cells=False,  # buggy
 55            clamp=True,
 56            use_slider3d=False,
 57            show_histo=True,
 58            show_icon=True,
 59            draggable=False,
 60            pos=(0, 0),
 61            size="auto",
 62            screensize="auto",
 63            title="",
 64            bg="white",
 65            bg2="lightblue",
 66            axes=7,
 67            resetcam=True,
 68            interactive=True,
 69        ):
 70        """
 71        Generate a rendering window with slicing planes for the input Volume.
 72     
 73        Arguments:
 74            alpha : (float)
 75                transparency of the slicing planes
 76            cmaps : (list)
 77                list of color maps names to cycle when clicking button
 78            map2cells : (bool)
 79                scalars are mapped to cells, not interpolated
 80            clamp : (bool)
 81                clamp scalar to reduce the effect of tails in color mapping
 82            use_slider3d : (bool)
 83                show sliders attached along the axes
 84            show_histo : (bool)
 85                show histogram on bottom left
 86            show_icon : (bool)
 87                show a small 3D rendering icon of the volume
 88            draggable : (bool)
 89                make the icon draggable
 90
 91        Examples:
 92            - [slicer1.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/slicer1.py)
 93
 94            <img src="https://vedo.embl.es/images/volumetric/slicer1.jpg" width="500">
 95        """
 96        self._cmap_slicer= 'gist_ncar_r'
 97
 98        if not title:
 99            if volume.filename:
100                title = volume.filename
101            else:
102                title = "Volume Slicer"
103
104        ################################
105        Plotter.__init__(
106            self,
107            pos=pos,
108            bg=bg,
109            bg2=bg2,
110            size=size,
111            screensize=screensize,
112            title=title,
113            interactive=interactive,
114            axes=axes,
115        )
116        ################################
117        box = volume.box().wireframe().alpha(0.1)
118
119        self.show(box, viewup="z", resetcam=resetcam, interactive=False)
120        if show_icon:
121            self.add_inset(
122                volume,
123                pos=(0.85, 0.85),
124                size=0.15, c="w",
125                draggable=draggable,
126            )
127
128        # inits
129        la, ld = 0.7, 0.3  # ambient, diffuse
130        dims = volume.dimensions()
131        data = volume.pointdata[0]
132        rmin, rmax = volume.imagedata().GetScalarRange()
133        if clamp:
134            hdata, edg = np.histogram(data, bins=50)
135            logdata = np.log(hdata + 1)
136            # mean  of the logscale plot
137            meanlog = np.sum(np.multiply(edg[:-1], logdata)) / np.sum(logdata)
138            rmax = min(rmax, meanlog + (meanlog - rmin) * 0.9)
139            rmin = max(rmin, meanlog - (rmax - meanlog) * 0.9)
140            vedo.logger.debug(
141                "scalar range clamped to range: ("
142                + precision(rmin, 3)
143                + ", "
144                + precision(rmax, 3)
145                + ")"
146            )
147        self._cmap_slicer = cmaps[0]
148        visibles = [None, None, None]
149        msh = volume.zslice(int(dims[2] / 2))
150        msh.alpha(alpha).lighting("", la, ld, 0)
151        msh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
152        if map2cells:
153            msh.mapPointsToCells()
154        self.renderer.AddActor(msh)
155        visibles[2] = msh
156        msh.add_scalarbar(pos=(0.04, 0.0), horizontal=True, font_size=0)
157
158        def sliderfunc_x(widget, event):
159            i = int(widget.GetRepresentation().GetValue())
160            msh = volume.xslice(i).alpha(alpha).lighting("", la, ld, 0)
161            msh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
162            if map2cells:
163                msh.mapPointsToCells()
164            self.renderer.RemoveActor(visibles[0])
165            if i and i < dims[0]:
166                self.renderer.AddActor(msh)
167            visibles[0] = msh
168
169        def sliderfunc_y(widget, event):
170            i = int(widget.GetRepresentation().GetValue())
171            msh = volume.yslice(i).alpha(alpha).lighting("", la, ld, 0)
172            msh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
173            if map2cells:
174                msh.mapPointsToCells()
175            self.renderer.RemoveActor(visibles[1])
176            if i and i < dims[1]:
177                self.renderer.AddActor(msh)
178            visibles[1] = msh
179
180        def sliderfunc_z(widget, event):
181            i = int(widget.GetRepresentation().GetValue())
182            msh = volume.zslice(i).alpha(alpha).lighting("", la, ld, 0)
183            msh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
184            if map2cells:
185                msh.mapPointsToCells()
186            self.renderer.RemoveActor(visibles[2])
187            if i and i < dims[2]:
188                self.renderer.AddActor(msh)
189            visibles[2] = msh
190
191        cx, cy, cz, ch = "dr", "dg", "db", (0.3, 0.3, 0.3)
192        if np.sum(self.renderer.GetBackground()) < 1.5:
193            cx, cy, cz = "lr", "lg", "lb"
194            ch = (0.8, 0.8, 0.8)
195
196        if not use_slider3d:
197            self.add_slider(
198                sliderfunc_x,
199                0,
200                dims[0],
201                title="X",
202                title_size=0.5,
203                pos=[(0.8, 0.12), (0.95, 0.12)],
204                show_value=False,
205                c=cx,
206            )
207            self.add_slider(
208                sliderfunc_y,
209                0,
210                dims[1],
211                title="Y",
212                title_size=0.5,
213                pos=[(0.8, 0.08), (0.95, 0.08)],
214                show_value=False,
215                c=cy,
216            )
217            self.add_slider(
218                sliderfunc_z,
219                0,
220                dims[2],
221                title="Z",
222                title_size=0.6,
223                value=int(dims[2] / 2),
224                pos=[(0.8, 0.04), (0.95, 0.04)],
225                show_value=False,
226                c=cz,
227            )
228        else:  # 3d sliders attached to the axes bounds
229            bs = box.bounds()
230            self.add_slider3d(
231                sliderfunc_x,
232                pos1=(bs[0], bs[2], bs[4]),
233                pos2=(bs[1], bs[2], bs[4]),
234                xmin=0,
235                xmax=dims[0],
236                t=box.diagonal_size() / mag(box.xbounds()) * 0.6,
237                c=cx,
238                show_value=False,
239            )
240            self.add_slider3d(
241                sliderfunc_y,
242                pos1=(bs[1], bs[2], bs[4]),
243                pos2=(bs[1], bs[3], bs[4]),
244                xmin=0,
245                xmax=dims[1],
246                t=box.diagonal_size() / mag(box.ybounds()) * 0.6,
247                c=cy,
248                show_value=False,
249            )
250            self.add_slider3d(
251                sliderfunc_z,
252                pos1=(bs[0], bs[2], bs[4]),
253                pos2=(bs[0], bs[2], bs[5]),
254                xmin=0,
255                xmax=dims[2],
256                value=int(dims[2] / 2),
257                t=box.diagonal_size() / mag(box.zbounds()) * 0.6,
258                c=cz,
259                show_value=False,
260            )
261
262        #################
263        def buttonfunc():
264            bu.switch()
265            self._cmap_slicer = bu.status()
266            for mesh in visibles:
267                if mesh:
268                    mesh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
269                    if map2cells:
270                        mesh.mapPointsToCells()
271            self.renderer.RemoveActor(mesh.scalarbar)
272            mesh.add_scalarbar(pos=(0.04, 0.0), horizontal=True)
273            self.renderer.AddActor(mesh.scalarbar)
274
275        bu = self.add_button(
276            buttonfunc,
277            pos=(0.27, 0.005),
278            states=cmaps,
279            c=["db"] * len(cmaps),
280            bc=["lb"] * len(cmaps),  # colors of states
281            size=14,
282            bold=True,
283        )
284
285        #################
286        hist = None
287        if show_histo:
288            hist = CornerHistogram(
289                data,
290                s=0.2,
291                bins=25,
292                logscale=1,
293                pos=(0.02, 0.02),
294                c=ch,
295                bg=ch,
296                alpha=0.7,
297            )
298
299        self.add([msh, hist], resetcam=False)
300        if interactive:
301            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):
305class Slicer2DPlotter(Plotter):
306    """
307    A single slice of a Volume which always faces the camera,
308    but at the same time can be oriented arbitrarily in space.
309    """
310    def __init__(
311        self,
312        volume,
313        levels=(None, None),
314        histo_color="red5",
315        **kwargs,
316    ):
317        """
318        A single slice of a Volume which always faces the camera,
319        but at the same time can be oriented arbitrarily in space.
320
321        Arguments:
322            levels : (list)
323                window and color levels
324            histo_color : (color)
325                histogram color, use `None` to disable it
326
327        <img src="https://vedo.embl.es/images/volumetric/read_volume3.jpg" width="500">
328        """
329        if "shape" not in kwargs:
330            custom_shape = [  # define here the 2 rendering rectangle spaces
331                dict(bottomleft=(0.0, 0.0), topright=(1, 1), bg="k9"),  # the full window
332                dict(bottomleft=(0.8, 0.8), topright=(1, 1), bg="k8", bg2="lb"),
333            ]
334            kwargs["shape"] = custom_shape
335
336        Plotter.__init__(self, **kwargs)
337
338        # reuse the same underlying data as in vol
339        vsl = vedo.volume.VolumeSlice(volume)
340
341        # no argument will grab the existing cmap in vol (or use build_lut())
342        vsl.colorize()
343
344        if levels[0] and levels[1]:
345            vsl.lighting(window=levels[0], level=levels[1])
346
347        usage = Text2D((
348            "Left click & drag  \rightarrow modify luminosity and contrast\n"
349            "SHIFT+Left click   \rightarrow slice image obliquely\n"
350            "SHIFT+Middle click \rightarrow slice image perpendicularly\n"
351            "R                  \rightarrow Reset the Window/Color levels\n"
352            "X                  \rightarrow Reset to sagittal view\n"
353            "Y                  \rightarrow Reset to coronal view\n"
354            "Z                  \rightarrow Reset to axial view"),
355            font="Calco",
356            pos="top-left",
357            s=0.8,
358            bg="yellow",
359            alpha=0.25,
360        )
361
362        hist = None
363        if histo_color is not None:
364            # hist = CornerHistogram(
365            #     volume.pointdata[0],
366            #     bins=25,
367            #     logscale=1,
368            #     pos=(0.02, 0.02),
369            #     s=0.175,
370            #     c="dg",
371            #     bg="k",
372            #     alpha=1,
373            # )
374            hist = vedo.pyplot.histogram(
375                volume.pointdata[0],
376                bins=10,
377                logscale=True,
378                c=histo_color,
379                ytitle="log_10 (counts)",
380                axes=dict(text_scale=1.9)
381            )
382            hist = hist.as2d(pos="bottom-left", scale=0.5)
383        
384        axes = kwargs.pop("axes", 7)
385        interactive = kwargs.pop("interactive", True)
386        if axes == 7:
387            ax = vedo.addons.RulerAxes(vsl, xtitle="x - ", ytitle="y - ", ztitle="z - ")
388
389        box = vsl.box().alpha(0.2)
390        self.at(0).show(vsl, box, ax, usage, hist, mode="image")
391        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)
310    def __init__(
311        self,
312        volume,
313        levels=(None, None),
314        histo_color="red5",
315        **kwargs,
316    ):
317        """
318        A single slice of a Volume which always faces the camera,
319        but at the same time can be oriented arbitrarily in space.
320
321        Arguments:
322            levels : (list)
323                window and color levels
324            histo_color : (color)
325                histogram color, use `None` to disable it
326
327        <img src="https://vedo.embl.es/images/volumetric/read_volume3.jpg" width="500">
328        """
329        if "shape" not in kwargs:
330            custom_shape = [  # define here the 2 rendering rectangle spaces
331                dict(bottomleft=(0.0, 0.0), topright=(1, 1), bg="k9"),  # the full window
332                dict(bottomleft=(0.8, 0.8), topright=(1, 1), bg="k8", bg2="lb"),
333            ]
334            kwargs["shape"] = custom_shape
335
336        Plotter.__init__(self, **kwargs)
337
338        # reuse the same underlying data as in vol
339        vsl = vedo.volume.VolumeSlice(volume)
340
341        # no argument will grab the existing cmap in vol (or use build_lut())
342        vsl.colorize()
343
344        if levels[0] and levels[1]:
345            vsl.lighting(window=levels[0], level=levels[1])
346
347        usage = Text2D((
348            "Left click & drag  \rightarrow modify luminosity and contrast\n"
349            "SHIFT+Left click   \rightarrow slice image obliquely\n"
350            "SHIFT+Middle click \rightarrow slice image perpendicularly\n"
351            "R                  \rightarrow Reset the Window/Color levels\n"
352            "X                  \rightarrow Reset to sagittal view\n"
353            "Y                  \rightarrow Reset to coronal view\n"
354            "Z                  \rightarrow Reset to axial view"),
355            font="Calco",
356            pos="top-left",
357            s=0.8,
358            bg="yellow",
359            alpha=0.25,
360        )
361
362        hist = None
363        if histo_color is not None:
364            # hist = CornerHistogram(
365            #     volume.pointdata[0],
366            #     bins=25,
367            #     logscale=1,
368            #     pos=(0.02, 0.02),
369            #     s=0.175,
370            #     c="dg",
371            #     bg="k",
372            #     alpha=1,
373            # )
374            hist = vedo.pyplot.histogram(
375                volume.pointdata[0],
376                bins=10,
377                logscale=True,
378                c=histo_color,
379                ytitle="log_10 (counts)",
380                axes=dict(text_scale=1.9)
381            )
382            hist = hist.as2d(pos="bottom-left", scale=0.5)
383        
384        axes = kwargs.pop("axes", 7)
385        interactive = kwargs.pop("interactive", True)
386        if axes == 7:
387            ax = vedo.addons.RulerAxes(vsl, xtitle="x - ", ytitle="y - ", ztitle="z - ")
388
389        box = vsl.box().alpha(0.2)
390        self.at(0).show(vsl, box, ax, usage, hist, mode="image")
391        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):
1041class SplinePlotter(Plotter):
1042    """
1043    Interactive drawing of splined curves on meshes.
1044    """
1045    def __init__(self, obj, init_points=(), **kwargs):
1046        """
1047        Create an interactive application that allows the user to click points and
1048        retrieve the coordinates of such points and optionally a spline or line
1049        (open or closed).
1050
1051        Input object can be a image file name or a 3D mesh.
1052        """
1053        super().__init__(**kwargs)
1054
1055        self.mode    = 'trackball'
1056        self.verbose = True
1057        self.splined = True
1058        self.resolution = None  # spline resolution (None = automatic)
1059        self.closed  = False
1060        self.lcolor  = 'yellow4'
1061        self.lwidth  = 3
1062        self.pcolor  = 'purple5'
1063        self.psize   = 10
1064
1065        self.cpoints = list(init_points)
1066        self.vpoints = None
1067        self.line = None
1068
1069        if isinstance(obj, str):
1070            self.object = vedo.io.load(obj)
1071        else:
1072            self.object = obj
1073        
1074        if isinstance(self.object, vedo.Picture):
1075            self.mode = 'image'
1076
1077        t = (
1078            "Click to add a point\n"
1079            "Right-click to remove it\n"
1080            "Drag mouse to change constrast\n"
1081            "Press c to clear points\n"
1082            "Press q to continue"
1083        )
1084        self.instructions = Text2D(
1085            t, pos='bottom-left', c='white', bg='green', font='Calco'
1086        )
1087        
1088        self += [self.object, self.instructions]
1089
1090        self.callid1 = self.add_callback('KeyPress', self._key_press)
1091        self.callid2 = self.add_callback('LeftButtonPress', self._on_left_click)
1092        self.callid3 = self.add_callback('RightButtonPress', self._on_right_click)
1093
1094    def points(self, newpts=None):
1095        """Retrieve the 3D coordinates of the clicked points"""
1096        if newpts is not None:
1097            self.cpoints = newpts
1098            self._update()
1099            return self
1100        return np.array(self.cpoints)
1101
1102    def _on_left_click(self, evt):
1103        if not evt.actor: 
1104            return
1105        if evt.actor.name == "points":
1106            # remove clicked point if clicked twice
1107            pid = self.vpoints.closest_point(evt.picked3d, return_point_id=True)
1108            self.cpoints.pop(pid)
1109            self._update()
1110            return
1111        p = evt.picked3d
1112        self.cpoints.append(p)
1113        self._update()
1114        if self.verbose:
1115            vedo.colors.printc("Added point:", precision(p,4), c='g')
1116
1117    def _on_right_click(self, evt):
1118        if evt.actor and len(self.cpoints)>0:
1119            self.cpoints.pop() # pop removes from the list the last pt
1120            self._update()
1121            if self.verbose:
1122                vedo.colors.printc("Deleted last point", c="r")
1123
1124    def _update(self):
1125        self.remove(self.line, self.vpoints)  # remove old points and spline
1126        self.vpoints = Points(self.cpoints).ps(self.psize).c(self.pcolor)
1127        self.vpoints.name = "points"
1128        self.vpoints.pickable(True)  # to allow toggle
1129        minnr = 1
1130        if self.splined:
1131            minnr = 2
1132        if self.lwidth and len(self.cpoints) > minnr:
1133            if self.splined:
1134                try:
1135                    self.line = Spline(self.cpoints, closed=self.closed, res=self.resolution)
1136                except ValueError:
1137                    # if clicking too close splining might fail
1138                    self.cpoints.pop()
1139                    return
1140            else:
1141                self.line = Line(self.cpoints, closed=self.closed)
1142            self.line.c(self.lcolor).lw(self.lwidth).pickable(False)
1143            self.add(self.vpoints, self.line)
1144        else:
1145            self.add(self.vpoints)
1146
1147    def _key_press(self, evt):
1148        if evt.keypress == 'c':
1149            self.cpoints = []
1150            self.remove(self.line, self.vpoints).render()
1151            if self.verbose:
1152                vedo.colors.printc("==== Cleared all points ====", c="r", invert=True)
1153
1154    def start(self):
1155        """Start the interaction"""
1156        self.show(self.object, self.instructions, mode=self.mode)
1157        return self

Interactive drawing of splined curves on meshes.

SplinePlotter(obj, init_points=(), **kwargs)
1045    def __init__(self, obj, init_points=(), **kwargs):
1046        """
1047        Create an interactive application that allows the user to click points and
1048        retrieve the coordinates of such points and optionally a spline or line
1049        (open or closed).
1050
1051        Input object can be a image file name or a 3D mesh.
1052        """
1053        super().__init__(**kwargs)