Module vedo.applications

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

Expand source code
import os
import numpy as np
import vedo
from vedo.colors import colorMap
from vedo.colors import getColor
from vedo.plotter import Plotter
from vedo.pointcloud import fitPlane
from vedo.pointcloud import Points
from vedo.pyplot import CornerHistogram
from vedo.shapes import Line
from vedo.shapes import Ribbon
from vedo.shapes import Spline
from vedo.shapes import Text2D
from vedo.utils import isSequence
from vedo.utils import linInterpolate
from vedo.utils import mag
from vedo.utils import precision

__doc__ = """
This module contains vedo applications which provide some *ready-to-use* funcionalities
.. image:: https://vedo.embl.es/images/advanced/app_raycaster.gif
"""


__all__ = [
    'Browser',
    'IsosurfaceBrowser',
    'FreeHandCutPlotter',
    'RayCastPlotter',
    "Slicer3DPlotter",
    "Slicer2DPlotter",
    # "Animation",
]


#################################
class Slicer3DPlotter(Plotter):
    """
    Generate a ``Plotter`` window with slicing planes for the input Volume.

    Returns the ``Plotter`` object.

    Parameters
    ----------
    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

    useSlider3D : bool
        show sliders attached along the axes

    showHisto : bool
        show histogram on bottom left

    showIcon : bool
        show a small 3D rendering icon of the volume

    draggable : bool
        make the icon draggable

    .. hint:: examples/volumetric/slicer1.py
        .. image:: https://vedo.embl.es/images/volumetric/slicer1.jpg
    """
    def __init__(
            self,
            volume,
            alpha=1,
            cmaps=('gist_ncar_r', "hot_r", "bone_r", "jet", "Spectral_r"),
            map2cells=False,  # buggy
            clamp=True,
            useSlider3D=False,
            showHisto=True,
            showIcon=True,
            draggable=False,
            pos=(0, 0),
            size="auto",
            screensize="auto",
            title="",
            bg="white",
            bg2="lightblue",
            axes=7,
            resetcam=True,
            interactive=True,
        ):
        self._cmap_slicer= 'gist_ncar_r'

        if not title:
            if volume.filename:
                title = volume.filename
            else:
                title = "Volume Slicer"

        ################################
        Plotter.__init__(
            self,
            pos=pos,
            bg=bg,
            bg2=bg2,
            size=size,
            screensize=screensize,
            title=title,
            interactive=interactive,
            axes=axes,
        )
        ################################
        box = volume.box().wireframe().alpha(0.1)

        self.show(box, viewup="z", resetcam=resetcam, interactive=False)
        if showIcon:
            self.addInset(volume, pos=(.85,.85), size=0.15, c='w', draggable=draggable)

        # inits
        la, ld = 0.7, 0.3 #ambient, diffuse
        dims = volume.dimensions()
        data = volume.pointdata[0]
        rmin, rmax = volume.imagedata().GetScalarRange()
        if clamp:
            hdata, edg = np.histogram(data, bins=50)
            logdata = np.log(hdata+1)
            # mean  of the logscale plot
            meanlog = np.sum(np.multiply(edg[:-1], logdata))/np.sum(logdata)
            rmax = min(rmax, meanlog+(meanlog-rmin)*0.9)
            rmin = max(rmin, meanlog-(rmax-meanlog)*0.9)
            vedo.logger.debug('scalar range clamped to range: (' + precision(rmin, 3) +', '+  precision(rmax, 3)+')')
        self._cmap_slicer = cmaps[0]
        visibles = [None, None, None]
        msh = volume.zSlice(int(dims[2]/2))
        msh.alpha(alpha).lighting('', la, ld, 0)
        msh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
        if map2cells: msh.mapPointsToCells()
        self.renderer.AddActor(msh)
        visibles[2] = msh
        msh.addScalarBar(pos=(0.04,0.0), horizontal=True, titleFontSize=0)

        def sliderfunc_x(widget, event):
            i = int(widget.GetRepresentation().GetValue())
            msh = volume.xSlice(i).alpha(alpha).lighting('', la, ld, 0)
            msh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
            if map2cells: msh.mapPointsToCells()
            self.renderer.RemoveActor(visibles[0])
            if i and i<dims[0]:
                self.renderer.AddActor(msh)
            visibles[0] = msh

        def sliderfunc_y(widget, event):
            i = int(widget.GetRepresentation().GetValue())
            msh = volume.ySlice(i).alpha(alpha).lighting('', la, ld, 0)
            msh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
            if map2cells: msh.mapPointsToCells()
            self.renderer.RemoveActor(visibles[1])
            if i and i<dims[1]:
                self.renderer.AddActor(msh)
            visibles[1] = msh

        def sliderfunc_z(widget, event):
            i = int(widget.GetRepresentation().GetValue())
            msh = volume.zSlice(i).alpha(alpha).lighting('', la, ld, 0)
            msh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
            if map2cells: msh.mapPointsToCells()
            self.renderer.RemoveActor(visibles[2])
            if i and i<dims[2]:
                self.renderer.AddActor(msh)
            visibles[2] = msh

        cx, cy, cz, ch = 'dr', 'dg', 'db', (0.3,0.3,0.3)
        if np.sum(self.renderer.GetBackground()) < 1.5:
            cx, cy, cz = 'lr', 'lg', 'lb'
            ch = (0.8,0.8,0.8)

        if not useSlider3D:
            self.addSlider2D(sliderfunc_x, 0, dims[0], title='X', titleSize=0.5,
                           pos=[(0.8,0.12), (0.95,0.12)], showValue=False, c=cx)
            self.addSlider2D(sliderfunc_y, 0, dims[1], title='Y', titleSize=0.5,
                           pos=[(0.8,0.08), (0.95,0.08)], showValue=False, c=cy)
            self.addSlider2D(sliderfunc_z, 0, dims[2], title='Z', titleSize=0.6,
                           value=int(dims[2]/2),
                           pos=[(0.8,0.04), (0.95,0.04)], showValue=False, c=cz)
        else: # 3d sliders attached to the axes bounds
            bs = box.bounds()
            self.addSlider3D(
                    sliderfunc_x,
                    pos1=(bs[0], bs[2], bs[4]),
                    pos2=(bs[1], bs[2], bs[4]),
                    xmin=0, xmax=dims[0],
                    t=box.diagonalSize()/mag(box.xbounds())*0.6,
                    c=cx,
                    showValue=False,
            )
            self.addSlider3D(
                    sliderfunc_y,
                    pos1=(bs[1], bs[2], bs[4]),
                    pos2=(bs[1], bs[3], bs[4]),
                    xmin=0, xmax=dims[1],
                    t=box.diagonalSize()/mag(box.ybounds())*0.6,
                    c=cy,
                    showValue=False,
            )
            self.addSlider3D(
                    sliderfunc_z,
                    pos1=(bs[0], bs[2], bs[4]),
                    pos2=(bs[0], bs[2], bs[5]),
                    xmin=0, xmax=dims[2],
                    value=int(dims[2]/2),
                    t=box.diagonalSize()/mag(box.zbounds())*0.6,
                    c=cz,
                    showValue=False,
            )


        #################
        def buttonfunc():
            bu.switch()
            self._cmap_slicer = bu.status()
            for mesh in visibles:
                if mesh:
                    mesh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
                    if map2cells:
                        mesh.mapPointsToCells()
            self.renderer.RemoveActor(mesh.scalarbar)
            mesh.addScalarBar(pos=(0.04,0.0), horizontal=True, titleFontSize=0)

        bu = self.addButton(buttonfunc,
            pos=(0.27, 0.005),
            states=cmaps,
            c=["db"]*len(cmaps),
            bc=["lb"]*len(cmaps),  # colors of states
            size=14,
            bold=True,
        )

        #################
        hist = None
        if showHisto:
            hist = CornerHistogram(data, s=0.2,
                                   bins=25, logscale=1, pos=(0.02, 0.02),
                                   c=ch, bg=ch, alpha=0.7,
            )

        self.add([msh, hist], resetcam=False)
        if interactive:
            self.interactive()


########################################################################################
class Slicer2DPlotter(Plotter):
    """
    Create a ``Plotter`` with a single slice of a Volume which always faces the camera,
    but at the same time can be oriented arbitrarily in space.

    Parameters
    ----------
    levels : list
        window and color level

    .. image:: https://vedo.embl.es/images/volumetric/read_volume3.jpg
    """
    def __init__(self,
                 volume,
                 levels=(None, None),
                 axes=None,
                 zoom=1.2,
                 pos=(0, 0),
                 size="auto",
                 screensize="auto",
                 title="",
                 bg="white",
                 bg2=None,
                 interactive=True,
        ):
        custom_shape = [ # define here the 2 rendering rectangle spaces
            dict(bottomleft=(0.0,0.0), topright=(1,1), bg='k9'), # the full window
            dict(bottomleft=(0.8,0.8), topright=(1,1), bg='k8', bg2='lb'),
        ]

        if not title:
            if volume.filename:
                title = volume.filename[:80]
            else:
                title = "Volume Slicer2D"

        Plotter.__init__(self, shape=custom_shape, title=title, pos=pos,
                         screensize=screensize, size=size, bg=bg, bg2=bg2, axes=0,
                         interactive=False)

        vsl = vedo.volume.VolumeSlice(volume)  # reuse the same underlying data as in vol

        # no argument will grab the existing cmap in vol (or use buildLUT())
        vsl.colorize()

        if levels[0] and levels[1]:
            vsl.lighting(window=levels[0], level=levels[1])

        usage = Text2D(
            f"SHIFT+Left click   \rightarrow rotate camera for oblique slicing\n"
            f"SHIFT+Middle click \rightarrow slice perpendicularly through image\n"
            f"Left click & drag  \rightarrow modify luminosity and contrast\n"
            f"R                  \rightarrow Reset the Window/Color levels\n"
            f"X                  \rightarrow Reset to sagittal view\n"
            f"Y                  \rightarrow Reset to coronal view\n"
            f"Z                  \rightarrow Reset to axial view",
            font="Calco", pos="top-left", s=0.8, bg='yellow', alpha=0.25
        )

        hist = CornerHistogram(volume.pointdata[0],
                               bins=25, logscale=1, pos=(0.02, 0.02), s=0.175,
                               c='dg', bg='k', alpha=1)
        ax = None
        if axes == 7:
            ax = vedo.addons.RulerAxes(vsl, xtitle='x - ', ytitle='y - ', ztitle='z - ')

        box = vsl.box().alpha(0.1)
        self.show(vsl, box, ax, usage, hist, at=0, mode="image", zoom=zoom)
        self.show(volume, at=1, interactive=interactive)


