vedo.applications

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

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

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

Browser( objects=(), sliderpos=((0.5, 0.07), (0.95, 0.07)), c=None, slider_title='', font='Calco', resetcam=False, **kwargs)
1030    def __init__(
1031        self,
1032        objects=(),
1033        sliderpos=((0.50, 0.07), (0.95, 0.07)),
1034        c=None,  # slider color
1035        slider_title="",
1036        font="Calco", # slider font
1037        resetcam=False, # resetcam while using the slider
1038        **kwargs,
1039    ):
1040        """
1041        Browse a series of vedo objects by using a simple slider.
1042
1043        The input object can be a list of objects or a list of lists of objects.
1044
1045        Arguments:
1046            objects : (list)
1047                list of objects to be browsed.
1048            sliderpos : (list)
1049                position of the slider.
1050            c : (str)
1051                color of the slider.
1052            slider_title : (str)
1053                title of the slider.
1054            font : (str)
1055                font of the slider.
1056            resetcam : (bool)
1057                resetcam while using the slider.
1058            **kwargs : (dict)
1059                keyword arguments to pass to Plotter.
1060
1061        Examples:
1062            ```python
1063            from vedo import load, dataurl
1064            from vedo.applications import Browser
1065            meshes = load(dataurl+'timecourse1d.npy') # python list of Meshes
1066            plt = Browser(meshes, bg='k')             # vedo.Plotter
1067            plt.show(interactive=False, zoom='tight') # show the meshes
1068            plt.play(dt=50)                           # delay in milliseconds
1069            plt.close()
1070            ```
1071
1072        - [morphomatics_tube.py](https://github.com/marcomusy/vedo/tree/master/examples/other/morphomatics_tube.py)
1073        """
1074        kwargs.pop("N", 1)
1075        kwargs.pop("shape", [])
1076        kwargs.pop("axes", 1)
1077        super().__init__(**kwargs)
1078
1079        if isinstance(objects, str):
1080            objects = vedo.file_io.load(objects)
1081
1082        self += objects
1083
1084        if is_sequence(objects[0]):
1085            nobs = len(objects[0])
1086            for ob in objects:
1087                n = len(ob)
1088                msg = f"in Browser lists must have the same length but found {n} and {nobs}"
1089                assert len(ob) == nobs, msg
1090        else:
1091            nobs = len(objects)
1092            objects = [objects]
1093
1094        self.slider = None
1095        self.timer_callback_id = None
1096        self._oldk = None
1097
1098        # define the slider func ##########################
1099        def slider_function(widget=None, event=None):
1100
1101            k = int(self.slider.value)
1102
1103            if k == self._oldk:
1104                return # no change
1105            self._oldk = k
1106
1107            n = len(objects)
1108            m = len(objects[0])
1109            for i in range(n):
1110                for j in range(m):
1111                    ak = objects[i][j]
1112                    try:
1113                        if j == k:
1114                            ak.on()
1115                            akon = ak
1116                        else:
1117                            ak.off()
1118                    except AttributeError:
1119                        pass
1120
1121            try:
1122                tx = str(k)
1123                if slider_title:
1124                    tx = slider_title + " " + tx
1125                elif n == 1 and akon.filename:
1126                    tx = akon.filename.split("/")[-1]
1127                    tx = tx.split("\\")[-1]  # windows os
1128                elif akon.name:
1129                    tx = ak.name + " " + tx
1130            except:
1131                pass
1132            self.slider.title = tx
1133
1134            if resetcam:
1135                self.reset_camera()
1136            self.render()
1137        ##################################################
1138
1139        self.slider_function = slider_function
1140        self.slider = self.add_slider(
1141            slider_function,
1142            0.5,
1143            nobs - 0.5,
1144            pos=sliderpos,
1145            font=font,
1146            c=c,
1147            show_value=False,
1148        )
1149        self.slider.GetRepresentation().SetTitleHeight(0.020)
1150        slider_function()  # init call

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

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

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

Start playing the slides at a given speed.

