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

Start playing the slides at a given speed.

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

Generate a Volume isosurfacing controlled by a slider.

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

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

A tool to edit meshes interactively.

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

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):
1487    def init(self, init_points):
1488        """Set an initial number of points to define a region"""
1489        if isinstance(init_points, Points):
1490            self.cpoints = init_points.vertices
1491        else:
1492            self.cpoints = np.array(init_points)
1493        self.points = Points(self.cpoints, r=self.linewidth).c(self.pointcolor).pickable(0)
1494        if self.splined:
1495            self.spline = Spline(self.cpoints, res=len(self.cpoints) * 4)
1496        else:
1497            self.spline = Line(self.cpoints)
1498        self.spline.lw(self.linewidth).c(self.linecolor).pickable(False)
1499        self.jline = Line(self.cpoints[0], self.cpoints[-1], lw=1, c=self.linecolor).pickable(0)
1500        self.add([self.points, self.spline, self.jline]).render()
1501        return self

Set an initial number of points to define a region

def write(self, filename='mesh_edited.vtk') -> FreeHandCutPlotter:
1617    def write(self, filename="mesh_edited.vtk") -> "FreeHandCutPlotter":
1618        """Save the resulting mesh to file"""
1619        self.mesh.write(filename)
1620        vedo.logger.info(f"mesh saved to file {filename}")
1621        return self

Save the resulting mesh to file

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

Start window interaction (with mouse and keyboard)

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

Generate Volume rendering using ray casting.

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