########################################################################
class RayCastPlotter(Plotter):
    """
    Generate a ``Plotter`` window for Volume rendering using ray casting.

    Returns the ``Plotter`` object.

    .. hint:: examples/volumetric/app_raycaster.py
        .. image:: https://vedo.embl.es/images/advanced/app_raycaster.gif
    """
    def __init__(self, volume, **kwargs):

        Plotter.__init__(self, **kwargs)

        self.alphaslider0 = 0.33
        self.alphaslider1 = 0.66
        self.alphaslider2 = 1

        volumeProperty = volume.GetProperty()
        img = volume.imagedata()

        if volume.dimensions()[2]<3:
            vedo.logger.error("RayCastPlotter: not enough z slices.")
            raise RuntimeError

        smin, smax = img.GetScalarRange()
        x0alpha = smin + (smax - smin) * 0.25
        x1alpha = smin + (smax - smin) * 0.5
        x2alpha = smin + (smax - smin) * 1.0

        ############################## color map slider
        # Create transfer mapping scalar value to color
        cmaps = ["jet",
                 "viridis",
                 "bone",
                 "hot",
                 "plasma",
                 "winter",
                 "cool",
                 "gist_earth",
                 "coolwarm",
                 "tab10",
        ]
        cols_cmaps = []
        for cm in cmaps:
            cols = colorMap(range(0, 21), cm, 0, 20)  # sample 20 colors
            cols_cmaps.append(cols)
        Ncols = len(cmaps)
        csl = (0.9, 0.9, 0.9)
        if sum(getColor(self.renderer.GetBackground())) > 1.5:
            csl = (0.1, 0.1, 0.1)

        def sliderColorMap(widget, event):
            sliderRep = widget.GetRepresentation()
            k = int(sliderRep.GetValue())
            sliderRep.SetTitleText(cmaps[k])
            volume.color(cmaps[k])

        w1 = self.addSlider2D(
            sliderColorMap,
            0,
            Ncols - 1,
            value=0,
            showValue=0,
            title=cmaps[0],
            c=csl,
            pos=[(0.8, 0.05), (0.965, 0.05)],
        )
        w1.GetRepresentation().SetTitleHeight(0.018)

        ############################## alpha sliders
        # Create transfer mapping scalar value to opacity
        opacityTransferFunction = volumeProperty.GetScalarOpacity()

        def setOTF():
            opacityTransferFunction.RemoveAllPoints()
            opacityTransferFunction.AddPoint(smin, 0.0)
            opacityTransferFunction.AddPoint(smin + (smax - smin) * 0.1, 0.0)
            opacityTransferFunction.AddPoint(x0alpha, self.alphaslider0)
            opacityTransferFunction.AddPoint(x1alpha, self.alphaslider1)
            opacityTransferFunction.AddPoint(x2alpha, self.alphaslider2)

        setOTF()

        def sliderA0(widget, event):
            self.alphaslider0 = widget.GetRepresentation().GetValue()
            setOTF()

        self.addSlider2D(sliderA0, 0, 1,
                        value=self.alphaslider0,
                        pos=[(0.84, 0.1), (0.84, 0.26)],
                        c=csl, showValue=0)

        def sliderA1(widget, event):
            self.alphaslider1 = widget.GetRepresentation().GetValue()
            setOTF()

        self.addSlider2D(sliderA1, 0, 1,
                        value=self.alphaslider1,
                        pos=[(0.89, 0.1), (0.89, 0.26)],
                        c=csl, showValue=0)

        def sliderA2(widget, event):
            self.alphaslider2 = widget.GetRepresentation().GetValue()
            setOTF()

        w2 = self.addSlider2D(sliderA2, 0, 1,
                            value=self.alphaslider2,
                            pos=[(0.96, 0.1), (0.96, 0.26)],
                            c=csl, showValue=0,
                            title="Opacity levels")
        w2.GetRepresentation().SetTitleHeight(0.016)

        # add a button
        def buttonfuncMode():
            s = volume.mode()
            snew = (s + 1) % 2
            volume.mode(snew)
            bum.switch()

        bum = self.addButton(
            buttonfuncMode,
            pos=(0.7, 0.035),
            states=["composite", "max proj."],
            c=["bb", "gray"],
            bc=["gray", "bb"],  # colors of states
            font="",
            size=16,
            bold=0,
            italic=False,
        )
        bum.status(volume.mode())

        # add histogram of scalar
        plot = CornerHistogram(volume,
            bins=25, logscale=1, c=(.7,.7,.7), bg=(.7,.7,.7), pos=(0.78, 0.065),
            lines=True, dots=False,
            nmax=3.1415e+06, # subsample otherwise is too slow
        )

        plot.GetPosition2Coordinate().SetValue(0.197, 0.20, 0)
        plot.GetXAxisActor2D().SetFontFactor(0.7)
        plot.GetProperty().SetOpacity(0.5)
        self.add([plot, volume])


#####################################################################################
class IsosurfaceBrowser(Plotter):
    """
    Generate a ``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).

    Example:
        .. code-block:: python

            from vedo import dataurl, Volume
            from vedo.applications import IsosurfaceBrowser
            vol = Volume(dataurl+'head.vti')
            plt = IsosurfaceBrowser(vol, c='gold')
            plt.show(axes=7, bg2='lb')

    .. hint:: examples/volumetric/app_isobrowser.py
        .. image:: https://vedo.embl.es/images/advanced/app_isobrowser.gif
    """
    def __init__(
            self,
            volume,
            threshold=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,
        ):

        Plotter.__init__(self,
                         pos=pos,
                         bg=bg,
                         bg2=bg2,
                         size=size,
                         screensize=screensize,
                         title=title,
                         interactive=interactive,
                         axes=axes,
        )

        self._prev_value = 1e30

        scrange = volume.scalarRange()
        delta = scrange[1] - scrange[0]
        if not delta:
            return

        if lego:
            res = int(res/2)  # because lego is much slower
            slidertitle = ""
        else:
            slidertitle = "threshold"

        allowed_vals = np.linspace(scrange[0], scrange[1], num=res)

        bacts = dict()  # cache the meshes so we dont need to recompute
        if precompute:
            delayed = False  # no need to delay the slider in this case
            if progress:
                pb = vedo.ProgressBar(0,len(allowed_vals))

            for value in allowed_vals:
                value_name = precision(value, 2)
                if lego:
                    mesh = volume.legosurface(vmin=value)
                    if mesh.NCells():
                        mesh.cmap(cmap, vmin=scrange[0], vmax=scrange[1], on='cells')
                else:
                    mesh = volume.isosurface(threshold=value).color(c).alpha(alpha)
                bacts.update({value_name: mesh}) # store it
                if progress:
                    pb.print("isosurfacing volume..")

        ############################## threshold slider callback
        def sliderThres(widget, event):

            prevact = self.actors[0]
            if isinstance(widget, float):
                value = widget
            else:
                value =  widget.GetRepresentation().GetValue()

            # snap to the closest
            idx = (np.abs(allowed_vals - value)).argmin()
            value = allowed_vals[idx]

            if abs(value - self._prev_value)/delta < 0.001:
                return
            self._prev_value = value

            value_name = precision(value, 2)
            if value_name in bacts.keys():  # reusing the already existing mesh
                #print('reusing')
                mesh = bacts[value_name]
            else:                        # else generate it
                #print('generating', value)
                if lego:
                    mesh = volume.legosurface(vmin=value)
                    if mesh.NCells():
                        mesh.cmap(cmap, vmin=scrange[0], vmax=scrange[1], on='cells')
                else:
                    mesh = volume.isosurface(threshold=value).color(c).alpha(alpha)
                bacts.update({value_name: mesh}) # store it

            self.renderer.RemoveActor(prevact)
            self.renderer.AddActor(mesh)
            self.actors[0] = mesh
        ################################################

        if threshold is None:
            threshold = delta / 3.0 + scrange[0]

        self.actors = [None]
        sliderThres(threshold, "")  # init call
        if lego:
            self.actors[0].addScalarBar(pos=(0.8,0.12))

        self.addSlider2D(
            sliderThres,
            scrange[0] + 0.02 * delta,
            scrange[1] - 0.02 * delta,
            value=threshold,
            pos=sliderpos,
            title=slidertitle,
            showValue=True,
            delayed=delayed,
        )