class IsosurfaceBrowser(vedo.plotter.Plotter):
 844class IsosurfaceBrowser(Plotter):
 845    """
 846    Generate a Volume isosurfacing controlled by a slider.
 847    """
 848    def __init__(
 849        self,
 850        volume,
 851        isovalue=None,
 852        scalar_range=(),
 853        c=None,
 854        alpha=1,
 855        lego=False,
 856        res=50,
 857        use_gpu=False,
 858        precompute=False,
 859        cmap="hot",
 860        delayed=False,
 861        sliderpos=4,
 862        **kwargs,
 863    ):
 864        """
 865        Generate a `vedo.Plotter` for Volume isosurfacing using a slider.
 866
 867        Arguments:
 868            volume : (Volume)
 869                the Volume object to be isosurfaced.
 870            isovalues : (float, list)
 871                isosurface value(s) to be displayed.
 872            scalar_range : (list)
 873                scalar range to be used.
 874            c : str, (list)
 875                color(s) of the isosurface(s).
 876            alpha : (float, list)
 877                opacity of the isosurface(s).
 878            lego : (bool)
 879                if True generate a lego plot instead of a surface.
 880            res : (int)
 881                resolution of the isosurface.
 882            use_gpu : (bool)
 883                use GPU acceleration.
 884            precompute : (bool)
 885                precompute the isosurfaces (so slider browsing will be smoother).
 886            cmap : (str)
 887                color map name to be used.
 888            delayed : (bool)
 889                delay the slider update on mouse release.
 890            sliderpos : (int)
 891                position of the slider.
 892            **kwargs : (dict)
 893                keyword arguments to pass to Plotter.
 894
 895        Examples:
 896            - [app_isobrowser.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/app_isobrowser.py)
 897
 898                ![](https://vedo.embl.es/images/advanced/app_isobrowser.gif)
 899        """
 900
 901        super().__init__(**kwargs)
 902
 903        ### GPU ################################
 904        if use_gpu and hasattr(volume.properties, "GetIsoSurfaceValues"):
 905
 906            if len(scalar_range) == 2:
 907                scrange = scalar_range
 908            else:
 909                scrange = volume.scalar_range()
 910            delta = scrange[1] - scrange[0]
 911            if not delta:
 912                return
 913
 914            if isovalue is None:
 915                isovalue = delta / 3.0 + scrange[0]
 916
 917            ### isovalue slider callback
 918            def slider_isovalue(widget, event):
 919                value = widget.GetRepresentation().GetValue()
 920                isovals.SetValue(0, value)
 921
 922            isovals = volume.properties.GetIsoSurfaceValues()
 923            isovals.SetValue(0, isovalue)
 924            self.add(volume.mode(5).alpha(alpha).cmap(c))
 925
 926            self.add_slider(
 927                slider_isovalue,
 928                scrange[0] + 0.02 * delta,
 929                scrange[1] - 0.02 * delta,
 930                value=isovalue,
 931                pos=sliderpos,
 932                title="scalar value",
 933                show_value=True,
 934                delayed=delayed,
 935            )
 936
 937        ### CPU ################################
 938        else:
 939
 940            self._prev_value = 1e30
 941
 942            scrange = volume.scalar_range()
 943            delta = scrange[1] - scrange[0]
 944            if not delta:
 945                return
 946
 947            if lego:
 948                res = int(res / 2)  # because lego is much slower
 949                slidertitle = ""
 950            else:
 951                slidertitle = "scalar value"
 952
 953            allowed_vals = np.linspace(scrange[0], scrange[1], num=res)
 954
 955            bacts = {}  # cache the meshes so we dont need to recompute
 956            if precompute:
 957                delayed = False  # no need to delay the slider in this case
 958
 959                for value in allowed_vals:
 960                    value_name = precision(value, 2)
 961                    if lego:
 962                        mesh = volume.legosurface(vmin=value)
 963                        if mesh.ncells:
 964                            mesh.cmap(cmap, vmin=scrange[0], vmax=scrange[1], on="cells")
 965                    else:
 966                        mesh = volume.isosurface(value).color(c).alpha(alpha)
 967                    bacts.update({value_name: mesh})  # store it
 968
 969            ### isovalue slider callback
 970            def slider_isovalue(widget, event):
 971
 972                prevact = self.vol_actors[0]
 973                if isinstance(widget, float):
 974                    value = widget
 975                else:
 976                    value = widget.GetRepresentation().GetValue()
 977
 978                # snap to the closest
 979                idx = (np.abs(allowed_vals - value)).argmin()
 980                value = allowed_vals[idx]
 981
 982                if abs(value - self._prev_value) / delta < 0.001:
 983                    return
 984                self._prev_value = value
 985
 986                value_name = precision(value, 2)
 987                if value_name in bacts:  # reusing the already existing mesh
 988                    # print('reusing')
 989                    mesh = bacts[value_name]
 990                else:  # else generate it
 991                    # print('generating', value)
 992                    if lego:
 993                        mesh = volume.legosurface(vmin=value)
 994                        if mesh.ncells:
 995                            mesh.cmap(cmap, vmin=scrange[0], vmax=scrange[1], on="cells")
 996                    else:
 997                        mesh = volume.isosurface(value).color(c).alpha(alpha)
 998                    bacts.update({value_name: mesh})  # store it
 999
1000                self.renderer.RemoveActor(prevact)
1001                self.renderer.AddActor(mesh)
1002                self.vol_actors[0] = mesh
1003
1004            ################################################
1005
1006            if isovalue is None:
1007                isovalue = delta / 3.0 + scrange[0]
1008
1009            self.vol_actors = [None]
1010            slider_isovalue(isovalue, "")  # init call
1011            if lego:
1012                self.vol_actors[0].add_scalarbar(pos=(0.8, 0.12))
1013
1014            self.add_slider(
1015                slider_isovalue,
1016                scrange[0] + 0.02 * delta,
1017                scrange[1] - 0.02 * delta,
1018                value=isovalue,
1019                pos=sliderpos,
1020                title=slidertitle,
1021                show_value=True,
1022                delayed=delayed,
1023            )

Generate a Volume isosurfacing controlled by a slider.