##############################################################################
class Browser(Plotter):
    """
    Browse a serie of vedo objects by using a simple slider.

    Example:
        .. code-block:: python

            import vedo
            from vedo.applications import Browser
            meshes = vedo.load("data/2*0.vtk") # a python list
            plt = Browser(meshes, resetcam=1, axes=4) # a vedo.Plotter
            plt.show()

    .. hint:: examples/other/morphomatics_tube.py
    """
    def __init__(
            self,
            objects=(),
            sliderpos=((0.55, 0.07),(0.96, 0.07)),
            c=None,  # slider color
            prefix="",
            pos=(0, 0),
            size="auto",
            screensize="auto",
            title="Browser",
            bg="white",
            bg2=None,
            axes=4,
            resetcam=False,
            interactive=True,
        ):

        Plotter.__init__(
            self,
            pos=pos,
            size=size,
            screensize=screensize,
            title=title,
            bg=bg,
            bg2=bg2,
            axes=axes,
            interactive=interactive,
        )

        self += objects

        self.slider = None

        # define the slider
        def sliderfunc(widget, event=None):
            k = int(widget.GetRepresentation().GetValue())
            ak = self.actors[k]
            for a in self.actors:
                if a == ak:
                    a.on()
                else:
                    a.off()
            if resetcam:
                self.resetCamera()
            tx = str(k)
            if ak.filename:
                tx = ak.filename.split("/")[-1]
                tx = tx.split("\\")[-1] # windows os
            elif ak.name:
                tx = ak.name
            widget.GetRepresentation().SetTitleText(prefix+tx)

        self.slider = self.addSlider2D(sliderfunc, 0.5, len(objects)-0.5,
                                       pos=sliderpos, font='courier', c=c, showValue=False)
        self.slider.GetRepresentation().SetTitleHeight(0.020)
        sliderfunc(self.slider) # init call


#############################################################################################
class FreeHandCutPlotter(Plotter):
    """
    A ``Plotter`` derived class which edits polygonal meshes interactively.
    Can also be invoked from command line. E.g. 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

    Parameters
    ----------
    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.

    .. hint:: examples/basic/cutFreeHand.py
        .. image:: https://vedo.embl.es/images/basic/cutFreeHand.gif
    """
    # thanks to Jakub Kaminski for the original version of this script
    def __init__(
            self,
            mesh,
            splined=True,
            font="Bongas",
            alpha=0.9,
            lw=4,
            lc="red5",
            pc="red4",
            c="green3",
            tc="k9",
            tol=0.008,
            **options
        ):

        if not isinstance(mesh, Points):
            vedo.logger.error("FreeHandCutPlotter input must be Points or Mesh")
            raise RuntimeError()

        super().__init__(**options)

        self.mesh = mesh
        self.mesh_prev = mesh
        self.splined = splined
        self.linecolor = lc
        self.linewidth = lw
        self.pointcolor = pc
        self.color = c
        self.alpha = alpha

        self.msg  = "Right-click and move to draw line\n"
        self.msg += "Second right-click to stop drawing\n"
        self.msg += "Press L to extract largest surface\n"
        self.msg += "        z/Z to cut mesh (s to save)\n"
        self.msg += "        c to clear points, u to undo"
        self.txt2d = Text2D(self.msg, pos='top-left', font=font, s=0.9)
        self.txt2d.c(tc).background(c, alpha).frame()

        self.idkeypress = self.addCallback('KeyPress', self._onKeyPress)
        self.idrightclck = self.addCallback('RightButton', self._onRightClick)
        self.idmousemove = self.addCallback('MouseMove', self._onMouseMove)
        self.drawmode = False
        self.tol = tol       # tolerance of point distance
        self.cpoints = []
        self.points = None
        self.spline = None
        self.jline = None
        self.topline = None
        self.top_pts = []

    def init(self, initpoints):
        if isinstance(initpoints, Points):
            self.cpoints = initpoints.points()
        else:
            self.cpoints = np.array(initpoints)
        self.points = Points(self.cpoints, r=self.linewidth).c(self.pointcolor).pickable(0)
        if self.splined:
            self.spline = Spline(self.cpoints, res=len(self.cpoints)*4)
        else:
            self.spline = Line(self.cpoints)
        self.spline.lw(self.linewidth).c(self.linecolor).pickable(False)
        self.jline = Line(self.cpoints[0], self.cpoints[-1], lw=1, c=self.linecolor).pickable(0)
        self.add([self.points, self.spline, self.jline], render=False)
        return self

    def _onRightClick(self, evt):
        self.drawmode = not self.drawmode # toggle mode
        if self.drawmode:
            self.txt2d.background(self.linecolor, self.alpha)
        else:
            self.txt2d.background(self.color, self.alpha)
            if len(self.cpoints) > 2:
                self.remove([self.spline, self.jline])
                if self.splined: # show the spline closed
                    self.spline = Spline(self.cpoints, closed=True, res=len(self.cpoints)*4)
                else:
                    self.spline = Line(self.cpoints, closed=True)
                self.spline.lw(self.linewidth).c(self.linecolor).pickable(False)
                self.add(self.spline)

    def _onMouseMove(self, evt):
        if self.drawmode:
            cpt = self.computeWorldPosition(evt.picked2d) # make this 2d-screen point 3d
            if self.cpoints and mag(cpt - self.cpoints[-1]) < self.mesh.diagonalSize()*self.tol:
                return  # new point is too close to the last one. skip
            self.cpoints.append(cpt)
            if len(self.cpoints) > 2:
                self.remove([self.points, self.spline, self.jline, self.topline])
                self.points = Points(self.cpoints, r=self.linewidth).c(self.pointcolor).pickable(0)
                if self.splined:
                    self.spline = Spline(self.cpoints, res=len(self.cpoints)*4) # not closed here
                else:
                    self.spline = Line(self.cpoints)

                if evt.actor:
                    self.top_pts.append(evt.picked3d)
                    # self.topline = Line(self.top_pts)
                    # self.topline.lw(self.linewidth-1).c(self.linecolor).pickable(False)
                    self.topline = Points(self.top_pts, r=self.linewidth)
                    self.topline.c(self.linecolor).pickable(False)

                self.spline.lw(self.linewidth).c(self.linecolor).pickable(False)
                self.txt2d.background(self.linecolor)
                self.jline = Line(self.cpoints[0], self.cpoints[-1], lw=1, c=self.linecolor).pickable(0)
                self.add([self.points, self.spline, self.jline, self.topline])

    def _onKeyPress(self, evt):
        if evt.keyPressed.lower() == 'z' and self.spline: # Cut mesh with a ribbon-like surface
            inv = False
            if evt.keyPressed == 'Z':
                inv = True
            self.txt2d.background('red8').text("  ... working ...  ")
            self.render()
            self.mesh_prev = self.mesh.clone()
            tol = self.mesh.diagonalSize()/2            # size of ribbon (not shown)
            pts = self.spline.points()
            n = fitPlane(pts, signed=True).normal       # compute normal vector to points
            rb = Ribbon(pts - tol*n, pts + tol*n, closed=True)
            self.mesh.cutWithMesh(rb, invert=inv)       # CUT
            self.txt2d.text(self.msg)                   # put back original message
            if self.drawmode:
                self._onRightClick(evt)                 # toggle mode to normal
            else:
                self.txt2d.background(self.color, self.alpha)
            self.remove([self.spline, self.points, self.jline, self.topline]).render()
            self.cpoints, self.points, self.spline = [], None, None
            self.top_pts, self.topline = [], None

        elif evt.keyPressed == 'L':
            self.txt2d.background('red8')
            self.txt2d.text(" ... removing smaller ... \n ... parts of the mesh ... ")
            self.render()
            self.remove(self.mesh)
            self.mesh_prev = self.mesh
            mcut = self.mesh.extractLargestRegion()
            mcut.filename = self.mesh.filename          # copy over various properties
            mcut.name = self.mesh.name
            mcut.scalarbar= self.mesh.scalarbar
            mcut.info = self.mesh.info
            self.mesh = mcut                            # discard old mesh by overwriting it
            self.txt2d.text(self.msg).background(self.color)   # put back original message
            self.add(mcut)

        elif evt.keyPressed == 'u':                     # Undo last action
            if self.drawmode:
                self._onRightClick(evt)                 # toggle mode to normal
            else:
                self.txt2d.background(self.color, self.alpha)
            self.remove([self.mesh, self.spline, self.jline, self.points, self.topline])
            self.mesh = self.mesh_prev
            self.cpoints, self.points, self.spline = [], None, None
            self.top_pts, self.topline = [], None
            self.add(self.mesh)

        elif evt.keyPressed == 'c' or evt.keyPressed == 'Delete':
            # clear all points
            self.remove([self.spline, self.points, self.jline, self.topline]).render()
            self.cpoints, self.points, self.spline = [], None, None
            self.top_pts, self.topline = [], None

        elif evt.keyPressed == 'r': # reset camera and axes
            try:
                self.remove(self.axes_instances[0])
                self.axes_instances[0] = None
                self.addGlobalAxes(axtype=1, c=None)
                self.renderer.ResetCamera()
                self.interactor.Render()
            except:
                pass

        elif evt.keyPressed == 's':
            if self.mesh.filename:
                fname = os.path.basename(self.mesh.filename)
                fname, extension = os.path.splitext(fname)
                fname = fname.replace("_edited","")
                fname = f"{fname}_edited{extension}"
            else:
                fname="mesh_edited.vtk"
            self.write(fname)

    def write(self, filename="mesh_edited.vtk"):
        """Save the resulting mesh to file"""
        self.mesh.write(filename)
        vedo.logger.info(f"\save saved to file {filename}")
        return self

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