IsosurfaceBrowser( volume, isovalue=None, scalar_range=(), c=None, alpha=1, lego=False, res=50, use_gpu=False, precompute=False, cmap='hot', delayed=False, sliderpos=4, **kwargs)
 848    def __init__(
 849        self,
 850        volume,
 851        isovalue=None,
 852        scalar_range=(),
 853        c=None,
 854        alpha=1,
 855        lego=False,
 856        res=50,
 857        use_gpu=False,
 858        precompute=False,
 859        cmap="hot",
 860        delayed=False,
 861        sliderpos=4,
 862        **kwargs,
 863    ):
 864        """
 865        Generate a `vedo.Plotter` for Volume isosurfacing using a slider.
 866
 867        Arguments:
 868            volume : (Volume)
 869                the Volume object to be isosurfaced.
 870            isovalues : (float, list)
 871                isosurface value(s) to be displayed.
 872            scalar_range : (list)
 873                scalar range to be used.
 874            c : str, (list)
 875                color(s) of the isosurface(s).
 876            alpha : (float, list)
 877                opacity of the isosurface(s).
 878            lego : (bool)
 879                if True generate a lego plot instead of a surface.
 880            res : (int)
 881                resolution of the isosurface.
 882            use_gpu : (bool)
 883                use GPU acceleration.
 884            precompute : (bool)
 885                precompute the isosurfaces (so slider browsing will be smoother).
 886            cmap : (str)
 887                color map name to be used.
 888            delayed : (bool)
 889                delay the slider update on mouse release.
 890            sliderpos : (int)
 891                position of the slider.
 892            **kwargs : (dict)
 893                keyword arguments to pass to Plotter.
 894
 895        Examples:
 896            - [app_isobrowser.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/app_isobrowser.py)
 897
 898                ![](https://vedo.embl.es/images/advanced/app_isobrowser.gif)
 899        """
 900
 901        super().__init__(**kwargs)
 902
 903        ### GPU ################################
 904        if use_gpu and hasattr(volume.properties, "GetIsoSurfaceValues"):
 905
 906            if len(scalar_range) == 2:
 907                scrange = scalar_range
 908            else:
 909                scrange = volume.scalar_range()
 910            delta = scrange[1] - scrange[0]
 911            if not delta:
 912                return
 913
 914            if isovalue is None:
 915                isovalue = delta / 3.0 + scrange[0]
 916
 917            ### isovalue slider callback
 918            def slider_isovalue(widget, event):
 919                value = widget.GetRepresentation().GetValue()
 920                isovals.SetValue(0, value)
 921
 922            isovals = volume.properties.GetIsoSurfaceValues()
 923            isovals.SetValue(0, isovalue)
 924            self.add(volume.mode(5).alpha(alpha).cmap(c))
 925
 926            self.add_slider(
 927                slider_isovalue,
 928                scrange[0] + 0.02 * delta,
 929                scrange[1] - 0.02 * delta,
 930                value=isovalue,
 931                pos=sliderpos,
 932                title="scalar value",
 933                show_value=True,
 934                delayed=delayed,
 935            )
 936
 937        ### CPU ################################
 938        else:
 939
 940            self._prev_value = 1e30
 941
 942            scrange = volume.scalar_range()
 943            delta = scrange[1] - scrange[0]
 944            if not delta:
 945                return
 946
 947            if lego:
 948                res = int(res / 2)  # because lego is much slower
 949                slidertitle = ""
 950            else:
 951                slidertitle = "scalar value"
 952
 953            allowed_vals = np.linspace(scrange[0], scrange[1], num=res)
 954
 955            bacts = {}  # cache the meshes so we dont need to recompute
 956            if precompute:
 957                delayed = False  # no need to delay the slider in this case
 958
 959                for value in allowed_vals:
 960                    value_name = precision(value, 2)
 961                    if lego:
 962                        mesh = volume.legosurface(vmin=value)
 963                        if mesh.ncells:
 964                            mesh.cmap(cmap, vmin=scrange[0], vmax=scrange[1], on="cells")
 965                    else:
 966                        mesh = volume.isosurface(value).color(c).alpha(alpha)
 967                    bacts.update({value_name: mesh})  # store it
 968
 969            ### isovalue slider callback
 970            def slider_isovalue(widget, event):
 971
 972                prevact = self.vol_actors[0]
 973                if isinstance(widget, float):
 974                    value = widget
 975                else:
 976                    value = widget.GetRepresentation().GetValue()
 977
 978                # snap to the closest
 979                idx = (np.abs(allowed_vals - value)).argmin()
 980                value = allowed_vals[idx]
 981
 982                if abs(value - self._prev_value) / delta < 0.001:
 983                    return
 984                self._prev_value = value
 985
 986                value_name = precision(value, 2)
 987                if value_name in bacts:  # reusing the already existing mesh
 988                    # print('reusing')
 989                    mesh = bacts[value_name]
 990                else:  # else generate it
 991                    # print('generating', value)
 992                    if lego:
 993                        mesh = volume.legosurface(vmin=value)
 994                        if mesh.ncells:
 995                            mesh.cmap(cmap, vmin=scrange[0], vmax=scrange[1], on="cells")
 996                    else:
 997                        mesh = volume.isosurface(value).color(c).alpha(alpha)
 998                    bacts.update({value_name: mesh})  # store it
 999
1000                self.renderer.RemoveActor(prevact)
1001                self.renderer.AddActor(mesh)
1002                self.vol_actors[0] = mesh
1003
1004            ################################################
1005
1006            if isovalue is None:
1007                isovalue = delta / 3.0 + scrange[0]
1008
1009            self.vol_actors = [None]
1010            slider_isovalue(isovalue, "")  # init call
1011            if lego:
1012                self.vol_actors[0].add_scalarbar(pos=(0.8, 0.12))
1013
1014            self.add_slider(
1015                slider_isovalue,
1016                scrange[0] + 0.02 * delta,
1017                scrange[1] - 0.02 * delta,
1018                value=isovalue,
1019                pos=sliderpos,
1020                title=slidertitle,
1021                show_value=True,
1022                delayed=delayed,
1023            )

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

Arguments:
  • volume : (Volume) the Volume object to be isosurfaced.
  • isovalues : (float, list) isosurface value(s) to be displayed.
  • scalar_range : (list) scalar range to be used.
  • c : str, (list) color(s) of the isosurface(s).
  • alpha : (float, list) opacity of the isosurface(s).
  • lego : (bool) if True generate a lego plot instead of a surface.
  • res : (int) resolution of the isosurface.
  • use_gpu : (bool) use GPU acceleration.
  • precompute : (bool) precompute the isosurfaces (so slider browsing will be smoother).
  • cmap : (str) color map name to be used.
  • delayed : (bool) delay the slider update on mouse release.
  • sliderpos : (int) position of the slider.
  • **kwargs : (dict) keyword arguments to pass to Plotter.
Examples:
class FreeHandCutPlotter(vedo.plotter.Plotter):
1160class FreeHandCutPlotter(Plotter):
1161    """A tool to edit meshes interactively."""
1162
1163    # thanks to Jakub Kaminski for the original version of this script
1164    def __init__(
1165        self,
1166        mesh,
1167        splined=True,
1168        font="Bongas",
1169        alpha=0.9,
1170        lw=4,
1171        lc="red5",
1172        pc="red4",
1173        c="green3",
1174        tc="k9",
1175        tol=0.008,
1176        **options,
1177    ):
1178        """
1179        A `vedo.Plotter` derived class which edits polygonal meshes interactively.
1180
1181        Can also be invoked from command line with:
1182
1183        ```bash
1184        vedo --edit https://vedo.embl.es/examples/data/porsche.ply
1185        ```
1186
1187        Usage:
1188            - Left-click and hold to rotate
1189            - Right-click and move to draw line
1190            - Second right-click to stop drawing
1191            - Press "c" to clear points
1192            -       "z/Z" to cut mesh (Z inverts inside-out the selection area)
1193            -       "L" to keep only the largest connected surface
1194            -       "s" to save mesh to file (tag `_edited` is appended to filename)
1195            -       "u" to undo last action
1196            -       "h" for help, "i" for info
1197
1198        Arguments:
1199            mesh : (Mesh, Points)
1200                The input Mesh or pointcloud.
1201            splined : (bool)
1202                join points with a spline or a simple line.
1203            font : (str)
1204                Font name for the instructions.
1205            alpha : (float)
1206                transparency of the instruction message panel.
1207            lw : (str)
1208                selection line width.
1209            lc : (str)
1210                selection line color.
1211            pc : (str)
1212                selection points color.
1213            c : (str)
1214                background color of instructions.
1215            tc : (str)
1216                text color of instructions.
1217            tol : (int)
1218                tolerance of the point proximity.
1219            **kwargs : (dict)
1220                keyword arguments to pass to Plotter.
1221
1222        Examples:
1223            - [cut_freehand.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/cut_freehand.py)
1224
1225                ![](https://vedo.embl.es/images/basic/cutFreeHand.gif)
1226        """
1227
1228        if not isinstance(mesh, Points):
1229            vedo.logger.error("FreeHandCutPlotter input must be Points or Mesh")
1230            raise RuntimeError()
1231
1232        super().__init__(**options)
1233
1234        self.mesh = mesh
1235        self.mesh_prev = mesh
1236        self.splined = splined
1237        self.linecolor = lc
1238        self.linewidth = lw
1239        self.pointcolor = pc
1240        self.color = c
1241        self.alpha = alpha
1242
1243        self.msg = "Right-click and move to draw line\n"
1244        self.msg += "Second right-click to stop drawing\n"
1245        self.msg += "Press L to extract largest surface\n"
1246        self.msg += "        z/Z to cut mesh (s to save)\n"
1247        self.msg += "        c to clear points, u to undo"
1248        self.txt2d = Text2D(self.msg, pos="top-left", font=font, s=0.9)
1249        self.txt2d.c(tc).background(c, alpha).frame()
1250
1251        self.idkeypress = self.add_callback("KeyPress", self._on_keypress)
1252        self.idrightclck = self.add_callback("RightButton", self._on_right_click)
1253        self.idmousemove = self.add_callback("MouseMove", self._on_mouse_move)
1254        self.drawmode = False
1255        self.tol = tol  # tolerance of point distance
1256        self.cpoints = []
1257        self.points = None
1258        self.spline = None
1259        self.jline = None
1260        self.topline = None
1261        self.top_pts = []
1262
1263    def init(self, init_points):
1264        """Set an initial number of points to define a region"""
1265        if isinstance(init_points, Points):
1266            self.cpoints = init_points.vertices
1267        else:
1268            self.cpoints = np.array(init_points)
1269        self.points = Points(self.cpoints, r=self.linewidth).c(self.pointcolor).pickable(0)
1270        if self.splined:
1271            self.spline = Spline(self.cpoints, res=len(self.cpoints) * 4)
1272        else:
1273            self.spline = Line(self.cpoints)
1274        self.spline.lw(self.linewidth).c(self.linecolor).pickable(False)
1275        self.jline = Line(self.cpoints[0], self.cpoints[-1], lw=1, c=self.linecolor).pickable(0)
1276        self.add([self.points, self.spline, self.jline]).render()
1277        return self
1278
1279    def _on_right_click(self, evt):
1280        self.drawmode = not self.drawmode  # toggle mode
1281        if self.drawmode:
1282            self.txt2d.background(self.linecolor, self.alpha)
1283        else:
1284            self.txt2d.background(self.color, self.alpha)
1285            if len(self.cpoints) > 2:
1286                self.remove([self.spline, self.jline])
1287                if self.splined:  # show the spline closed
1288                    self.spline = Spline(self.cpoints, closed=True, res=len(self.cpoints) * 4)
1289                else:
1290                    self.spline = Line(self.cpoints, closed=True)
1291                self.spline.lw(self.linewidth).c(self.linecolor).pickable(False)
1292                self.add(self.spline)
1293        self.render()
1294
1295    def _on_mouse_move(self, evt):
1296        if self.drawmode:
1297            cpt = self.compute_world_coordinate(evt.picked2d)  # make this 2d-screen point 3d
1298            if self.cpoints and mag(cpt - self.cpoints[-1]) < self.mesh.diagonal_size() * self.tol:
1299                return  # new point is too close to the last one. skip
1300            self.cpoints.append(cpt)
1301            if len(self.cpoints) > 2:
1302                self.remove([self.points, self.spline, self.jline, self.topline])
1303                self.points = Points(self.cpoints, r=self.linewidth).c(self.pointcolor).pickable(0)
1304                if self.splined:
1305                    self.spline = Spline(self.cpoints, res=len(self.cpoints) * 4)  # not closed here
1306                else:
1307                    self.spline = Line(self.cpoints)
1308
1309                if evt.actor:
1310                    self.top_pts.append(evt.picked3d)
1311                    self.topline = Points(self.top_pts, r=self.linewidth)
1312                    self.topline.c(self.linecolor).pickable(False)
1313
1314                self.spline.lw(self.linewidth).c(self.linecolor).pickable(False)
1315                self.txt2d.background(self.linecolor)
1316                self.jline = Line(self.cpoints[0], self.cpoints[-1], lw=1, c=self.linecolor).pickable(0)
1317                self.add([self.points, self.spline, self.jline, self.topline]).render()
1318
1319    def _on_keypress(self, evt):
1320        if evt.keypress.lower() == "z" and self.spline:  # Cut mesh with a ribbon-like surface
1321            inv = False
1322            if evt.keypress == "Z":
1323                inv = True
1324            self.txt2d.background("red8").text("  ... working ...  ")
1325            self.render()
1326            self.mesh_prev = self.mesh.clone()
1327            tol = self.mesh.diagonal_size() / 2  # size of ribbon (not shown)
1328            pts = self.spline.vertices
1329            n = fit_plane(pts, signed=True).normal  # compute normal vector to points
1330            rb = Ribbon(pts - tol * n, pts + tol * n, closed=True)
1331            self.mesh.cut_with_mesh(rb, invert=inv)  # CUT
1332            self.txt2d.text(self.msg)  # put back original message
1333            if self.drawmode:
1334                self._on_right_click(evt)  # toggle mode to normal
1335            else:
1336                self.txt2d.background(self.color, self.alpha)
1337            self.remove([self.spline, self.points, self.jline, self.topline]).render()
1338            self.cpoints, self.points, self.spline = [], None, None
1339            self.top_pts, self.topline = [], None
1340
1341        elif evt.keypress == "L":
1342            self.txt2d.background("red8")
1343            self.txt2d.text(" ... removing smaller ... \n ... parts of the mesh ... ")
1344            self.render()
1345            self.remove(self.mesh)
1346            self.mesh_prev = self.mesh
1347            mcut = self.mesh.extract_largest_region()
1348            mcut.filename = self.mesh.filename  # copy over various properties
1349            mcut.name = self.mesh.name
1350            mcut.scalarbar = self.mesh.scalarbar
1351            mcut.info = self.mesh.info
1352            self.mesh = mcut                            # discard old mesh by overwriting it
1353            self.txt2d.text(self.msg).background(self.color)   # put back original message
1354            self.add(mcut).render()
1355
1356        elif evt.keypress == 'u':                       # Undo last action
1357            if self.drawmode:
1358                self._on_right_click(evt)               # toggle mode to normal
1359            else:
1360                self.txt2d.background(self.color, self.alpha)
1361            self.remove([self.mesh, self.spline, self.jline, self.points, self.topline])
1362            self.mesh = self.mesh_prev
1363            self.cpoints, self.points, self.spline = [], None, None
1364            self.top_pts, self.topline = [], None
1365            self.add(self.mesh).render()
1366
1367        elif evt.keypress in ("c", "Delete"):
1368            # clear all points
1369            self.remove([self.spline, self.points, self.jline, self.topline]).render()
1370            self.cpoints, self.points, self.spline = [], None, None
1371            self.top_pts, self.topline = [], None
1372
1373        elif evt.keypress == "r":  # reset camera and axes
1374            try:
1375                self.remove(self.axes_instances[0])
1376                self.axes_instances[0] = None
1377                self.add_global_axes(axtype=1, c=None, bounds=self.mesh.bounds())
1378                self.renderer.ResetCamera()
1379                self.render()
1380            except:
1381                pass
1382
1383        elif evt.keypress == "s":
1384            if self.mesh.filename:
1385                fname = os.path.basename(self.mesh.filename)
1386                fname, extension = os.path.splitext(fname)
1387                fname = fname.replace("_edited", "")
1388                fname = f"{fname}_edited{extension}"
1389            else:
1390                fname = "mesh_edited.vtk"
1391            self.write(fname)
1392
1393    def write(self, filename="mesh_edited.vtk"):
1394        """Save the resulting mesh to file"""
1395        self.mesh.write(filename)
1396        vedo.logger.info(f"mesh saved to file {filename}")
1397        return self
1398
1399    def start(self, *args, **kwargs):
1400        """Start window interaction (with mouse and keyboard)"""
1401        acts = [self.txt2d, self.mesh, self.points, self.spline, self.jline]
1402        self.show(acts + list(args), **kwargs)
1403        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)
1164    def __init__(
1165        self,
1166        mesh,
1167        splined=True,
1168        font="Bongas",
1169        alpha=0.9,
1170        lw=4,
1171        lc="red5",
1172        pc="red4",
1173        c="green3",
1174        tc="k9",
1175        tol=0.008,
1176        **options,
1177    ):
1178        """
1179        A `vedo.Plotter` derived class which edits polygonal meshes interactively.
1180
1181        Can also be invoked from command line with:
1182
1183        ```bash
1184        vedo --edit https://vedo.embl.es/examples/data/porsche.ply
1185        ```
1186
1187        Usage:
1188            - Left-click and hold to rotate
1189            - Right-click and move to draw line
1190            - Second right-click to stop drawing
1191            - Press "c" to clear points
1192            -       "z/Z" to cut mesh (Z inverts inside-out the selection area)
1193            -       "L" to keep only the largest connected surface
1194            -       "s" to save mesh to file (tag `_edited` is appended to filename)
1195            -       "u" to undo last action
1196            -       "h" for help, "i" for info
1197
1198        Arguments:
1199            mesh : (Mesh, Points)
1200                The input Mesh or pointcloud.
1201            splined : (bool)
1202                join points with a spline or a simple line.
1203            font : (str)
1204                Font name for the instructions.
1205            alpha : (float)
1206                transparency of the instruction message panel.
1207            lw : (str)
1208                selection line width.
1209            lc : (str)
1210                selection line color.
1211            pc : (str)
1212                selection points color.
1213            c : (str)
1214                background color of instructions.
1215            tc : (str)
1216                text color of instructions.
1217            tol : (int)
1218                tolerance of the point proximity.
1219            **kwargs : (dict)
1220                keyword arguments to pass to Plotter.
1221
1222        Examples:
1223            - [cut_freehand.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/cut_freehand.py)
1224
1225                ![](https://vedo.embl.es/images/basic/cutFreeHand.gif)
1226        """
1227
1228        if not isinstance(mesh, Points):
1229            vedo.logger.error("FreeHandCutPlotter input must be Points or Mesh")
1230            raise RuntimeError()
1231
1232        super().__init__(**options)
1233
1234        self.mesh = mesh
1235        self.mesh_prev = mesh
1236        self.splined = splined
1237        self.linecolor = lc
1238        self.linewidth = lw
1239        self.pointcolor = pc
1240        self.color = c
1241        self.alpha = alpha
1242
1243        self.msg = "Right-click and move to draw line\n"
1244        self.msg += "Second right-click to stop drawing\n"
1245        self.msg += "Press L to extract largest surface\n"
1246        self.msg += "        z/Z to cut mesh (s to save)\n"
1247        self.msg += "        c to clear points, u to undo"
1248        self.txt2d = Text2D(self.msg, pos="top-left", font=font, s=0.9)
1249        self.txt2d.c(tc).background(c, alpha).frame()
1250
1251        self.idkeypress = self.add_callback("KeyPress", self._on_keypress)
1252        self.idrightclck = self.add_callback("RightButton", self._on_right_click)
1253        self.idmousemove = self.add_callback("MouseMove", self._on_mouse_move)
1254        self.drawmode = False
1255        self.tol = tol  # tolerance of point distance
1256        self.cpoints = []
1257        self.points = None
1258        self.spline = None
1259        self.jline = None
1260        self.topline = None
1261        self.top_pts = []

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