########################################################################
class Animation(Plotter):
    """
    A ``Plotter`` derived class that allows to animate simultaneously various objects
    by specifying event times and durations of different visual effects.

    Parameters
    ----------
    totalDuration : float
        expand or shrink the total duration of video to this value

    timeResolution : float
        in seconds, save a frame at this rate

    showProgressBar : bool
        whether to show a progress bar or not

    videoFileName : str
        output file name of the video

    videoFPS : int
        desired value of the nr of frames per second

    .. warning:: this is still an experimental feature at the moment.
    """
    def __init__(self, totalDuration=None, timeResolution=0.02, showProgressBar=True,
                 videoFileName='animation.mp4', videoFPS=12):
        Plotter.__init__(self)
        self.resetcam = True

        self.events = []
        self.timeResolution = timeResolution
        self.totalDuration = totalDuration
        self.showProgressBar = showProgressBar
        self.videoFileName = videoFileName
        self.videoFPS = videoFPS
        self.bookingMode = True
        self._inputvalues = []
        self._performers = []
        self._lastT = None
        self._lastDuration = None
        self._lastActs = None
        self.eps = 0.00001


    def _parse(self, objs, t, duration):
        if t is None:
            if self._lastT:
                t = self._lastT
            else:
                t = 0.0
        if duration is None:
            if self._lastDuration:
                duration = self._lastDuration
            else:
                duration = 0.0
        if objs is None:
            if self._lastActs:
                objs = self._lastActs
            else:
                vedo.logger.error("Need to specify actors!")
                raise RuntimeError

        objs2 = objs

        if isSequence(objs):
            objs2 = objs
        else:
            objs2 = [objs]

        #quantize time steps and duration
        t = int(t/self.timeResolution+0.5)*self.timeResolution
        nsteps =   int(duration/self.timeResolution+0.5)
        duration = nsteps*self.timeResolution

        rng = np.linspace(t, t+duration, nsteps+1)

        self._lastT = t
        self._lastDuration = duration
        self._lastActs = objs2

        for a in objs2:
            if a not in self.actors:
                self.actors.append(a)

        return objs2, t, duration, rng


    def switchOn(self, acts=None, t=None, duration=None):
        """Switch on the input list of meshes."""
        return self.fadeIn(acts, t, 0)

    def switchOff(self, acts=None, t=None, duration=None):
        """Switch off the input list of meshes."""
        return self.fadeOut(acts, t, 0)


    def fadeIn(self, acts=None, t=None, duration=None):
        """Gradually switch on the input list of meshes by increasing opacity."""
        if self.bookingMode:
            acts, t, duration, rng = self._parse(acts, t, duration)
            for tt in rng:
                alpha = linInterpolate(tt, [t,t+duration], [0,1])
                self.events.append((tt, self.fadeIn, acts, alpha))
        else:
            for a in self._performers:
                if hasattr(a, 'alpha'):
                    if a.alpha() >= self._inputvalues:
                        continue
                    a.alpha(self._inputvalues)
        return self

    def fadeOut(self, acts=None, t=None, duration=None):
        """Gradually switch off the input list of meshes by increasing transparency."""
        if self.bookingMode:
            acts, t, duration, rng = self._parse(acts, t, duration)
            for tt in rng:
                alpha = linInterpolate(tt, [t,t+duration], [1,0])
                self.events.append((tt, self.fadeOut, acts, alpha))
        else:
            for a in self._performers:
                if a.alpha() <= self._inputvalues:
                    continue
                a.alpha(self._inputvalues)
        return self


    def changeAlphaBetween(self, alpha1, alpha2, acts=None, t=None, duration=None):
        """Gradually change transparency for the input list of meshes."""
        if self.bookingMode:
            acts, t, duration, rng = self._parse(acts, t, duration)
            for tt in rng:
                alpha = linInterpolate(tt, [t,t+duration], [alpha1, alpha2])
                self.events.append((tt, self.fadeOut, acts, alpha))
        else:
            for a in self._performers:
                a.alpha(self._inputvalues)
        return self


    def changeColor(self,  c, acts=None, t=None, duration=None):
        """Gradually change color for the input list of meshes."""
        if self.bookingMode:
            acts, t, duration, rng = self._parse(acts, t, duration)

            col2 = getColor(c)
            for tt in rng:
                inputvalues = []
                for a in acts:
                    col1 = a.color()
                    r = linInterpolate(tt, [t,t+duration], [col1[0], col2[0]])
                    g = linInterpolate(tt, [t,t+duration], [col1[1], col2[1]])
                    b = linInterpolate(tt, [t,t+duration], [col1[2], col2[2]])
                    inputvalues.append((r,g,b))
                self.events.append((tt, self.changeColor, acts, inputvalues))
        else:
            for i,a in enumerate(self._performers):
                a.color(self._inputvalues[i])
        return self


    def changeBackColor(self, c, acts=None, t=None, duration=None):
        """Gradually change backface color for the input list of meshes.
        An initial backface color should be set in advance."""
        if self.bookingMode:
            acts, t, duration, rng = self._parse(acts, t, duration)

            col2 = getColor(c)
            for tt in rng:
                inputvalues = []
                for a in acts:
                    if a.GetBackfaceProperty():
                        col1 = a.backColor()
                        r = linInterpolate(tt, [t,t+duration], [col1[0], col2[0]])
                        g = linInterpolate(tt, [t,t+duration], [col1[1], col2[1]])
                        b = linInterpolate(tt, [t,t+duration], [col1[2], col2[2]])
                        inputvalues.append((r,g,b))
                    else:
                        inputvalues.append(None)
                self.events.append((tt, self.changeBackColor, acts, inputvalues))
        else:
            for i,a in enumerate(self._performers):
                a.backColor(self._inputvalues[i])
        return self


    def changeToWireframe(self, acts=None, t=None):
        """Switch representation to wireframe for the input list of meshes at time `t`."""
        if self.bookingMode:
            acts, t, duration, rng = self._parse(acts, t, None)
            self.events.append((t, self.changeToWireframe, acts, True))
        else:
            for a in self._performers:
                a.wireframe(self._inputvalues)
        return self

    def changeToSurface(self, acts=None, t=None):
        """Switch representation to surface for the input list of meshes at time `t`."""
        if self.bookingMode:
            acts, t, duration, rng = self._parse(acts, t, None)
            self.events.append((t, self.changeToSurface, acts, False))
        else:
            for a in self._performers:
                a.wireframe(self._inputvalues)
        return self


    def changeLineWidth(self, lw, acts=None, t=None, duration=None):
        """Gradually change line width of the mesh edges for the input list of meshes."""
        if self.bookingMode:
            acts, t, duration, rng = self._parse(acts, t, duration)
            for tt in rng:
                inputvalues = []
                for a in acts:
                    newlw = linInterpolate(tt, [t,t+duration], [a.lw(), lw])
                    inputvalues.append(newlw)
                self.events.append((tt, self.changeLineWidth, acts, inputvalues))
        else:
            for i,a in enumerate(self._performers):
                a.lw(self._inputvalues[i])
        return self


    def changeLineColor(self, c, acts=None, t=None, duration=None):
        """Gradually change line color of the mesh edges for the input list of meshes."""
        if self.bookingMode:
            acts, t, duration, rng = self._parse(acts, t, duration)
            col2 = getColor(c)
            for tt in rng:
                inputvalues = []
                for a in acts:
                    col1 = a.lineColor()
                    r = linInterpolate(tt, [t,t+duration], [col1[0], col2[0]])
                    g = linInterpolate(tt, [t,t+duration], [col1[1], col2[1]])
                    b = linInterpolate(tt, [t,t+duration], [col1[2], col2[2]])
                    inputvalues.append((r,g,b))
                self.events.append((tt, self.changeLineColor, acts, inputvalues))
        else:
            for i,a in enumerate(self._performers):
                a.lineColor(self._inputvalues[i])
        return self


    def changeLighting(self, style, acts=None, t=None, duration=None):
        """Gradually change the lighting style for the input list of meshes.

        Allowed styles are: [metallic, plastic, shiny, glossy, default].
        """
        if self.bookingMode:
            acts, t, duration, rng = self._parse(acts, t, duration)

            c = (1,1,0.99)
            if   style=='metallic': pars = [0.1, 0.3, 1.0, 10, c]
            elif style=='plastic' : pars = [0.3, 0.4, 0.3,  5, c]
            elif style=='shiny'   : pars = [0.2, 0.6, 0.8, 50, c]
            elif style=='glossy'  : pars = [0.1, 0.7, 0.9, 90, c]
            elif style=='default' : pars = [0.1, 1.0, 0.05, 5, c]
            else:
                vedo.logger.error(f"Unknown lighting style {style}")

            for tt in rng:
                inputvalues = []
                for a in acts:
                    pr = a.GetProperty()
                    aa = pr.GetAmbient()
                    ad = pr.GetDiffuse()
                    asp = pr.GetSpecular()
                    aspp = pr.GetSpecularPower()
                    naa  = linInterpolate(tt, [t,t+duration], [aa,  pars[0]])
                    nad  = linInterpolate(tt, [t,t+duration], [ad,  pars[1]])
                    nasp = linInterpolate(tt, [t,t+duration], [asp, pars[2]])
                    naspp= linInterpolate(tt, [t,t+duration], [aspp,pars[3]])
                    inputvalues.append((naa, nad, nasp, naspp))
                self.events.append((tt, self.changeLighting, acts, inputvalues))
        else:
            for i,a in enumerate(self._performers):
                pr = a.GetProperty()
                vals = self._inputvalues[i]
                pr.SetAmbient(vals[0])
                pr.SetDiffuse(vals[1])
                pr.SetSpecular(vals[2])
                pr.SetSpecularPower(vals[3])
        return self


    def move(self, act=None, pt=(0,0,0), t=None, duration=None, style='linear'):
        """Smoothly change the position of a specific object to a new point in space."""
        if self.bookingMode:
            acts, t, duration, rng = self._parse(act, t, duration)
            if len(acts) != 1:
                vedo.logger.error("in move(), can move only one object.")
            cpos = acts[0].pos()
            pt = np.array(pt)
            dv = (pt - cpos)/len(rng)
            for j,tt in enumerate(rng):
                i = j+1
                if 'quad' in style:
                    x = i/len(rng)
                    y = x*x
                    self.events.append((tt, self.move, acts, cpos+dv*i*y))
                else:
                    self.events.append((tt, self.move, acts, cpos+dv*i))
        else:
            self._performers[0].pos(self._inputvalues)
        return self


    def rotate(self, act=None, axis=(1,0,0), angle=0, t=None, duration=None):
        """Smoothly rotate a specific object by a specified angle and axis."""
        if self.bookingMode:
            acts, t, duration, rng = self._parse(act, t, duration)
            if len(acts) != 1:
                vedo.logger.error("in rotate(), can move only one object.")
            for tt in rng:
                ang = angle/len(rng)
                self.events.append((tt, self.rotate, acts, (axis, ang)))
        else:
            ax = self._inputvalues[0]
            if   ax == 'x':
                self._performers[0].rotateX(self._inputvalues[1])
            elif ax == 'y':
                self._performers[0].rotateY(self._inputvalues[1])
            elif ax == 'z':
                self._performers[0].rotateZ(self._inputvalues[1])
        return self


    def scale(self, acts=None, factor=1, t=None, duration=None):
        """Smoothly scale a specific object to a specified scale factor."""
        if self.bookingMode:
            acts, t, duration, rng = self._parse(acts, t, duration)
            for tt in rng:
                fac = linInterpolate(tt, [t,t+duration], [1, factor])
                self.events.append((tt, self.scale, acts, fac))
        else:
            for a in self._performers:
                a.scale(self._inputvalues)
        return self


    def meshErode(self, act=None, corner=6, t=None, duration=None):
        """Erode a mesh by removing cells that are close to one of the 8 corners
        of the bounding box.
        """
        if self.bookingMode:
            acts, t, duration, rng = self._parse(act, t, duration)
            if len(acts) != 1:
                vedo.logger.error("in meshErode(), can erode only one object.")
            diag = acts[0].diagonalSize()
            x0,x1, y0,y1, z0,z1 = acts[0].GetBounds()
            corners = [ (x0,y0,z0), (x1,y0,z0), (x1,y1,z0), (x0,y1,z0),
                        (x0,y0,z1), (x1,y0,z1), (x1,y1,z1), (x0,y1,z1) ]
            pcl = acts[0].closestPoint(corners[corner])
            dmin = np.linalg.norm(pcl - corners[corner])
            for tt in rng:
                d = linInterpolate(tt, [t,t+duration], [dmin, diag*1.01])
                if d>0:
                    ids = acts[0].closestPoint(corners[corner],
                                               radius=d, returnPointId=True)
                    if len(ids) <= acts[0].N():
                        self.events.append((tt, self.meshErode, acts, ids))
        # else:
        #     self._performers[0].deletePoints(self._inputvalues)
        return self


    def moveCamera(self, camstart=None, camstop=None, t=None, duration=None):
        """
        Smoothly move camera between two ``vtkCamera`` objects.
        """
        if self.bookingMode:
            if camstart is None:
                if not self.camera:
                    vedo.logger.error("in moveCamera(), no camera exist, skip.")
                    return self
                camstart = self.camera
            acts, t, duration, rng = self._parse(None, t, duration)
            p1 = np.array(camstart.GetPosition())
            f1 = np.array(camstart.GetFocalPoint())
            v1 = np.array(camstart.GetViewUp())
            c1 = np.array(camstart.GetClippingRange())
            s1 = camstart.GetDistance()

            p2 = np.array(camstop.GetPosition())
            f2 = np.array(camstop.GetFocalPoint())
            v2 = np.array(camstop.GetViewUp())
            c2 = np.array(camstop.GetClippingRange())
            s2 = camstop.GetDistance()
            for tt in rng:
                np1 = linInterpolate(tt, [t,t+duration], [p1,p2])
                nf1 = linInterpolate(tt, [t,t+duration], [f1,f2])
                nv1 = linInterpolate(tt, [t,t+duration], [v1,v2])
                nc1 = linInterpolate(tt, [t,t+duration], [c1,c2])
                ns1 = linInterpolate(tt, [t,t+duration], [s1,s2])
                inps = (np1, nf1, nv1, nc1, ns1)
                self.events.append((tt, self.moveCamera, acts, inps))
        else:
            if not self.camera:
                return self
            np1, nf1, nv1, nc1, ns1 = self._inputvalues
            self.camera.SetPosition(np1)
            self.camera.SetFocalPoint(nf1)
            self.camera.SetViewUp(nv1)
            self.camera.SetClippingRange(nc1)
            self.camera.SetDistance(ns1)
        return self


    def play(self):
        """Play the internal list of events and save a video."""

        self.events = sorted(self.events, key=lambda x: x[0])
        self.bookingMode = False

        if self.showProgressBar:
            pb = vedo.ProgressBar(0, len(self.events), c='g')

        if self.totalDuration is None:
            self.totalDuration = self.events[-1][0] - self.events[0][0]

        if self.videoFileName:
            vd = vedo.Video(self.videoFileName, fps=self.videoFPS, duration=self.totalDuration)

        ttlast=0
        for e in self.events:

            tt, action, self._performers, self._inputvalues = e
            action(0,0)

            dt = tt-ttlast
            if dt > self.eps:
                self.show(interactive=False, resetcam=self.resetcam)
                if self.videoFileName: vd.addFrame()

                if dt > self.timeResolution+self.eps:
                    if self.videoFileName: vd.pause(dt)

            ttlast = tt

            if self.showProgressBar:
                pb.print('t='+str(int(tt*100)/100)+'s,  '+action.__name__)

        self.show(interactive=False, resetcam=self.resetcam)
        if self.videoFileName:
            vd.addFrame()
            vd.close()

        self.show(interactive=True, resetcam=self.resetcam)
        self.bookingMode = True