Can also be invoked from command line with:

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

Set an initial number of points to define a region

def write(self, filename='mesh_edited.vtk'):
1393    def write(self, filename="mesh_edited.vtk"):
1394        """Save the resulting mesh to file"""
1395        self.mesh.write(filename)
1396        vedo.logger.info(f"mesh saved to file {filename}")
1397        return self

Save the resulting mesh to file

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

Start window interaction (with mouse and keyboard)

class RayCastPlotter(vedo.plotter.Plotter):
662class RayCastPlotter(Plotter):
663    """
664    Generate Volume rendering using ray casting.
665    """
666
667    def __init__(self, volume, **kwargs):
668        """
669        Generate a window for Volume rendering using ray casting.
670
671        Arguments:
672            volume : (Volume)
673                the Volume object to be isosurfaced.
674            **kwargs : (dict)
675                keyword arguments to pass to Plotter.
676
677        Returns:
678            `vedo.Plotter` object.
679
680        Examples:
681            - [app_raycaster.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/app_raycaster.py)
682
683            ![](https://vedo.embl.es/images/advanced/app_raycaster.gif)
684        """
685
686        super().__init__(**kwargs)
687
688        self.alphaslider0 = 0.33
689        self.alphaslider1 = 0.66
690        self.alphaslider2 = 1
691
692        self.properties = volume.properties
693        img = volume.dataset
694
695        if volume.dimensions()[2] < 3:
696            vedo.logger.error("RayCastPlotter: not enough z slices.")
697            raise RuntimeError
698
699        smin, smax = img.GetScalarRange()
700        x0alpha = smin + (smax - smin) * 0.25
701        x1alpha = smin + (smax - smin) * 0.5
702        x2alpha = smin + (smax - smin) * 1.0
703
704        ############################## color map slider
705        # Create transfer mapping scalar value to color
706        cmaps = [
707            "jet",
708            "viridis",
709            "bone",
710            "hot",
711            "plasma",
712            "winter",
713            "cool",
714            "gist_earth",
715            "coolwarm",
716            "tab10",
717        ]
718        cols_cmaps = []
719        for cm in cmaps:
720            cols = color_map(range(0, 21), cm, 0, 20)  # sample 20 colors
721            cols_cmaps.append(cols)
722        Ncols = len(cmaps)
723        csl = (0.9, 0.9, 0.9)
724        if sum(get_color(self.renderer.GetBackground())) > 1.5:
725            csl = (0.1, 0.1, 0.1)
726
727        def sliderColorMap(widget, event):
728            sliderRep = widget.GetRepresentation()
729            k = int(sliderRep.GetValue())
730            sliderRep.SetTitleText(cmaps[k])
731            volume.color(cmaps[k])
732
733        w1 = self.add_slider(
734            sliderColorMap,
735            0,
736            Ncols - 1,
737            value=0,
738            show_value=0,
739            title=cmaps[0],
740            c=csl,
741            pos=[(0.8, 0.05), (0.965, 0.05)],
742        )
743        w1.GetRepresentation().SetTitleHeight(0.018)
744
745        ############################## alpha sliders
746        # Create transfer mapping scalar value to opacity
747        opacityTransferFunction = self.properties.GetScalarOpacity()
748
749        def setOTF():
750            opacityTransferFunction.RemoveAllPoints()
751            opacityTransferFunction.AddPoint(smin, 0.0)
752            opacityTransferFunction.AddPoint(smin + (smax - smin) * 0.1, 0.0)
753            opacityTransferFunction.AddPoint(x0alpha, self.alphaslider0)
754            opacityTransferFunction.AddPoint(x1alpha, self.alphaslider1)
755            opacityTransferFunction.AddPoint(x2alpha, self.alphaslider2)
756
757        setOTF()
758
759        def sliderA0(widget, event):
760            self.alphaslider0 = widget.GetRepresentation().GetValue()
761            setOTF()
762
763        self.add_slider(
764            sliderA0,
765            0,
766            1,
767            value=self.alphaslider0,
768            pos=[(0.84, 0.1), (0.84, 0.26)],
769            c=csl,
770            show_value=0,
771        )
772
773        def sliderA1(widget, event):
774            self.alphaslider1 = widget.GetRepresentation().GetValue()
775            setOTF()
776
777        self.add_slider(
778            sliderA1,
779            0,
780            1,
781            value=self.alphaslider1,
782            pos=[(0.89, 0.1), (0.89, 0.26)],
783            c=csl,
784            show_value=0,
785        )
786
787        def sliderA2(widget, event):
788            self.alphaslider2 = widget.GetRepresentation().GetValue()
789            setOTF()
790
791        w2 = self.add_slider(
792            sliderA2,
793            0,
794            1,
795            value=self.alphaslider2,
796            pos=[(0.96, 0.1), (0.96, 0.26)],
797            c=csl,
798            show_value=0,
799            title="Opacity levels",
800        )
801        w2.GetRepresentation().SetTitleHeight(0.016)
802
803        # add a button
804        def button_func_mode(_obj, _ename):
805            s = volume.mode()
806            snew = (s + 1) % 2
807            volume.mode(snew)
808            bum.switch()
809
810        bum = self.add_button(
811            button_func_mode,
812            pos=(0.7, 0.035),
813            states=["composite", "max proj."],
814            c=["bb", "gray"],
815            bc=["gray", "bb"],  # colors of states
816            font="",
817            size=16,
818            bold=0,
819            italic=False,
820        )
821        bum.frame(color='w')
822        bum.status(volume.mode())
823
824        # add histogram of scalar
825        plot = CornerHistogram(
826            volume,
827            bins=25,
828            logscale=1,
829            c=(0.7, 0.7, 0.7),
830            bg=(0.7, 0.7, 0.7),
831            pos=(0.78, 0.065),
832            lines=True,
833            dots=False,
834            nmax=3.1415e06,  # subsample otherwise is too slow
835        )
836
837        plot.GetPosition2Coordinate().SetValue(0.197, 0.20, 0)
838        plot.GetXAxisActor2D().SetFontFactor(0.7)
839        plot.GetProperty().SetOpacity(0.5)
840        self.add([plot, volume])