Classes

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

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

Example

.. code-block:: python

import vedo
from vedo.applications import Browser
meshes = vedo.load("data/2*0.vtk") # a python list
plt = Browser(meshes, resetcam=1, axes=4) # a vedo.Plotter
plt.show()

Hint: examples/other/morphomatics_tube.py

Expand source code
class Browser(Plotter):
    """
    Browse a serie of vedo objects by using a simple slider.

    Example:
        .. code-block:: python

            import vedo
            from vedo.applications import Browser
            meshes = vedo.load("data/2*0.vtk") # a python list
            plt = Browser(meshes, resetcam=1, axes=4) # a vedo.Plotter
            plt.show()

    .. hint:: examples/other/morphomatics_tube.py
    """
    def __init__(
            self,
            objects=(),
            sliderpos=((0.55, 0.07),(0.96, 0.07)),
            c=None,  # slider color
            prefix="",
            pos=(0, 0),
            size="auto",
            screensize="auto",
            title="Browser",
            bg="white",
            bg2=None,
            axes=4,
            resetcam=False,
            interactive=True,
        ):

        Plotter.__init__(
            self,
            pos=pos,
            size=size,
            screensize=screensize,
            title=title,
            bg=bg,
            bg2=bg2,
            axes=axes,
            interactive=interactive,
        )

        self += objects

        self.slider = None

        # define the slider
        def sliderfunc(widget, event=None):
            k = int(widget.GetRepresentation().GetValue())
            ak = self.actors[k]
            for a in self.actors:
                if a == ak:
                    a.on()
                else:
                    a.off()
            if resetcam:
                self.resetCamera()
            tx = str(k)
            if ak.filename:
                tx = ak.filename.split("/")[-1]
                tx = tx.split("\\")[-1] # windows os
            elif ak.name:
                tx = ak.name
            widget.GetRepresentation().SetTitleText(prefix+tx)

        self.slider = self.addSlider2D(sliderfunc, 0.5, len(objects)-0.5,
                                       pos=sliderpos, font='courier', c=c, showValue=False)
        self.slider.GetRepresentation().SetTitleHeight(0.020)
        sliderfunc(self.slider) # init call

Ancestors

Inherited members

class FreeHandCutPlotter (mesh, splined=True, font='Bongas', alpha=0.9, lw=4, lc='red5', pc='red4', c='green3', tc='k9', tol=0.008, **options)

A Plotter derived class which edits polygonal meshes interactively. Can also be invoked from command line. E.g. 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

Parameters

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.

Hint: examples/basic/cutFreeHand.py