Generate Volume rendering using ray casting.

RayCastPlotter(volume, **kwargs)
667    def __init__(self, volume, **kwargs):
668        """
669        Generate a window for Volume rendering using ray casting.
670
671        Arguments:
672            volume : (Volume)
673                the Volume object to be isosurfaced.
674            **kwargs : (dict)
675                keyword arguments to pass to Plotter.
676
677        Returns:
678            `vedo.Plotter` object.
679
680        Examples:
681            - [app_raycaster.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/app_raycaster.py)
682
683            ![](https://vedo.embl.es/images/advanced/app_raycaster.gif)
684        """
685
686        super().__init__(**kwargs)
687
688        self.alphaslider0 = 0.33
689        self.alphaslider1 = 0.66
690        self.alphaslider2 = 1
691
692        self.properties = volume.properties
693        img = volume.dataset
694
695        if volume.dimensions()[2] < 3:
696            vedo.logger.error("RayCastPlotter: not enough z slices.")
697            raise RuntimeError
698
699        smin, smax = img.GetScalarRange()
700        x0alpha = smin + (smax - smin) * 0.25
701        x1alpha = smin + (smax - smin) * 0.5
702        x2alpha = smin + (smax - smin) * 1.0
703
704        ############################## color map slider
705        # Create transfer mapping scalar value to color
706        cmaps = [
707            "jet",
708            "viridis",
709            "bone",
710            "hot",
711            "plasma",
712            "winter",
713            "cool",
714            "gist_earth",
715            "coolwarm",
716            "tab10",
717        ]
718        cols_cmaps = []
719        for cm in cmaps:
720            cols = color_map(range(0, 21), cm, 0, 20)  # sample 20 colors
721            cols_cmaps.append(cols)
722        Ncols = len(cmaps)
723        csl = (0.9, 0.9, 0.9)
724        if sum(get_color(self.renderer.GetBackground())) > 1.5:
725            csl = (0.1, 0.1, 0.1)
726
727        def sliderColorMap(widget, event):
728            sliderRep = widget.GetRepresentation()
729            k = int(sliderRep.GetValue())
730            sliderRep.SetTitleText(cmaps[k])
731            volume.color(cmaps[k])
732
733        w1 = self.add_slider(
734            sliderColorMap,
735            0,
736            Ncols - 1,
737            value=0,
738            show_value=0,
739            title=cmaps[0],
740            c=csl,
741            pos=[(0.8, 0.05), (0.965, 0.05)],
742        )
743        w1.GetRepresentation().SetTitleHeight(0.018)
744
745        ############################## alpha sliders
746        # Create transfer mapping scalar value to opacity
747        opacityTransferFunction = self.properties.GetScalarOpacity()
748
749        def setOTF():
750            opacityTransferFunction.RemoveAllPoints()
751            opacityTransferFunction.AddPoint(smin, 0.0)
752            opacityTransferFunction.AddPoint(smin + (smax - smin) * 0.1, 0.0)
753            opacityTransferFunction.AddPoint(x0alpha, self.alphaslider0)
754            opacityTransferFunction.AddPoint(x1alpha, self.alphaslider1)
755            opacityTransferFunction.AddPoint(x2alpha, self.alphaslider2)
756
757        setOTF()
758
759        def sliderA0(widget, event):
760            self.alphaslider0 = widget.GetRepresentation().GetValue()
761            setOTF()
762
763        self.add_slider(
764            sliderA0,
765            0,
766            1,
767            value=self.alphaslider0,
768            pos=[(0.84, 0.1), (0.84, 0.26)],
769            c=csl,
770            show_value=0,
771        )
772
773        def sliderA1(widget, event):
774            self.alphaslider1 = widget.GetRepresentation().GetValue()
775            setOTF()
776
777        self.add_slider(
778            sliderA1,
779            0,
780            1,
781            value=self.alphaslider1,
782            pos=[(0.89, 0.1), (0.89, 0.26)],
783            c=csl,
784            show_value=0,
785        )
786
787        def sliderA2(widget, event):
788            self.alphaslider2 = widget.GetRepresentation().GetValue()
789            setOTF()
790
791        w2 = self.add_slider(
792            sliderA2,
793            0,
794            1,
795            value=self.alphaslider2,
796            pos=[(0.96, 0.1), (0.96, 0.26)],
797            c=csl,
798            show_value=0,
799            title="Opacity levels",
800        )
801        w2.GetRepresentation().SetTitleHeight(0.016)
802
803        # add a button
804        def button_func_mode(_obj, _ename):
805            s = volume.mode()
806            snew = (s + 1) % 2
807            volume.mode(snew)
808            bum.switch()
809
810        bum = self.add_button(
811            button_func_mode,
812            pos=(0.7, 0.035),
813            states=["composite", "max proj."],
814            c=["bb", "gray"],
815            bc=["gray", "bb"],  # colors of states
816            font="",
817            size=16,
818            bold=0,
819            italic=False,
820        )
821        bum.frame(color='w')
822        bum.status(volume.mode())
823
824        # add histogram of scalar
825        plot = CornerHistogram(
826            volume,
827            bins=25,
828            logscale=1,
829            c=(0.7, 0.7, 0.7),
830            bg=(0.7, 0.7, 0.7),
831            pos=(0.78, 0.065),
832            lines=True,
833            dots=False,
834            nmax=3.1415e06,  # subsample otherwise is too slow
835        )
836
837        plot.GetPosition2Coordinate().SetValue(0.197, 0.20, 0)
838        plot.GetXAxisActor2D().SetFontFactor(0.7)
839        plot.GetProperty().SetOpacity(0.5)
840        self.add([plot, volume])