Expand source code
class FreeHandCutPlotter(Plotter):
    """
    A ``Plotter`` derived class which edits polygonal meshes interactively.
    Can also be invoked from command line. E.g. 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

    Parameters
    ----------
    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.

    .. hint:: examples/basic/cutFreeHand.py
        .. image:: https://vedo.embl.es/images/basic/cutFreeHand.gif
    """
    # thanks to Jakub Kaminski for the original version of this script
    def __init__(
            self,
            mesh,
            splined=True,
            font="Bongas",
            alpha=0.9,
            lw=4,
            lc="red5",
            pc="red4",
            c="green3",
            tc="k9",
            tol=0.008,
            **options
        ):

        if not isinstance(mesh, Points):
            vedo.logger.error("FreeHandCutPlotter input must be Points or Mesh")
            raise RuntimeError()

        super().__init__(**options)

        self.mesh = mesh
        self.mesh_prev = mesh
        self.splined = splined
        self.linecolor = lc
        self.linewidth = lw
        self.pointcolor = pc
        self.color = c
        self.alpha = alpha

        self.msg  = "Right-click and move to draw line\n"
        self.msg += "Second right-click to stop drawing\n"
        self.msg += "Press L to extract largest surface\n"
        self.msg += "        z/Z to cut mesh (s to save)\n"
        self.msg += "        c to clear points, u to undo"
        self.txt2d = Text2D(self.msg, pos='top-left', font=font, s=0.9)
        self.txt2d.c(tc).background(c, alpha).frame()

        self.idkeypress = self.addCallback('KeyPress', self._onKeyPress)
        self.idrightclck = self.addCallback('RightButton', self._onRightClick)
        self.idmousemove = self.addCallback('MouseMove', self._onMouseMove)
        self.drawmode = False
        self.tol = tol       # tolerance of point distance
        self.cpoints = []
        self.points = None
        self.spline = None
        self.jline = None
        self.topline = None
        self.top_pts = []

    def init(self, initpoints):
        if isinstance(initpoints, Points):
            self.cpoints = initpoints.points()
        else:
            self.cpoints = np.array(initpoints)
        self.points = Points(self.cpoints, r=self.linewidth).c(self.pointcolor).pickable(0)
        if self.splined:
            self.spline = Spline(self.cpoints, res=len(self.cpoints)*4)
        else:
            self.spline = Line(self.cpoints)
        self.spline.lw(self.linewidth).c(self.linecolor).pickable(False)
        self.jline = Line(self.cpoints[0], self.cpoints[-1], lw=1, c=self.linecolor).pickable(0)
        self.add([self.points, self.spline, self.jline], render=False)
        return self

    def _onRightClick(self, evt):
        self.drawmode = not self.drawmode # toggle mode
        if self.drawmode:
            self.txt2d.background(self.linecolor, self.alpha)
        else:
            self.txt2d.background(self.color, self.alpha)
            if len(self.cpoints) > 2:
                self.remove([self.spline, self.jline])
                if self.splined: # show the spline closed
                    self.spline = Spline(self.cpoints, closed=True, res=len(self.cpoints)*4)
                else:
                    self.spline = Line(self.cpoints, closed=True)
                self.spline.lw(self.linewidth).c(self.linecolor).pickable(False)
                self.add(self.spline)

    def _onMouseMove(self, evt):
        if self.drawmode:
            cpt = self.computeWorldPosition(evt.picked2d) # make this 2d-screen point 3d
            if self.cpoints and mag(cpt - self.cpoints[-1]) < self.mesh.diagonalSize()*self.tol:
                return  # new point is too close to the last one. skip
            self.cpoints.append(cpt)
            if len(self.cpoints) > 2:
                self.remove([self.points, self.spline, self.jline, self.topline])
                self.points = Points(self.cpoints, r=self.linewidth).c(self.pointcolor).pickable(0)
                if self.splined:
                    self.spline = Spline(self.cpoints, res=len(self.cpoints)*4) # not closed here
                else:
                    self.spline = Line(self.cpoints)

                if evt.actor:
                    self.top_pts.append(evt.picked3d)
                    # self.topline = Line(self.top_pts)
                    # self.topline.lw(self.linewidth-1).c(self.linecolor).pickable(False)
                    self.topline = Points(self.top_pts, r=self.linewidth)
                    self.topline.c(self.linecolor).pickable(False)

                self.spline.lw(self.linewidth).c(self.linecolor).pickable(False)
                self.txt2d.background(self.linecolor)
                self.jline = Line(self.cpoints[0], self.cpoints[-1], lw=1, c=self.linecolor).pickable(0)
                self.add([self.points, self.spline, self.jline, self.topline])

    def _onKeyPress(self, evt):
        if evt.keyPressed.lower() == 'z' and self.spline: # Cut mesh with a ribbon-like surface
            inv = False
            if evt.keyPressed == 'Z':
                inv = True
            self.txt2d.background('red8').text("  ... working ...  ")
            self.render()
            self.mesh_prev = self.mesh.clone()
            tol = self.mesh.diagonalSize()/2            # size of ribbon (not shown)
            pts = self.spline.points()
            n = fitPlane(pts, signed=True).normal       # compute normal vector to points
            rb = Ribbon(pts - tol*n, pts + tol*n, closed=True)
            self.mesh.cutWithMesh(rb, invert=inv)       # CUT
            self.txt2d.text(self.msg)                   # put back original message
            if self.drawmode:
                self._onRightClick(evt)                 # toggle mode to normal
            else:
                self.txt2d.background(self.color, self.alpha)
            self.remove([self.spline, self.points, self.jline, self.topline]).render()
            self.cpoints, self.points, self.spline = [], None, None
            self.top_pts, self.topline = [], None

        elif evt.keyPressed == 'L':
            self.txt2d.background('red8')
            self.txt2d.text(" ... removing smaller ... \n ... parts of the mesh ... ")
            self.render()
            self.remove(self.mesh)
            self.mesh_prev = self.mesh
            mcut = self.mesh.extractLargestRegion()
            mcut.filename = self.mesh.filename          # copy over various properties
            mcut.name = self.mesh.name
            mcut.scalarbar= self.mesh.scalarbar
            mcut.info = self.mesh.info
            self.mesh = mcut                            # discard old mesh by overwriting it
            self.txt2d.text(self.msg).background(self.color)   # put back original message
            self.add(mcut)

        elif evt.keyPressed == 'u':                     # Undo last action
            if self.drawmode:
                self._onRightClick(evt)                 # toggle mode to normal
            else:
                self.txt2d.background(self.color, self.alpha)
            self.remove([self.mesh, self.spline, self.jline, self.points, self.topline])
            self.mesh = self.mesh_prev
            self.cpoints, self.points, self.spline = [], None, None
            self.top_pts, self.topline = [], None
            self.add(self.mesh)

        elif evt.keyPressed == 'c' or evt.keyPressed == 'Delete':
            # clear all points
            self.remove([self.spline, self.points, self.jline, self.topline]).render()
            self.cpoints, self.points, self.spline = [], None, None
            self.top_pts, self.topline = [], None

        elif evt.keyPressed == 'r': # reset camera and axes
            try:
                self.remove(self.axes_instances[0])
                self.axes_instances[0] = None
                self.addGlobalAxes(axtype=1, c=None)
                self.renderer.ResetCamera()
                self.interactor.Render()
            except:
                pass

        elif evt.keyPressed == 's':
            if self.mesh.filename:
                fname = os.path.basename(self.mesh.filename)
                fname, extension = os.path.splitext(fname)
                fname = fname.replace("_edited","")
                fname = f"{fname}_edited{extension}"
            else:
                fname="mesh_edited.vtk"
            self.write(fname)

    def write(self, filename="mesh_edited.vtk"):
        """Save the resulting mesh to file"""
        self.mesh.write(filename)
        vedo.logger.info(f"\save saved to file {filename}")
        return self

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

Ancestors

Methods

def init(self, initpoints)
Expand source code
def init(self, initpoints):
    if isinstance(initpoints, Points):
        self.cpoints = initpoints.points()
    else:
        self.cpoints = np.array(initpoints)
    self.points = Points(self.cpoints, r=self.linewidth).c(self.pointcolor).pickable(0)
    if self.splined:
        self.spline = Spline(self.cpoints, res=len(self.cpoints)*4)
    else:
        self.spline = Line(self.cpoints)
    self.spline.lw(self.linewidth).c(self.linecolor).pickable(False)
    self.jline = Line(self.cpoints[0], self.cpoints[-1], lw=1, c=self.linecolor).pickable(0)
    self.add([self.points, self.spline, self.jline], render=False)
    return self
def start(self, *args, **kwargs)

Start window interaction (with mouse and keyboard)

Expand source code
def start(self, *args, **kwargs):
    """Start window interaction (with mouse and keyboard)"""
    acts = [self.txt2d, self.mesh, self.points, self.spline, self.jline]
    self.show(acts + list(args), **kwargs)
    return self
def write(self, filename='mesh_edited.vtk')

Save the resulting mesh to file

Expand source code
def write(self, filename="mesh_edited.vtk"):
    """Save the resulting mesh to file"""
    self.mesh.write(filename)
    vedo.logger.info(f"\save saved to file {filename}")
    return self

Inherited members

class IsosurfaceBrowser (volume, threshold=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)

Generate a 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).

Example

.. code-block:: python

from vedo import dataurl, Volume
from vedo.applications import IsosurfaceBrowser
vol = Volume(dataurl+'head.vti')
plt = IsosurfaceBrowser(vol, c='gold')
plt.show(axes=7, bg2='lb')

Hint: examples/volumetric/app_isobrowser.py

Expand source code
class IsosurfaceBrowser(Plotter):
    """
    Generate a ``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).

    Example:
        .. code-block:: python

            from vedo import dataurl, Volume
            from vedo.applications import IsosurfaceBrowser
            vol = Volume(dataurl+'head.vti')
            plt = IsosurfaceBrowser(vol, c='gold')
            plt.show(axes=7, bg2='lb')

    .. hint:: examples/volumetric/app_isobrowser.py
        .. image:: https://vedo.embl.es/images/advanced/app_isobrowser.gif
    """
    def __init__(
            self,
            volume,
            threshold=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,
        ):

        Plotter.__init__(self,
                         pos=pos,
                         bg=bg,
                         bg2=bg2,
                         size=size,
                         screensize=screensize,
                         title=title,
                         interactive=interactive,
                         axes=axes,
        )

        self._prev_value = 1e30

        scrange = volume.scalarRange()
        delta = scrange[1] - scrange[0]
        if not delta:
            return

        if lego:
            res = int(res/2)  # because lego is much slower
            slidertitle = ""
        else:
            slidertitle = "threshold"

        allowed_vals = np.linspace(scrange[0], scrange[1], num=res)

        bacts = dict()  # cache the meshes so we dont need to recompute
        if precompute:
            delayed = False  # no need to delay the slider in this case
            if progress:
                pb = vedo.ProgressBar(0,len(allowed_vals))

            for value in allowed_vals:
                value_name = precision(value, 2)
                if lego:
                    mesh = volume.legosurface(vmin=value)
                    if mesh.NCells():
                        mesh.cmap(cmap, vmin=scrange[0], vmax=scrange[1], on='cells')
                else:
                    mesh = volume.isosurface(threshold=value).color(c).alpha(alpha)
                bacts.update({value_name: mesh}) # store it
                if progress:
                    pb.print("isosurfacing volume..")

        ############################## threshold slider callback
        def sliderThres(widget, event):

            prevact = self.actors[0]
            if isinstance(widget, float):
                value = widget
            else:
                value =  widget.GetRepresentation().GetValue()

            # snap to the closest
            idx = (np.abs(allowed_vals - value)).argmin()
            value = allowed_vals[idx]

            if abs(value - self._prev_value)/delta < 0.001:
                return
            self._prev_value = value

            value_name = precision(value, 2)
            if value_name in bacts.keys():  # reusing the already existing mesh
                #print('reusing')
                mesh = bacts[value_name]
            else:                        # else generate it
                #print('generating', value)
                if lego:
                    mesh = volume.legosurface(vmin=value)
                    if mesh.NCells():
                        mesh.cmap(cmap, vmin=scrange[0], vmax=scrange[1], on='cells')
                else:
                    mesh = volume.isosurface(threshold=value).color(c).alpha(alpha)
                bacts.update({value_name: mesh}) # store it

            self.renderer.RemoveActor(prevact)
            self.renderer.AddActor(mesh)
            self.actors[0] = mesh
        ################################################

        if threshold is None:
            threshold = delta / 3.0 + scrange[0]

        self.actors = [None]
        sliderThres(threshold, "")  # init call
        if lego:
            self.actors[0].addScalarBar(pos=(0.8,0.12))

        self.addSlider2D(
            sliderThres,
            scrange[0] + 0.02 * delta,
            scrange[1] - 0.02 * delta,
            value=threshold,
            pos=sliderpos,
            title=slidertitle,
            showValue=True,
            delayed=delayed,
        )

Ancestors

Inherited members

class RayCastPlotter (volume, **kwargs)

Generate a Plotter window for Volume rendering using ray casting.

Returns the Plotter object.

Hint: examples/volumetric/app_raycaster.py

Expand source code
class RayCastPlotter(Plotter):
    """
    Generate a ``Plotter`` window for Volume rendering using ray casting.

    Returns the ``Plotter`` object.

    .. hint:: examples/volumetric/app_raycaster.py
        .. image:: https://vedo.embl.es/images/advanced/app_raycaster.gif
    """
    def __init__(self, volume, **kwargs):

        Plotter.__init__(self, **kwargs)

        self.alphaslider0 = 0.33
        self.alphaslider1 = 0.66
        self.alphaslider2 = 1

        volumeProperty = volume.GetProperty()
        img = volume.imagedata()

        if volume.dimensions()[2]<3:
            vedo.logger.error("RayCastPlotter: not enough z slices.")
            raise RuntimeError

        smin, smax = img.GetScalarRange()
        x0alpha = smin + (smax - smin) * 0.25
        x1alpha = smin + (smax - smin) * 0.5
        x2alpha = smin + (smax - smin) * 1.0

        ############################## color map slider
        # Create transfer mapping scalar value to color
        cmaps = ["jet",
                 "viridis",
                 "bone",
                 "hot",
                 "plasma",
                 "winter",
                 "cool",
                 "gist_earth",
                 "coolwarm",
                 "tab10",
        ]
        cols_cmaps = []
        for cm in cmaps:
            cols = colorMap(range(0, 21), cm, 0, 20)  # sample 20 colors
            cols_cmaps.append(cols)
        Ncols = len(cmaps)
        csl = (0.9, 0.9, 0.9)
        if sum(getColor(self.renderer.GetBackground())) > 1.5:
            csl = (0.1, 0.1, 0.1)

        def sliderColorMap(widget, event):
            sliderRep = widget.GetRepresentation()
            k = int(sliderRep.GetValue())
            sliderRep.SetTitleText(cmaps[k])
            volume.color(cmaps[k])

        w1 = self.addSlider2D(
            sliderColorMap,
            0,
            Ncols - 1,
            value=0,
            showValue=0,
            title=cmaps[0],
            c=csl,
            pos=[(0.8, 0.05), (0.965, 0.05)],
        )
        w1.GetRepresentation().SetTitleHeight(0.018)

        ############################## alpha sliders
        # Create transfer mapping scalar value to opacity
        opacityTransferFunction = volumeProperty.GetScalarOpacity()

        def setOTF():
            opacityTransferFunction.RemoveAllPoints()
            opacityTransferFunction.AddPoint(smin, 0.0)
            opacityTransferFunction.AddPoint(smin + (smax - smin) * 0.1, 0.0)
            opacityTransferFunction.AddPoint(x0alpha, self.alphaslider0)
            opacityTransferFunction.AddPoint(x1alpha, self.alphaslider1)
            opacityTransferFunction.AddPoint(x2alpha, self.alphaslider2)

        setOTF()

        def sliderA0(widget, event):
            self.alphaslider0 = widget.GetRepresentation().GetValue()
            setOTF()

        self.addSlider2D(sliderA0, 0, 1,
                        value=self.alphaslider0,
                        pos=[(0.84, 0.1), (0.84, 0.26)],
                        c=csl, showValue=0)

        def sliderA1(widget, event):
            self.alphaslider1 = widget.GetRepresentation().GetValue()
            setOTF()

        self.addSlider2D(sliderA1, 0, 1,
                        value=self.alphaslider1,
                        pos=[(0.89, 0.1), (0.89, 0.26)],
                        c=csl, showValue=0)

        def sliderA2(widget, event):
            self.alphaslider2 = widget.GetRepresentation().GetValue()
            setOTF()

        w2 = self.addSlider2D(sliderA2, 0, 1,
                            value=self.alphaslider2,
                            pos=[(0.96, 0.1), (0.96, 0.26)],
                            c=csl, showValue=0,
                            title="Opacity levels")
        w2.GetRepresentation().SetTitleHeight(0.016)

        # add a button
        def buttonfuncMode():
            s = volume.mode()
            snew = (s + 1) % 2
            volume.mode(snew)
            bum.switch()

        bum = self.addButton(
            buttonfuncMode,
            pos=(0.7, 0.035),
            states=["composite", "max proj."],
            c=["bb", "gray"],
            bc=["gray", "bb"],  # colors of states
            font="",
            size=16,
            bold=0,
            italic=False,
        )
        bum.status(volume.mode())

        # add histogram of scalar
        plot = CornerHistogram(volume,
            bins=25, logscale=1, c=(.7,.7,.7), bg=(.7,.7,.7), pos=(0.78, 0.065),
            lines=True, dots=False,
            nmax=3.1415e+06, # subsample otherwise is too slow
        )

        plot.GetPosition2Coordinate().SetValue(0.197, 0.20, 0)
        plot.GetXAxisActor2D().SetFontFactor(0.7)
        plot.GetProperty().SetOpacity(0.5)
        self.add([plot, volume])

Ancestors

Inherited members

class Slicer2DPlotter (volume, levels=(None, None), axes=None, zoom=1.2, pos=(0, 0), size='auto', screensize='auto', title='', bg='white', bg2=None, interactive=True)

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

Parameters

levels : list
window and color level