Generate a window for Volume rendering using ray casting.

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

vedo.Plotter object.

Examples:

class Slicer2DPlotter(vedo.plotter.Plotter):
450class Slicer2DPlotter(Plotter):
451    """
452    A single slice of a Volume which always faces the camera,
453    but at the same time can be oriented arbitrarily in space.
454    """
455
456    def __init__(self, vol, levels=(None, None), histo_color="red5", **kwargs):
457        """
458        A single slice of a Volume which always faces the camera,
459        but at the same time can be oriented arbitrarily in space.
460
461        Arguments:
462            vol : (Volume)
463                the Volume object to be isosurfaced.
464            levels : (list)
465                window and color levels
466            histo_color : (color)
467                histogram color, use `None` to disable it
468            **kwargs : (dict)
469                keyword arguments to pass to Plotter.
470
471        <img src="https://vedo.embl.es/images/volumetric/read_volume3.jpg" width="500">
472        """
473
474        if "shape" not in kwargs:
475            custom_shape = [  # define here the 2 rendering rectangle spaces
476                dict(bottomleft=(0.0, 0.0), topright=(1, 1), bg="k9"),  # the full window
477                dict(bottomleft=(0.8, 0.8), topright=(1, 1), bg="k8", bg2="lb"),
478            ]
479            kwargs["shape"] = custom_shape
480        
481        if "interactive" not in kwargs:
482            kwargs["interactive"] = True
483
484        super().__init__(**kwargs)
485
486        orig_volume = vol.clone(deep=False)
487        self.volume = vol
488
489        self.volume.actor = vtk.new("ImageSlice")
490        self.volume.properties = self.volume.actor.GetProperty()
491
492        self.volume.mapper = vtk.new("ImageResliceMapper")
493        self.volume.mapper.SliceFacesCameraOn()
494        self.volume.mapper.SliceAtFocalPointOn()
495        self.volume.mapper.SetAutoAdjustImageQuality(False)
496        self.volume.mapper.BorderOff()
497        self.volume.properties.SetInterpolationTypeToLinear()
498
499        self.volume.mapper.SetInputData(self.volume.dataset)
500        self.volume.actor.SetMapper(self.volume.mapper)
501
502        # no argument will grab the existing cmap in vol (or use build_lut())
503        self.lut = None
504        self.cmap()
505
506        if levels[0] and levels[1]:
507            self.lighting(window=levels[0], level=levels[1])
508
509        self.usage_txt = (
510            "H                  :rightarrow Toggle this banner on/5off\n"
511            "Left click & drag  :rightarrow Modify luminosity and contrast\n"
512            "SHIFT+Left click   :rightarrow Slice image obliquely\n"
513            "SHIFT+Middle click :rightarrow Slice image perpendicularly\n"
514            "SHIFT+R            :rightarrow Fly to closest cartesian view\n"
515            "SHIFT+U            :rightarrow Toggle parallel projection"
516        )
517
518        self.usage = Text2D(
519            self.usage_txt,
520            font="Calco",
521            pos="top-left",
522            s=0.8,
523            bg="yellow",
524            alpha=0.25,
525        )
526
527        hist = None
528        if histo_color is not None:
529            # try to reduce the number of values to histogram
530            dims = self.volume.dimensions()
531            n = (dims[0]-1) * (dims[1]-1) * (dims[2]-1)
532            n = min(1_000_000, n)
533            arr = np.random.choice(self.volume.pointdata[0], n)
534
535            hist = vedo.pyplot.histogram(
536                arr,
537                bins=12,
538                logscale=True,
539                c=histo_color,
540                ytitle="log_10 (counts)",
541                axes=dict(text_scale=1.9),
542            ).clone2d(pos="bottom-left", scale=0.4)
543
544        axes = kwargs.pop("axes", 7)
545        axe = None
546        if axes == 7:
547            axe = vedo.addons.RulerAxes(
548                orig_volume, xtitle="x - ", ytitle="y - ", ztitle="z - "
549            )
550
551        box = orig_volume.box().alpha(0.25)
552
553        volume_axes_inset = vedo.addons.Axes(
554            box,
555            yzgrid=False, 
556            xlabel_size=0, ylabel_size=0, zlabel_size=0, tip_size=0.08,
557            axes_linewidth=3, 
558            xline_color='dr',  yline_color='dg',  zline_color='db',
559            xtitle_color='dr', ytitle_color='dg', ztitle_color='db',
560            xtitle_size=0.1, ytitle_size=0.1, ztitle_size=0.1,
561            title_font='VictorMono',
562        )
563
564        self.user_mode("image")
565        self.at(0).add(self.volume.actor, box, axe, self.usage, hist)
566        self.at(1).add(orig_volume, volume_axes_inset)
567        self.at(0) # set focus here
568
569    ####################################################################
570    def on_key_press(self, evt):
571        if evt.keypress == "q":
572            self.break_interaction()
573        elif evt.keypress.lower() == "h":
574            t = self.usage
575            if len(t.text()) > 50:
576                self.usage.text("Press H to show help")
577            else:
578                self.usage.text(self.usage_txt)
579            self.render()
580
581    def cmap(self, lut=None, fix_scalar_range=False):
582        """
583        Assign a LUT (Look Up Table) to colorize the slice, leave it `None`
584        to reuse an existing Volume color map.
585        Use "bw" for automatic black and white.
586        """
587        if lut is None and self.lut:
588            self.volume.properties.SetLookupTable(self.lut)
589        elif isinstance(lut, vtk.vtkLookupTable):
590            self.volume.properties.SetLookupTable(lut)
591        elif lut == "bw":
592            self.volume.properties.SetLookupTable(None)
593        self.volume.properties.SetUseLookupTableScalarRange(fix_scalar_range)
594        return self
595
596    def alpha(self, value):
597        """Set opacity to the slice"""
598        self.volume.properties.SetOpacity(value)
599        return self
600
601    def auto_adjust_quality(self, value=True):
602        """Automatically reduce the rendering quality for greater speed when interacting"""
603        self.volume.mapper.SetAutoAdjustImageQuality(value)
604        return self
605
606    def slab(self, thickness=0, mode=0, sample_factor=2):
607        """
608        Make a thick slice (slab).
609
610        Arguments:
611            thickness : (float)
612                set the slab thickness, for thick slicing
613            mode : (int)
614                The slab type:
615                    0 = min
616                    1 = max
617                    2 = mean
618                    3 = sum
619            sample_factor : (float)
620                Set the number of slab samples to use as a factor of the number of input slices
621                within the slab thickness. The default value is 2, but 1 will increase speed
622                with very little loss of quality.
623        """
624        self.volume.mapper.SetSlabThickness(thickness)
625        self.volume.mapper.SetSlabType(mode)
626        self.volume.mapper.SetSlabSampleFactor(sample_factor)
627        return self
628
629    def face_camera(self, value=True):
630        """Make the slice always face the camera or not."""
631        self.volume.mapper.SetSliceFacesCameraOn(value)
632        return self
633
634    def jump_to_nearest_slice(self, value=True):
635        """
636        This causes the slicing to occur at the closest slice to the focal point,
637        instead of the default behavior where a new slice is interpolated between
638        the original slices.
639        Nothing happens if the plane is oblique to the original slices."""
640        self.volume.SetJumpToNearestSlice(value)
641        return self
642
643    def fill_background(self, value=True):
644        """
645        Instead of rendering only to the image border,
646        render out to the viewport boundary with the background color.
647        The background color will be the lowest color on the lookup
648        table that is being used for the image."""
649        self.volume.mapper.SetBackground(value)
650        return self
651
652    def lighting(self, window, level, ambient=1.0, diffuse=0.0):
653        """Assign the values for window and color level."""
654        self.volume.properties.SetColorWindow(window)
655        self.volume.properties.SetColorLevel(level)
656        self.volume.properties.SetAmbient(ambient)
657        self.volume.properties.SetDiffuse(diffuse)
658        return self

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