Expand source code
class Slicer2DPlotter(Plotter):
    """
    Create a ``Plotter`` with a single slice of a Volume which always faces the camera,
    but at the same time can be oriented arbitrarily in space.

    Parameters
    ----------
    levels : list
        window and color level

    .. image:: https://vedo.embl.es/images/volumetric/read_volume3.jpg
    """
    def __init__(self,
                 volume,
                 levels=(None, None),
                 axes=None,
                 zoom=1.2,
                 pos=(0, 0),
                 size="auto",
                 screensize="auto",
                 title="",
                 bg="white",
                 bg2=None,
                 interactive=True,
        ):
        custom_shape = [ # define here the 2 rendering rectangle spaces
            dict(bottomleft=(0.0,0.0), topright=(1,1), bg='k9'), # the full window
            dict(bottomleft=(0.8,0.8), topright=(1,1), bg='k8', bg2='lb'),
        ]

        if not title:
            if volume.filename:
                title = volume.filename[:80]
            else:
                title = "Volume Slicer2D"

        Plotter.__init__(self, shape=custom_shape, title=title, pos=pos,
                         screensize=screensize, size=size, bg=bg, bg2=bg2, axes=0,
                         interactive=False)

        vsl = vedo.volume.VolumeSlice(volume)  # reuse the same underlying data as in vol

        # no argument will grab the existing cmap in vol (or use buildLUT())
        vsl.colorize()

        if levels[0] and levels[1]:
            vsl.lighting(window=levels[0], level=levels[1])

        usage = Text2D(
            f"SHIFT+Left click   \rightarrow rotate camera for oblique slicing\n"
            f"SHIFT+Middle click \rightarrow slice perpendicularly through image\n"
            f"Left click & drag  \rightarrow modify luminosity and contrast\n"
            f"R                  \rightarrow Reset the Window/Color levels\n"
            f"X                  \rightarrow Reset to sagittal view\n"
            f"Y                  \rightarrow Reset to coronal view\n"
            f"Z                  \rightarrow Reset to axial view",
            font="Calco", pos="top-left", s=0.8, bg='yellow', alpha=0.25
        )

        hist = CornerHistogram(volume.pointdata[0],
                               bins=25, logscale=1, pos=(0.02, 0.02), s=0.175,
                               c='dg', bg='k', alpha=1)
        ax = None
        if axes == 7:
            ax = vedo.addons.RulerAxes(vsl, xtitle='x - ', ytitle='y - ', ztitle='z - ')

        box = vsl.box().alpha(0.1)
        self.show(vsl, box, ax, usage, hist, at=0, mode="image", zoom=zoom)
        self.show(volume, at=1, interactive=interactive)

Ancestors

Inherited members

class Slicer3DPlotter (volume, alpha=1, cmaps=('gist_ncar_r', 'hot_r', 'bone_r', 'jet', 'Spectral_r'), map2cells=False, clamp=True, useSlider3D=False, showHisto=True, showIcon=True, draggable=False, pos=(0, 0), size='auto', screensize='auto', title='', bg='white', bg2='lightblue', axes=7, resetcam=True, interactive=True)

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

Returns the Plotter object.

Parameters

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
useSlider3D : bool
show sliders attached along the axes
showHisto : bool
show histogram on bottom left
showIcon : bool
show a small 3D rendering icon of the volume
draggable : bool
make the icon draggable

Hint: examples/volumetric/slicer1.py

Expand source code
class Slicer3DPlotter(Plotter):
    """
    Generate a ``Plotter`` window with slicing planes for the input Volume.

    Returns the ``Plotter`` object.

    Parameters
    ----------
    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

    useSlider3D : bool
        show sliders attached along the axes

    showHisto : bool
        show histogram on bottom left

    showIcon : bool
        show a small 3D rendering icon of the volume

    draggable : bool
        make the icon draggable

    .. hint:: examples/volumetric/slicer1.py
        .. image:: https://vedo.embl.es/images/volumetric/slicer1.jpg
    """
    def __init__(
            self,
            volume,
            alpha=1,
            cmaps=('gist_ncar_r', "hot_r", "bone_r", "jet", "Spectral_r"),
            map2cells=False,  # buggy
            clamp=True,
            useSlider3D=False,
            showHisto=True,
            showIcon=True,
            draggable=False,
            pos=(0, 0),
            size="auto",
            screensize="auto",
            title="",
            bg="white",
            bg2="lightblue",
            axes=7,
            resetcam=True,
            interactive=True,
        ):
        self._cmap_slicer= 'gist_ncar_r'

        if not title:
            if volume.filename:
                title = volume.filename
            else:
                title = "Volume Slicer"

        ################################
        Plotter.__init__(
            self,
            pos=pos,
            bg=bg,
            bg2=bg2,
            size=size,
            screensize=screensize,
            title=title,
            interactive=interactive,
            axes=axes,
        )
        ################################
        box = volume.box().wireframe().alpha(0.1)

        self.show(box, viewup="z", resetcam=resetcam, interactive=False)
        if showIcon:
            self.addInset(volume, pos=(.85,.85), size=0.15, c='w', draggable=draggable)

        # inits
        la, ld = 0.7, 0.3 #ambient, diffuse
        dims = volume.dimensions()
        data = volume.pointdata[0]
        rmin, rmax = volume.imagedata().GetScalarRange()
        if clamp:
            hdata, edg = np.histogram(data, bins=50)
            logdata = np.log(hdata+1)
            # mean  of the logscale plot
            meanlog = np.sum(np.multiply(edg[:-1], logdata))/np.sum(logdata)
            rmax = min(rmax, meanlog+(meanlog-rmin)*0.9)
            rmin = max(rmin, meanlog-(rmax-meanlog)*0.9)
            vedo.logger.debug('scalar range clamped to range: (' + precision(rmin, 3) +', '+  precision(rmax, 3)+')')
        self._cmap_slicer = cmaps[0]
        visibles = [None, None, None]
        msh = volume.zSlice(int(dims[2]/2))
        msh.alpha(alpha).lighting('', la, ld, 0)
        msh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
        if map2cells: msh.mapPointsToCells()
        self.renderer.AddActor(msh)
        visibles[2] = msh
        msh.addScalarBar(pos=(0.04,0.0), horizontal=True, titleFontSize=0)

        def sliderfunc_x(widget, event):
            i = int(widget.GetRepresentation().GetValue())
            msh = volume.xSlice(i).alpha(alpha).lighting('', la, ld, 0)
            msh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
            if map2cells: msh.mapPointsToCells()
            self.renderer.RemoveActor(visibles[0])
            if i and i<dims[0]:
                self.renderer.AddActor(msh)
            visibles[0] = msh

        def sliderfunc_y(widget, event):
            i = int(widget.GetRepresentation().GetValue())
            msh = volume.ySlice(i).alpha(alpha).lighting('', la, ld, 0)
            msh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
            if map2cells: msh.mapPointsToCells()
            self.renderer.RemoveActor(visibles[1])
            if i and i<dims[1]:
                self.renderer.AddActor(msh)
            visibles[1] = msh

        def sliderfunc_z(widget, event):
            i = int(widget.GetRepresentation().GetValue())
            msh = volume.zSlice(i).alpha(alpha).lighting('', la, ld, 0)
            msh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
            if map2cells: msh.mapPointsToCells()
            self.renderer.RemoveActor(visibles[2])
            if i and i<dims[2]:
                self.renderer.AddActor(msh)
            visibles[2] = msh

        cx, cy, cz, ch = 'dr', 'dg', 'db', (0.3,0.3,0.3)
        if np.sum(self.renderer.GetBackground()) < 1.5:
            cx, cy, cz = 'lr', 'lg', 'lb'
            ch = (0.8,0.8,0.8)

        if not useSlider3D:
            self.addSlider2D(sliderfunc_x, 0, dims[0], title='X', titleSize=0.5,
                           pos=[(0.8,0.12), (0.95,0.12)], showValue=False, c=cx)
            self.addSlider2D(sliderfunc_y, 0, dims[1], title='Y', titleSize=0.5,
                           pos=[(0.8,0.08), (0.95,0.08)], showValue=False, c=cy)
            self.addSlider2D(sliderfunc_z, 0, dims[2], title='Z', titleSize=0.6,
                           value=int(dims[2]/2),
                           pos=[(0.8,0.04), (0.95,0.04)], showValue=False, c=cz)
        else: # 3d sliders attached to the axes bounds
            bs = box.bounds()
            self.addSlider3D(
                    sliderfunc_x,
                    pos1=(bs[0], bs[2], bs[4]),
                    pos2=(bs[1], bs[2], bs[4]),
                    xmin=0, xmax=dims[0],
                    t=box.diagonalSize()/mag(box.xbounds())*0.6,
                    c=cx,
                    showValue=False,
            )
            self.addSlider3D(
                    sliderfunc_y,
                    pos1=(bs[1], bs[2], bs[4]),
                    pos2=(bs[1], bs[3], bs[4]),
                    xmin=0, xmax=dims[1],
                    t=box.diagonalSize()/mag(box.ybounds())*0.6,
                    c=cy,
                    showValue=False,
            )
            self.addSlider3D(
                    sliderfunc_z,
                    pos1=(bs[0], bs[2], bs[4]),
                    pos2=(bs[0], bs[2], bs[5]),
                    xmin=0, xmax=dims[2],
                    value=int(dims[2]/2),
                    t=box.diagonalSize()/mag(box.zbounds())*0.6,
                    c=cz,
                    showValue=False,
            )


        #################
        def buttonfunc():
            bu.switch()
            self._cmap_slicer = bu.status()
            for mesh in visibles:
                if mesh:
                    mesh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
                    if map2cells:
                        mesh.mapPointsToCells()
            self.renderer.RemoveActor(mesh.scalarbar)
            mesh.addScalarBar(pos=(0.04,0.0), horizontal=True, titleFontSize=0)

        bu = self.addButton(buttonfunc,
            pos=(0.27, 0.005),
            states=cmaps,
            c=["db"]*len(cmaps),
            bc=["lb"]*len(cmaps),  # colors of states
            size=14,
            bold=True,
        )

        #################
        hist = None
        if showHisto:
            hist = CornerHistogram(data, s=0.2,
                                   bins=25, logscale=1, pos=(0.02, 0.02),
                                   c=ch, bg=ch, alpha=0.7,
            )

        self.add([msh, hist], resetcam=False)
        if interactive:
            self.interactive()

Ancestors

Inherited members