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,
 952                horizontal=True,
 953                pos=[(0.8, 0), (0.97, 0.1)],
 954                font_size=0
 955            )
 956            self.add(self.color_scalarbar)
 957
 958        w1 = self.add_slider(
 959            slider_cmap,
 960            0, Ncols - 1,
 961            value=0,
 962            show_value=False,
 963            c=csl,
 964            pos=[(0.8, 0.05), (0.965, 0.05)],
 965        )
 966        w1.representation.SetTitleHeight(0.018)
 967
 968        ############################## alpha sliders
 969        # Create transfer mapping scalar value to opacity transfer function
 970        def setOTF():
 971            otf = self.properties.GetScalarOpacity()
 972            otf.RemoveAllPoints()
 973            otf.AddPoint(smin, 0.0)
 974            otf.AddPoint(smin + (smax - smin) * 0.1, 0.0)
 975            otf.AddPoint(x0alpha, self.alphaslider0)
 976            otf.AddPoint(x1alpha, self.alphaslider1)
 977            otf.AddPoint(x2alpha, self.alphaslider2)
 978            slider_cmap()
 979
 980        setOTF()  ################
 981
 982        def sliderA0(widget, event):
 983            self.alphaslider0 = widget.value
 984            setOTF()
 985
 986        self.add_slider(
 987            sliderA0,
 988            0, 1,
 989            value=self.alphaslider0,
 990            pos=[(0.84, 0.1), (0.84, 0.26)],
 991            c=csl,
 992            show_value=0,
 993        )
 994
 995        def sliderA1(widget, event):
 996            self.alphaslider1 = widget.value
 997            setOTF()
 998
 999        self.add_slider(
1000            sliderA1,
1001            0, 1,
1002            value=self.alphaslider1,
1003            pos=[(0.89, 0.1), (0.89, 0.26)],
1004            c=csl,
1005            show_value=0,
1006        )
1007
1008        def sliderA2(widget, event):
1009            self.alphaslider2 = widget.value
1010            setOTF()
1011
1012        w2 = self.add_slider(
1013            sliderA2,
1014            0, 1,
1015            value=self.alphaslider2,
1016            pos=[(0.96, 0.1), (0.96, 0.26)],
1017            c=csl,
1018            show_value=0,
1019            title="Opacity Levels",
1020        )
1021        w2.GetRepresentation().SetTitleHeight(0.015)
1022
1023        # add a button
1024        def button_func_mode(_obj, _ename):
1025            s = volume.mode()
1026            snew = (s + 1) % 2
1027            volume.mode(snew)
1028            bum.switch()
1029
1030        bum = self.add_button(
1031            button_func_mode,
1032            pos=(0.89, 0.31),
1033            states=["  composite   ", "max projection"],
1034            c=[ "k3", "k6"],
1035            bc=["k6", "k3"],  # colors of states
1036            font="Calco",
1037            size=18,
1038            bold=0,
1039            italic=False,
1040        )
1041        bum.frame(color="k6")
1042        bum.status(volume.mode())
1043
1044        slider_cmap() ############# init call to create scalarbar
1045
1046        # add histogram of scalar
1047        plot = CornerHistogram(
1048            volume,
1049            bins=25,
1050            logscale=1,
1051            c='k5',
1052            bg='k5',
1053            pos=(0.78, 0.065),
1054            lines=True,
1055            dots=False,
1056            nmax=3.1415e06,  # subsample otherwise is too slow
1057        )
1058
1059        plot.GetPosition2Coordinate().SetValue(0.197, 0.20, 0)
1060        plot.GetXAxisActor2D().SetFontFactor(0.7)
1061        plot.GetProperty().SetOpacity(0.5)
1062        self.add([plot, volume])
1063
1064
1065#####################################################################################
1066class IsosurfaceBrowser(Plotter):
1067    """
1068    Generate a Volume isosurfacing controlled by a slider.
1069    """
1070
1071    def __init__(
1072        self,
1073        volume: vedo.Volume,
1074        isovalue=None,
1075        scalar_range=(),
1076        c=None,
1077        alpha=1,
1078        lego=False,
1079        res=50,
1080        use_gpu=False,
1081        precompute=False,
1082        cmap="hot",
1083        delayed=False,
1084        sliderpos=4,
1085        **kwargs,
1086    ) -> None:
1087        """
1088        Generate a `vedo.Plotter` for Volume isosurfacing using a slider.
1089
1090        Arguments:
1091            volume : (Volume)
1092                the Volume object to be isosurfaced.
1093            isovalues : (float, list)
1094                isosurface value(s) to be displayed.
1095            scalar_range : (list)
1096                scalar range to be used.
1097            c : str, (list)
1098                color(s) of the isosurface(s).
1099            alpha : (float, list)
1100                opacity of the isosurface(s).
1101            lego : (bool)
1102                if True generate a lego plot instead of a surface.
1103            res : (int)
1104                resolution of the isosurface.
1105            use_gpu : (bool)
1106                use GPU acceleration.
1107            precompute : (bool)
1108                precompute the isosurfaces (so slider browsing will be smoother).
1109            cmap : (str)
1110                color map name to be used.
1111            delayed : (bool)
1112                delay the slider update on mouse release.
1113            sliderpos : (int)
1114                position of the slider.
1115            **kwargs : (dict)
1116                keyword arguments to pass to Plotter.
1117
1118        Examples:
1119            - [app_isobrowser.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/app_isobrowser.py)
1120
1121                ![](https://vedo.embl.es/images/advanced/app_isobrowser.gif)
1122        """
1123
1124        super().__init__(**kwargs)
1125
1126        self.slider = None
1127
1128        ### GPU ################################
1129        if use_gpu and hasattr(volume.properties, "GetIsoSurfaceValues"):
1130
1131            if len(scalar_range) == 2:
1132                scrange = scalar_range
1133            else:
1134                scrange = volume.scalar_range()
1135            delta = scrange[1] - scrange[0]
1136            if not delta:
1137                return
1138
1139            if isovalue is None:
1140                isovalue = delta / 3.0 + scrange[0]
1141
1142            ### isovalue slider callback
1143            def slider_isovalue(widget, event):
1144                value = widget.GetRepresentation().GetValue()
1145                isovals.SetValue(0, value)
1146
1147            isovals = volume.properties.GetIsoSurfaceValues()
1148            isovals.SetValue(0, isovalue)
1149            self.add(volume.mode(5).alpha(alpha).cmap(c))
1150
1151            self.slider = self.add_slider(
1152                slider_isovalue,
1153                scrange[0] + 0.02 * delta,
1154                scrange[1] - 0.02 * delta,
1155                value=isovalue,
1156                pos=sliderpos,
1157                title="scalar value",
1158                show_value=True,
1159                delayed=delayed,
1160            )
1161
1162        ### CPU ################################
1163        else:
1164
1165            self._prev_value = 1e30
1166
1167            scrange = volume.scalar_range()
1168            delta = scrange[1] - scrange[0]
1169            if not delta:
1170                return
1171
1172            if lego:
1173                res = int(res / 2)  # because lego is much slower
1174                slidertitle = ""
1175            else:
1176                slidertitle = "scalar value"
1177
1178            allowed_vals = np.linspace(scrange[0], scrange[1], num=res)
1179
1180            bacts = {}  # cache the meshes so we dont need to recompute
1181            if precompute:
1182                delayed = False  # no need to delay the slider in this case
1183
1184                for value in allowed_vals:
1185                    value_name = precision(value, 2)
1186                    if lego:
1187                        mesh = volume.legosurface(vmin=value)
1188                        if mesh.ncells:
1189                            mesh.cmap(cmap, vmin=scrange[0], vmax=scrange[1], on="cells")
1190                    else:
1191                        mesh = volume.isosurface(value).color(c).alpha(alpha)
1192                    bacts.update({value_name: mesh})  # store it
1193
1194            ### isovalue slider callback
1195            def slider_isovalue(widget, event):
1196
1197                prevact = self.vol_actors[0]
1198                if isinstance(widget, float):
1199                    value = widget
1200                else:
1201                    value = widget.GetRepresentation().GetValue()
1202
1203                # snap to the closest
1204                idx = (np.abs(allowed_vals - value)).argmin()
1205                value = allowed_vals[idx]
1206
1207                if abs(value - self._prev_value) / delta < 0.001:
1208                    return
1209                self._prev_value = value
1210
1211                value_name = precision(value, 2)
1212                if value_name in bacts:  # reusing the already existing mesh
1213                    # print('reusing')
1214                    mesh = bacts[value_name]
1215                else:  # else generate it
1216                    # print('generating', value)
1217                    if lego:
1218                        mesh = volume.legosurface(vmin=value)
1219                        if mesh.ncells:
1220                            mesh.cmap(cmap, vmin=scrange[0], vmax=scrange[1], on="cells")
1221                    else:
1222                        mesh = volume.isosurface(value).color(c).alpha(alpha)
1223                    bacts.update({value_name: mesh})  # store it
1224
1225                self.remove(prevact).add(mesh)
1226                self.vol_actors[0] = mesh
1227
1228            ################################################
1229
1230            if isovalue is None:
1231                isovalue = delta / 3.0 + scrange[0]
1232
1233            self.vol_actors = [None]
1234            slider_isovalue(isovalue, "")  # init call
1235            if lego:
1236                if self.vol_actors[0]:
1237                    self.vol_actors[0].add_scalarbar(pos=(0.8, 0.12))
1238
1239            self.slider = self.add_slider(
1240                slider_isovalue,
1241                scrange[0] + 0.02 * delta,
1242                scrange[1] - 0.02 * delta,
1243                value=isovalue,
1244                pos=sliderpos,
1245                title=slidertitle,
1246                show_value=True,
1247                delayed=delayed,
1248            )
1249
1250
1251##############################################################################
1252class Browser(Plotter):
1253    """Browse a series of vedo objects by using a simple slider."""
1254
1255    def __init__(
1256        self,
1257        objects=(),
1258        sliderpos=((0.50, 0.07), (0.95, 0.07)),
1259        c=None,  # slider color
1260        slider_title="",
1261        font="Calco",  # slider font
1262        resetcam=False,  # resetcam while using the slider
1263        **kwargs,
1264    ):
1265        """
1266        Browse a series of vedo objects by using a simple slider.
1267
1268        The input object can be a list of objects or a list of lists of objects.
1269
1270        Arguments:
1271            objects : (list)
1272                list of objects to be browsed.
1273            sliderpos : (list)
1274                position of the slider.
1275            c : (str)
1276                color of the slider.
1277            slider_title : (str)
1278                title of the slider.
1279            font : (str)
1280                font of the slider.
1281            resetcam : (bool)
1282                resetcam while using the slider.
1283            **kwargs : (dict)
1284                keyword arguments to pass to Plotter.
1285
1286        Examples:
1287            ```python
1288            from vedo import load, dataurl
1289            from vedo.applications import Browser
1290            meshes = load(dataurl+'timecourse1d.npy') # python list of Meshes
1291            plt = Browser(meshes, bg='k')             # vedo.Plotter
1292            plt.show(interactive=False, zoom='tight') # show the meshes
1293            plt.play(dt=50)                           # delay in milliseconds
1294            plt.close()
1295            ```
1296
1297        - [morphomatics_tube.py](https://github.com/marcomusy/vedo/tree/master/examples/other/morphomatics_tube.py)
1298        """
1299        kwargs.pop("N", 1)
1300        kwargs.pop("shape", [])
1301        kwargs.pop("axes", 1)
1302        super().__init__(**kwargs)
1303
1304        if isinstance(objects, str):
1305            objects = vedo.file_io.load(objects)
1306
1307        self += objects
1308
1309        if len(objects) and is_sequence(objects[0]):
1310            nobs = len(objects[0])
1311            for ob in objects:
1312                n = len(ob)
1313                msg = f"in Browser lists must have the same length but found {n} and {nobs}"
1314                assert len(ob) == nobs, msg
1315        else:
1316            nobs = len(objects)
1317            if nobs:
1318                objects = [objects]
1319
1320        self.slider = None
1321        self.timer_callback_id = None
1322        self._oldk = None
1323
1324        # define the slider func ##########################
1325        def slider_function(widget=None, event=None):
1326
1327            k = int(self.slider.value)
1328
1329            if k == self._oldk:
1330                return  # no change
1331            self._oldk = k
1332
1333            n = len(objects)
1334            m = len(objects[0])
1335            for i in range(n):
1336                for j in range(m):
1337                    ak = objects[i][j]
1338                    try:
1339                        if j == k:
1340                            ak.on()
1341                            akon = ak
1342                        else:
1343                            ak.off()
1344                    except AttributeError:
1345                        pass
1346
1347            try:
1348                tx = str(k)
1349                if slider_title:
1350                    tx = slider_title + " " + tx
1351                elif n == 1 and akon.filename:
1352                    tx = akon.filename.split("/")[-1]
1353                    tx = tx.split("\\")[-1]  # windows os
1354                elif akon.name:
1355                    tx = ak.name + " " + tx
1356            except:
1357                pass
1358            self.slider.title = tx
1359
1360            if resetcam:
1361                self.reset_camera()
1362            self.render()
1363
1364        ##################################################
1365
1366        self.slider_function = slider_function
1367        self.slider = self.add_slider(
1368            slider_function,
1369            0.5,
1370            nobs - 0.5,
1371            pos=sliderpos,
1372            font=font,
1373            c=c,
1374            show_value=False,
1375        )
1376        self.slider.GetRepresentation().SetTitleHeight(0.020)
1377        slider_function()  # init call
1378
1379    def play(self, dt=100):
1380        """Start playing the slides at a given speed."""
1381        self.timer_callback_id = self.add_callback("timer", self.slider_function)
1382        self.timer_callback("start", dt=dt)
1383        self.interactive()
1384
1385
1386#############################################################################################
1387class FreeHandCutPlotter(Plotter):
1388    """A tool to edit meshes interactively."""
1389
1390    # thanks to Jakub Kaminski for the original version of this script
1391    def __init__(
1392        self,
1393        mesh: Union[vedo.Mesh, vedo.Points],
1394        splined=True,
1395        font="Bongas",
1396        alpha=0.9,
1397        lw=4,
1398        lc="red5",
1399        pc="red4",
1400        c="green3",
1401        tc="k9",
1402        tol=0.008,
1403        **options,
1404    ):
1405        """
1406        A `vedo.Plotter` derived class which edits polygonal meshes interactively.
1407
1408        Can also be invoked from command line with:
1409
1410        ```bash
1411        vedo --edit https://vedo.embl.es/examples/data/porsche.ply
1412        ```
1413
1414        Usage:
1415            - Left-click and hold to rotate
1416            - Right-click and move to draw line
1417            - Second right-click to stop drawing
1418            - Press "c" to clear points
1419            -       "z/Z" to cut mesh (Z inverts inside-out the selection area)
1420            -       "L" to keep only the largest connected surface
1421            -       "s" to save mesh to file (tag `_edited` is appended to filename)
1422            -       "u" to undo last action
1423            -       "h" for help, "i" for info
1424
1425        Arguments:
1426            mesh : (Mesh, Points)
1427                The input Mesh or pointcloud.
1428            splined : (bool)
1429                join points with a spline or a simple line.
1430            font : (str)
1431                Font name for the instructions.
1432            alpha : (float)
1433                transparency of the instruction message panel.
1434            lw : (str)
1435                selection line width.
1436            lc : (str)
1437                selection line color.
1438            pc : (str)
1439                selection points color.
1440            c : (str)
1441                background color of instructions.
1442            tc : (str)
1443                text color of instructions.
1444            tol : (int)
1445                tolerance of the point proximity.
1446            **kwargs : (dict)
1447                keyword arguments to pass to Plotter.
1448
1449        Examples:
1450            - [cut_freehand.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/cut_freehand.py)
1451
1452                ![](https://vedo.embl.es/images/basic/cutFreeHand.gif)
1453        """
1454
1455        if not isinstance(mesh, Points):
1456            vedo.logger.error("FreeHandCutPlotter input must be Points or Mesh")
1457            raise RuntimeError()
1458
1459        super().__init__(**options)
1460
1461        self.mesh = mesh
1462        self.mesh_prev = mesh
1463        self.splined = splined
1464        self.linecolor = lc
1465        self.linewidth = lw
1466        self.pointcolor = pc
1467        self.color = c
1468        self.alpha = alpha
1469
1470        self.msg = "Right-click and move to draw line\n"
1471        self.msg += "Second right-click to stop drawing\n"
1472        self.msg += "Press L to extract largest surface\n"
1473        self.msg += "        z/Z to cut mesh (s to save)\n"
1474        self.msg += "        c to clear points, u to undo"
1475        self.txt2d = Text2D(self.msg, pos="top-left", font=font, s=0.9)
1476        self.txt2d.c(tc).background(c, alpha).frame()
1477
1478        self.idkeypress = self.add_callback("KeyPress", self._on_keypress)
1479        self.idrightclck = self.add_callback("RightButton", self._on_right_click)
1480        self.idmousemove = self.add_callback("MouseMove", self._on_mouse_move)
1481        self.drawmode = False
1482        self.tol = tol  # tolerance of point distance
1483        self.cpoints = []
1484        self.points = None
1485        self.spline = None
1486        self.jline = None
1487        self.topline = None
1488        self.top_pts = []
1489
1490    def init(self, init_points):
1491        """Set an initial number of points to define a region"""
1492        if isinstance(init_points, Points):
1493            self.cpoints = init_points.vertices
1494        else:
1495            self.cpoints = np.array(init_points)
1496        self.points = Points(self.cpoints, r=self.linewidth).c(self.pointcolor).pickable(0)
1497        if self.splined:
1498            self.spline = Spline(self.cpoints, res=len(self.cpoints) * 4)
1499        else:
1500            self.spline = Line(self.cpoints)
1501        self.spline.lw(self.linewidth).c(self.linecolor).pickable(False)
1502        self.jline = Line(self.cpoints[0], self.cpoints[-1], lw=1, c=self.linecolor).pickable(0)
1503        self.add([self.points, self.spline, self.jline]).render()
1504        return self
1505
1506    def _on_right_click(self, evt):
1507        self.drawmode = not self.drawmode  # toggle mode
1508        if self.drawmode:
1509            self.txt2d.background(self.linecolor, self.alpha)
1510        else:
1511            self.txt2d.background(self.color, self.alpha)
1512            if len(self.cpoints) > 2:
1513                self.remove([self.spline, self.jline])
1514                if self.splined:  # show the spline closed
1515                    self.spline = Spline(self.cpoints, closed=True, res=len(self.cpoints) * 4)
1516                else:
1517                    self.spline = Line(self.cpoints, closed=True)
1518                self.spline.lw(self.linewidth).c(self.linecolor).pickable(False)
1519                self.add(self.spline)
1520        self.render()
1521
1522    def _on_mouse_move(self, evt):
1523        if self.drawmode:
1524            cpt = self.compute_world_coordinate(evt.picked2d)  # make this 2d-screen point 3d
1525            if self.cpoints and mag(cpt - self.cpoints[-1]) < self.mesh.diagonal_size() * self.tol:
1526                return  # new point is too close to the last one. skip
1527            self.cpoints.append(cpt)
1528            if len(self.cpoints) > 2:
1529                self.remove([self.points, self.spline, self.jline, self.topline])
1530                self.points = Points(self.cpoints, r=self.linewidth).c(self.pointcolor).pickable(0)
1531                if self.splined:
1532                    self.spline = Spline(self.cpoints, res=len(self.cpoints) * 4)  # not closed here
1533                else:
1534                    self.spline = Line(self.cpoints)
1535
1536                if evt.actor:
1537                    self.top_pts.append(evt.picked3d)
1538                    self.topline = Points(self.top_pts, r=self.linewidth)
1539                    self.topline.c(self.linecolor).pickable(False)
1540
1541                self.spline.lw(self.linewidth).c(self.linecolor).pickable(False)
1542                self.txt2d.background(self.linecolor)
1543                self.jline = Line(self.cpoints[0], self.cpoints[-1], lw=1, c=self.linecolor).pickable(0)
1544                self.add([self.points, self.spline, self.jline, self.topline]).render()
1545
1546    def _on_keypress(self, evt):
1547        if evt.keypress.lower() == "z" and self.spline:  # Cut mesh with a ribbon-like surface
1548            inv = False
1549            if evt.keypress == "Z":
1550                inv = True
1551            self.txt2d.background("red8").text("  ... working ...  ")
1552            self.render()
1553            self.mesh_prev = self.mesh.clone()
1554            tol = self.mesh.diagonal_size() / 2  # size of ribbon (not shown)
1555            pts = self.spline.vertices
1556            n = fit_plane(pts, signed=True).normal  # compute normal vector to points
1557            rb = Ribbon(pts - tol * n, pts + tol * n, closed=True)
1558            self.mesh.cut_with_mesh(rb, invert=inv)  # CUT
1559            self.txt2d.text(self.msg)  # put back original message
1560            if self.drawmode:
1561                self._on_right_click(evt)  # toggle mode to normal
1562            else:
1563                self.txt2d.background(self.color, self.alpha)
1564            self.remove([self.spline, self.points, self.jline, self.topline]).render()
1565            self.cpoints, self.points, self.spline = [], None, None
1566            self.top_pts, self.topline = [], None
1567
1568        elif evt.keypress == "L":
1569            self.txt2d.background("red8")
1570            self.txt2d.text(" ... removing smaller ... \n ... parts of the mesh ... ")
1571            self.render()
1572            self.remove(self.mesh)
1573            self.mesh_prev = self.mesh
1574            mcut = self.mesh.extract_largest_region()
1575            mcut.filename = self.mesh.filename  # copy over various properties
1576            mcut.name = self.mesh.name
1577            mcut.scalarbar = self.mesh.scalarbar
1578            mcut.info = self.mesh.info
1579            self.mesh = mcut                            # discard old mesh by overwriting it
1580            self.txt2d.text(self.msg).background(self.color)   # put back original message
1581            self.add(mcut).render()
1582
1583        elif evt.keypress == "u":  # Undo last action
1584            if self.drawmode:
1585                self._on_right_click(evt)  # toggle mode to normal
1586            else:
1587                self.txt2d.background(self.color, self.alpha)
1588            self.remove([self.mesh, self.spline, self.jline, self.points, self.topline])
1589            self.mesh = self.mesh_prev
1590            self.cpoints, self.points, self.spline = [], None, None
1591            self.top_pts, self.topline = [], None
1592            self.add(self.mesh).render()
1593
1594        elif evt.keypress in ("c", "Delete"):
1595            # clear all points
1596            self.remove([self.spline, self.points, self.jline, self.topline]).render()
1597            self.cpoints, self.points, self.spline = [], None, None
1598            self.top_pts, self.topline = [], None
1599
1600        elif evt.keypress == "r":  # reset camera and axes
1601            try:
1602                self.remove(self.axes_instances[0])
1603                self.axes_instances[0] = None
1604                self.add_global_axes(axtype=1, c=None, bounds=self.mesh.bounds())
1605                self.renderer.ResetCamera()
1606                self.render()
1607            except:
1608                pass
1609
1610        elif evt.keypress == "s":
1611            if self.mesh.filename:
1612                fname = os.path.basename(self.mesh.filename)
1613                fname, extension = os.path.splitext(fname)
1614                fname = fname.replace("_edited", "")
1615                fname = f"{fname}_edited{extension}"
1616            else:
1617                fname = "mesh_edited.vtk"
1618            self.write(fname)
1619
1620    def write(self, filename="mesh_edited.vtk") -> "FreeHandCutPlotter":
1621        """Save the resulting mesh to file"""
1622        self.mesh.write(filename)
1623        vedo.logger.info(f"mesh saved to file {filename}")
1624        return self
1625
1626    def start(self, *args, **kwargs) -> "FreeHandCutPlotter":
1627        """Start window interaction (with mouse and keyboard)"""
1628        acts = [self.txt2d, self.mesh, self.points, self.spline, self.jline]
1629        self.show(acts + list(args), **kwargs)
1630        return self
1631
1632
1633########################################################################
1634class SplinePlotter(Plotter):
1635    """
1636    Interactive drawing of splined curves on meshes.
1637    """
1638
1639    def __init__(self, obj, init_points=(), closed=False, splined=True, mode="auto", **kwargs):
1640        """
1641        Create an interactive application that allows the user to click points and
1642        retrieve the coordinates of such points and optionally a spline or line
1643        (open or closed).
1644        Input object can be a image file name or a 3D mesh.
1645
1646        Arguments:
1647            obj : (Mesh, str)
1648                The input object can be a image file name or a 3D mesh.
1649            init_points : (list)
1650                Set an initial number of points to define a region.
1651            closed : (bool)
1652                Close the spline or line.
1653            splined : (bool)
1654                Join points with a spline or a simple line.
1655            mode : (str)
1656                Set the mode of interaction.
1657            **kwargs : (dict)
1658                keyword arguments to pass to Plotter.
1659        """
1660        super().__init__(**kwargs)
1661
1662        self.verbose = True
1663        self.splined = splined
1664        self.resolution = None  # spline resolution (None = automatic)
1665        self.closed = closed
1666        self.lcolor = "yellow4"
1667        self.lwidth = 3
1668        self.pcolor = "purple5"
1669        self.psize = 10
1670
1671        self.cpoints = list(init_points)
1672        self.vpoints = None
1673        self.line = None
1674
1675        if isinstance(obj, str):
1676            self.object = vedo.file_io.load(obj)
1677        else:
1678            self.object = obj
1679
1680        self.mode = mode
1681        if self.mode == "auto":
1682            if isinstance(self.object, vedo.Image):
1683                self.mode = "image"
1684                self.parallel_projection(True)
1685            else:
1686                self.mode = "TrackballCamera"
1687
1688        t = (
1689            "Click to add a point\n"
1690            "Right-click to remove it\n"
1691            "Drag mouse to change contrast\n"
1692            "Press c to clear points\n"
1693            "Press q to continue"
1694        )
1695        self.instructions = Text2D(t, pos="bottom-left", c="white", bg="green", font="Calco")
1696
1697        self += [self.object, self.instructions]
1698
1699        self.callid1 = self.add_callback("KeyPress", self._key_press)
1700        self.callid2 = self.add_callback("LeftButtonPress", self._on_left_click)
1701        self.callid3 = self.add_callback("RightButtonPress", self._on_right_click)
1702
1703
1704    def points(self, newpts=None) -> Union["SplinePlotter", np.ndarray]:
1705        """Retrieve the 3D coordinates of the clicked points"""
1706        if newpts is not None:
1707            self.cpoints = newpts
1708            self.update()
1709            return self
1710        return np.array(self.cpoints)
1711
1712    def _on_left_click(self, evt):
1713        if not evt.actor:
1714            return
1715        if evt.actor.name == "points":
1716            # remove clicked point if clicked twice
1717            pid = self.vpoints.closest_point(evt.picked3d, return_point_id=True)
1718            self.cpoints.pop(pid)
1719            self.update()
1720            return
1721        p = evt.picked3d
1722        self.cpoints.append(p)
1723        self.update()
1724        if self.verbose:
1725            vedo.colors.printc("Added point:", precision(p, 4), c="g")
1726
1727    def _on_right_click(self, evt):
1728        if evt.actor and len(self.cpoints) > 0:
1729            self.cpoints.pop()  # pop removes from the list the last pt
1730            self.update()
1731            if self.verbose:
1732                vedo.colors.printc("Deleted last point", c="r")
1733
1734    def update(self):
1735        self.remove(self.line, self.vpoints)  # remove old points and spline
1736        self.vpoints = Points(self.cpoints).ps(self.psize).c(self.pcolor)
1737        self.vpoints.name = "points"
1738        self.vpoints.pickable(True)  # to allow toggle
1739        minnr = 1
1740        if self.splined:
1741            minnr = 2
1742        if self.lwidth and len(self.cpoints) > minnr:
1743            if self.splined:
1744                try:
1745                    self.line = Spline(self.cpoints, closed=self.closed, res=self.resolution)
1746                except ValueError:
1747                    # if clicking too close splining might fail
1748                    self.cpoints.pop()
1749                    return
1750            else:
1751                self.line = Line(self.cpoints, closed=self.closed)
1752            self.line.c(self.lcolor).lw(self.lwidth).pickable(False)
1753            self.add(self.vpoints, self.line)
1754        else:
1755            self.add(self.vpoints)
1756
1757    def _key_press(self, evt):
1758        if evt.keypress == "c":
1759            self.cpoints = []
1760            self.remove(self.line, self.vpoints).render()
1761            if self.verbose:
1762                vedo.colors.printc("==== Cleared all points ====", c="r", invert=True)
1763
1764    def start(self) -> "SplinePlotter":
1765        """Start the interaction"""
1766        self.update()
1767        self.show(self.object, self.instructions, mode=self.mode)
1768        return self
1769
1770
1771########################################################################
1772class Animation(Plotter):
1773    """
1774    A `Plotter` derived class that allows to animate simultaneously various objects
1775    by specifying event times and durations of different visual effects.
1776
1777    Arguments:
1778        total_duration : (float)
1779            expand or shrink the total duration of video to this value
1780        time_resolution : (float)
1781            in seconds, save a frame at this rate
1782        show_progressbar : (bool)
1783            whether to show a progress bar or not
1784        video_filename : (str)
1785            output file name of the video
1786        video_fps : (int)
1787            desired value of the nr of frames per second
1788
1789    .. warning:: this is still very experimental at the moment.
1790    """
1791
1792    def __init__(
1793        self,
1794        total_duration=None,
1795        time_resolution=0.02,
1796        show_progressbar=True,
1797        video_filename="animation.mp4",
1798        video_fps=12,
1799    ):
1800        super().__init__()
1801        self.resetcam = True
1802
1803        self.events = []
1804        self.time_resolution = time_resolution
1805        self.total_duration = total_duration
1806        self.show_progressbar = show_progressbar
1807        self.video_filename = video_filename
1808        self.video_fps = video_fps
1809        self.bookingMode = True
1810        self._inputvalues = []
1811        self._performers = []
1812        self._lastT = None
1813        self._lastDuration = None
1814        self._lastActs = None
1815        self.eps = 0.00001
1816
1817    def _parse(self, objs, t, duration):
1818        if t is None:
1819            if self._lastT:
1820                t = self._lastT
1821            else:
1822                t = 0.0
1823        if duration is None:
1824            if self._lastDuration:
1825                duration = self._lastDuration
1826            else:
1827                duration = 0.0
1828        if objs is None:
1829            if self._lastActs:
1830                objs = self._lastActs
1831            else:
1832                vedo.logger.error("Need to specify actors!")
1833                raise RuntimeError
1834
1835        objs2 = objs
1836
1837        if is_sequence(objs):
1838            objs2 = objs
1839        else:
1840            objs2 = [objs]
1841
1842        # quantize time steps and duration
1843        t = int(t / self.time_resolution + 0.5) * self.time_resolution
1844        nsteps = int(duration / self.time_resolution + 0.5)
1845        duration = nsteps * self.time_resolution
1846
1847        rng = np.linspace(t, t + duration, nsteps + 1)
1848
1849        self._lastT = t
1850        self._lastDuration = duration
1851        self._lastActs = objs2
1852
1853        for a in objs2:
1854            if a not in self.objects:
1855                self.objects.append(a)
1856
1857        return objs2, t, duration, rng
1858
1859    def switch_on(self, acts=None, t=None):
1860        """Switch on the input list of meshes."""
1861        return self.fade_in(acts, t, 0)
1862
1863    def switch_off(self, acts=None, t=None):
1864        """Switch off the input list of meshes."""
1865        return self.fade_out(acts, t, 0)
1866
1867    def fade_in(self, acts=None, t=None, duration=None):
1868        """Gradually switch on the input list of meshes by increasing opacity."""
1869        if self.bookingMode:
1870            acts, t, duration, rng = self._parse(acts, t, duration)
1871            for tt in rng:
1872                alpha = lin_interpolate(tt, [t, t + duration], [0, 1])
1873                self.events.append((tt, self.fade_in, acts, alpha))
1874        else:
1875            for a in self._performers:
1876                if hasattr(a, "alpha"):
1877                    if a.alpha() >= self._inputvalues:
1878                        continue
1879                    a.alpha(self._inputvalues)
1880        return self
1881
1882    def fade_out(self, acts=None, t=None, duration=None):
1883        """Gradually switch off the input list of meshes by increasing transparency."""
1884        if self.bookingMode:
1885            acts, t, duration, rng = self._parse(acts, t, duration)
1886            for tt in rng:
1887                alpha = lin_interpolate(tt, [t, t + duration], [1, 0])
1888                self.events.append((tt, self.fade_out, acts, alpha))
1889        else:
1890            for a in self._performers:
1891                if a.alpha() <= self._inputvalues:
1892                    continue
1893                a.alpha(self._inputvalues)
1894        return self
1895
1896    def change_alpha_between(self, alpha1, alpha2, acts=None, t=None, duration=None):
1897        """Gradually change transparency for the input list of meshes."""
1898        if self.bookingMode:
1899            acts, t, duration, rng = self._parse(acts, t, duration)
1900            for tt in rng:
1901                alpha = lin_interpolate(tt, [t, t + duration], [alpha1, alpha2])
1902                self.events.append((tt, self.fade_out, acts, alpha))
1903        else:
1904            for a in self._performers:
1905                a.alpha(self._inputvalues)
1906        return self
1907
1908    def change_color(self, c, acts=None, t=None, duration=None):
1909        """Gradually change color for the input list of meshes."""
1910        if self.bookingMode:
1911            acts, t, duration, rng = self._parse(acts, t, duration)
1912
1913            col2 = get_color(c)
1914            for tt in rng:
1915                inputvalues = []
1916                for a in acts:
1917                    col1 = a.color()
1918                    r = lin_interpolate(tt, [t, t + duration], [col1[0], col2[0]])
1919                    g = lin_interpolate(tt, [t, t + duration], [col1[1], col2[1]])
1920                    b = lin_interpolate(tt, [t, t + duration], [col1[2], col2[2]])
1921                    inputvalues.append((r, g, b))
1922                self.events.append((tt, self.change_color, acts, inputvalues))
1923        else:
1924            for i, a in enumerate(self._performers):
1925                a.color(self._inputvalues[i])
1926        return self
1927
1928    def change_backcolor(self, c, acts=None, t=None, duration=None):
1929        """Gradually change backface color for the input list of meshes.
1930        An initial backface color should be set in advance."""
1931        if self.bookingMode:
1932            acts, t, duration, rng = self._parse(acts, t, duration)
1933
1934            col2 = get_color(c)
1935            for tt in rng:
1936                inputvalues = []
1937                for a in acts:
1938                    if a.GetBackfaceProperty():
1939                        col1 = a.backColor()
1940                        r = lin_interpolate(tt, [t, t + duration], [col1[0], col2[0]])
1941                        g = lin_interpolate(tt, [t, t + duration], [col1[1], col2[1]])
1942                        b = lin_interpolate(tt, [t, t + duration], [col1[2], col2[2]])
1943                        inputvalues.append((r, g, b))
1944                    else:
1945                        inputvalues.append(None)
1946                self.events.append((tt, self.change_backcolor, acts, inputvalues))
1947        else:
1948            for i, a in enumerate(self._performers):
1949                a.backColor(self._inputvalues[i])
1950        return self
1951
1952    def change_to_wireframe(self, acts=None, t=None):
1953        """Switch representation to wireframe for the input list of meshes at time `t`."""
1954        if self.bookingMode:
1955            acts, t, _, _ = self._parse(acts, t, None)
1956            self.events.append((t, self.change_to_wireframe, acts, True))
1957        else:
1958            for a in self._performers:
1959                a.wireframe(self._inputvalues)
1960        return self
1961
1962    def change_to_surface(self, acts=None, t=None):
1963        """Switch representation to surface for the input list of meshes at time `t`."""
1964        if self.bookingMode:
1965            acts, t, _, _ = self._parse(acts, t, None)
1966            self.events.append((t, self.change_to_surface, acts, False))
1967        else:
1968            for a in self._performers:
1969                a.wireframe(self._inputvalues)
1970        return self
1971
1972    def change_line_width(self, lw, acts=None, t=None, duration=None):
1973        """Gradually change line width of the mesh edges for the input list of meshes."""
1974        if self.bookingMode:
1975            acts, t, duration, rng = self._parse(acts, t, duration)
1976            for tt in rng:
1977                inputvalues = []
1978                for a in acts:
1979                    newlw = lin_interpolate(tt, [t, t + duration], [a.lw(), lw])
1980                    inputvalues.append(newlw)
1981                self.events.append((tt, self.change_line_width, acts, inputvalues))
1982        else:
1983            for i, a in enumerate(self._performers):
1984                a.lw(self._inputvalues[i])
1985        return self
1986
1987    def change_line_color(self, c, acts=None, t=None, duration=None):
1988        """Gradually change line color of the mesh edges for the input list of meshes."""
1989        if self.bookingMode:
1990            acts, t, duration, rng = self._parse(acts, t, duration)
1991            col2 = get_color(c)
1992            for tt in rng:
1993                inputvalues = []
1994                for a in acts:
1995                    col1 = a.linecolor()
1996                    r = lin_interpolate(tt, [t, t + duration], [col1[0], col2[0]])
1997                    g = lin_interpolate(tt, [t, t + duration], [col1[1], col2[1]])
1998                    b = lin_interpolate(tt, [t, t + duration], [col1[2], col2[2]])
1999                    inputvalues.append((r, g, b))
2000                self.events.append((tt, self.change_line_color, acts, inputvalues))
2001        else:
2002            for i, a in enumerate(self._performers):
2003                a.linecolor(self._inputvalues[i])
2004        return self
2005
2006    def change_lighting(self, style, acts=None, t=None, duration=None):
2007        """Gradually change the lighting style for the input list of meshes.
2008
2009        Allowed styles are: [metallic, plastic, shiny, glossy, default].
2010        """
2011        if self.bookingMode:
2012            acts, t, duration, rng = self._parse(acts, t, duration)
2013
2014            c = (1,1,0.99)
2015            if   style=='metallic': pars = [0.1, 0.3, 1.0, 10, c]
2016            elif style=='plastic' : pars = [0.3, 0.4, 0.3,  5, c]
2017            elif style=='shiny'   : pars = [0.2, 0.6, 0.8, 50, c]
2018            elif style=='glossy'  : pars = [0.1, 0.7, 0.9, 90, c]
2019            elif style=='default' : pars = [0.1, 1.0, 0.05, 5, c]
2020            else:
2021                vedo.logger.error(f"Unknown lighting style {style}")
2022
2023            for tt in rng:
2024                inputvalues = []
2025                for a in acts:
2026                    pr = a.properties
2027                    aa = pr.GetAmbient()
2028                    ad = pr.GetDiffuse()
2029                    asp = pr.GetSpecular()
2030                    aspp = pr.GetSpecularPower()
2031                    naa = lin_interpolate(tt, [t, t + duration], [aa, pars[0]])
2032                    nad = lin_interpolate(tt, [t, t + duration], [ad, pars[1]])
2033                    nasp = lin_interpolate(tt, [t, t + duration], [asp, pars[2]])
2034                    naspp = lin_interpolate(tt, [t, t + duration], [aspp, pars[3]])
2035                    inputvalues.append((naa, nad, nasp, naspp))
2036                self.events.append((tt, self.change_lighting, acts, inputvalues))
2037        else:
2038            for i, a in enumerate(self._performers):
2039                pr = a.properties
2040                vals = self._inputvalues[i]
2041                pr.SetAmbient(vals[0])
2042                pr.SetDiffuse(vals[1])
2043                pr.SetSpecular(vals[2])
2044                pr.SetSpecularPower(vals[3])
2045        return self
2046
2047    def move(self, act=None, pt=(0, 0, 0), t=None, duration=None, style="linear"):
2048        """Smoothly change the position of a specific object to a new point in space."""
2049        if self.bookingMode:
2050            acts, t, duration, rng = self._parse(act, t, duration)
2051            if len(acts) != 1:
2052                vedo.logger.error("in move(), can move only one object.")
2053            cpos = acts[0].pos()
2054            pt = np.array(pt)
2055            dv = (pt - cpos) / len(rng)
2056            for j, tt in enumerate(rng):
2057                i = j + 1
2058                if "quad" in style:
2059                    x = i / len(rng)
2060                    y = x * x
2061                    self.events.append((tt, self.move, acts, cpos + dv * i * y))
2062                else:
2063                    self.events.append((tt, self.move, acts, cpos + dv * i))
2064        else:
2065            self._performers[0].pos(self._inputvalues)
2066        return self
2067
2068    def rotate(self, act=None, axis=(1, 0, 0), angle=0, t=None, duration=None):
2069        """Smoothly rotate a specific object by a specified angle and axis."""
2070        if self.bookingMode:
2071            acts, t, duration, rng = self._parse(act, t, duration)
2072            if len(acts) != 1:
2073                vedo.logger.error("in rotate(), can move only one object.")
2074            for tt in rng:
2075                ang = angle / len(rng)
2076                self.events.append((tt, self.rotate, acts, (axis, ang)))
2077        else:
2078            ax = self._inputvalues[0]
2079            if ax == "x":
2080                self._performers[0].rotate_x(self._inputvalues[1])
2081            elif ax == "y":
2082                self._performers[0].rotate_y(self._inputvalues[1])
2083            elif ax == "z":
2084                self._performers[0].rotate_z(self._inputvalues[1])
2085        return self
2086
2087    def scale(self, acts=None, factor=1, t=None, duration=None):
2088        """Smoothly scale a specific object to a specified scale factor."""
2089        if self.bookingMode:
2090            acts, t, duration, rng = self._parse(acts, t, duration)
2091            for tt in rng:
2092                fac = lin_interpolate(tt, [t, t + duration], [1, factor])
2093                self.events.append((tt, self.scale, acts, fac))
2094        else:
2095            for a in self._performers:
2096                a.scale(self._inputvalues)
2097        return self
2098
2099    def mesh_erode(self, act=None, corner=6, t=None, duration=None):
2100        """Erode a mesh by removing cells that are close to one of the 8 corners
2101        of the bounding box.
2102        """
2103        if self.bookingMode:
2104            acts, t, duration, rng = self._parse(act, t, duration)
2105            if len(acts) != 1:
2106                vedo.logger.error("in meshErode(), can erode only one object.")
2107            diag = acts[0].diagonal_size()
2108            x0, x1, y0, y1, z0, z1 = acts[0].GetBounds()
2109            corners = [
2110                (x0, y0, z0),
2111                (x1, y0, z0),
2112                (x1, y1, z0),
2113                (x0, y1, z0),
2114                (x0, y0, z1),
2115                (x1, y0, z1),
2116                (x1, y1, z1),
2117                (x0, y1, z1),
2118            ]
2119            pcl = acts[0].closest_point(corners[corner])
2120            dmin = np.linalg.norm(pcl - corners[corner])
2121            for tt in rng:
2122                d = lin_interpolate(tt, [t, t + duration], [dmin, diag * 1.01])
2123                if d > 0:
2124                    ids = acts[0].closest_point(corners[corner], radius=d, return_point_id=True)
2125                    if len(ids) <= acts[0].npoints:
2126                        self.events.append((tt, self.mesh_erode, acts, ids))
2127        return self
2128
2129    def play(self):
2130        """Play the internal list of events and save a video."""
2131
2132        self.events = sorted(self.events, key=lambda x: x[0])
2133        self.bookingMode = False
2134
2135        if self.show_progressbar:
2136            pb = vedo.ProgressBar(0, len(self.events), c="g")
2137
2138        if self.total_duration is None:
2139            self.total_duration = self.events[-1][0] - self.events[0][0]
2140
2141        if self.video_filename:
2142            vd = vedo.Video(self.video_filename, fps=self.video_fps, duration=self.total_duration)
2143
2144        ttlast = 0
2145        for e in self.events:
2146
2147            tt, action, self._performers, self._inputvalues = e
2148            action(0, 0)
2149
2150            dt = tt - ttlast
2151            if dt > self.eps:
2152                self.show(interactive=False, resetcam=self.resetcam)
2153                if self.video_filename:
2154                    vd.add_frame()
2155
2156                if dt > self.time_resolution + self.eps:
2157                    if self.video_filename:
2158                        vd.pause(dt)
2159
2160            ttlast = tt
2161
2162            if self.show_progressbar:
2163                pb.print("t=" + str(int(tt * 100) / 100) + "s,  " + action.__name__)
2164
2165        self.show(interactive=False, resetcam=self.resetcam)
2166        if self.video_filename:
2167            vd.add_frame()
2168            vd.close()
2169
2170        self.show(interactive=True, resetcam=self.resetcam)
2171        self.bookingMode = True
2172
2173
2174########################################################################
2175class AnimationPlayer(vedo.Plotter):
2176    """
2177    A Plotter with play/pause, step forward/backward and slider functionalties.
2178    Useful for inspecting time series.
2179
2180    The user has the responsibility to update all actors in the callback function.
2181
2182    Arguments:
2183        func :  (Callable)
2184            a function that passes an integer as input and updates the scene
2185        irange : (tuple)
2186            the range of the integer input representing the time series index
2187        dt : (float)
2188            the time interval between two calls to `func` in milliseconds
2189        loop : (bool)
2190            whether to loop the animation
2191        c : (list, str)
2192            the color of the play/pause button
2193        bc : (list)
2194            the background color of the play/pause button and the slider
2195        button_size : (int)
2196            the size of the play/pause buttons
2197        button_pos : (float, float)
2198            the position of the play/pause buttons as a fraction of the window size
2199        button_gap : (float)
2200            the gap between the buttons
2201        slider_length : (float)
2202            the length of the slider as a fraction of the window size
2203        slider_pos : (float, float)
2204            the position of the slider as a fraction of the window size
2205        kwargs: (dict)
2206            keyword arguments to be passed to `Plotter`
2207
2208    Examples:
2209        - [aspring2_player.py](https://vedo.embl.es/images/simulations/spring_player.gif)
2210    """
2211
2212    # Original class contributed by @mikaeltulldahl (Mikael Tulldahl)
2213
2214    PLAY_SYMBOL        = "    \u23F5   "
2215    PAUSE_SYMBOL       = "   \u23F8   "
2216    ONE_BACK_SYMBOL    = " \u29CF"
2217    ONE_FORWARD_SYMBOL = "\u29D0 "
2218
2219    def __init__(
2220        self,
2221        func,
2222        irange: tuple,
2223        dt: float = 1.0,
2224        loop: bool = True,
2225        c=("white", "white"),
2226        bc=("green3", "red4"),
2227        button_size=25,
2228        button_pos=(0.5, 0.04),
2229        button_gap=0.055,
2230        slider_length=0.5,
2231        slider_pos=(0.5, 0.055),
2232        **kwargs,
2233    ):
2234        super().__init__(**kwargs)
2235
2236        min_value, max_value = np.array(irange).astype(int)
2237        button_pos = np.array(button_pos)
2238        slider_pos = np.array(slider_pos)
2239
2240        self._func = func
2241
2242        self.value = min_value - 1
2243        self.min_value = min_value
2244        self.max_value = max_value
2245        self.dt = max(dt, 1)
2246        self.is_playing = False
2247        self._loop = loop
2248
2249        self.timer_callback_id = self.add_callback(
2250            "timer", self._handle_timer, enable_picking=False
2251        )
2252        self.timer_id = None
2253
2254        self.play_pause_button = self.add_button(
2255            self.toggle,
2256            pos=button_pos,  # x,y fraction from bottom left corner
2257            states=[self.PLAY_SYMBOL, self.PAUSE_SYMBOL],
2258            font="Kanopus",
2259            size=button_size,
2260            bc=bc,
2261        )
2262        self.button_oneback = self.add_button(
2263            self.onebackward,
2264            pos=(-button_gap, 0) + button_pos,
2265            states=[self.ONE_BACK_SYMBOL],
2266            font="Kanopus",
2267            size=button_size,
2268            c=c,
2269            bc=bc,
2270        )
2271        self.button_oneforward = self.add_button(
2272            self.oneforward,
2273            pos=(button_gap, 0) + button_pos,
2274            states=[self.ONE_FORWARD_SYMBOL],
2275            font="Kanopus",
2276            size=button_size,
2277            bc=bc,
2278        )
2279        d = (1 - slider_length) / 2
2280        self.slider: SliderWidget = self.add_slider(
2281            self._slider_callback,
2282            self.min_value,
2283            self.max_value - 1,
2284            value=self.min_value,
2285            pos=[(d - 0.5, 0) + slider_pos, (0.5 - d, 0) + slider_pos],
2286            show_value=False,
2287            c=bc[0],
2288            alpha=1,
2289        )
2290
2291    def pause(self) -> None:
2292        """Pause the animation."""
2293        self.is_playing = False
2294        if self.timer_id is not None:
2295            self.timer_callback("destroy", self.timer_id)
2296            self.timer_id = None
2297        self.play_pause_button.status(self.PLAY_SYMBOL)
2298
2299    def resume(self) -> None:
2300        """Resume the animation."""
2301        if self.timer_id is not None:
2302            self.timer_callback("destroy", self.timer_id)
2303        self.timer_id = self.timer_callback("create", dt=int(self.dt))
2304        self.is_playing = True
2305        self.play_pause_button.status(self.PAUSE_SYMBOL)
2306
2307    def toggle(self, _obj, _evt) -> None:
2308        """Toggle between play and pause."""
2309        if not self.is_playing:
2310            self.resume()
2311        else:
2312            self.pause()
2313
2314    def oneforward(self, _obj, _evt) -> None:
2315        """Advance the animation by one frame."""
2316        self.pause()
2317        self.set_frame(self.value + 1)
2318
2319    def onebackward(self, _obj, _evt) -> None:
2320        """Go back one frame in the animation."""
2321        self.pause()
2322        self.set_frame(self.value - 1)
2323
2324    def set_frame(self, value: int) -> None:
2325        """Set the current value of the animation."""
2326        if self._loop:
2327            if value < self.min_value:
2328                value = self.max_value - 1
2329            elif value >= self.max_value:
2330                value = self.min_value
2331        else:
2332            if value < self.min_value:
2333                self.pause()
2334                value = self.min_value
2335            elif value >= self.max_value - 1:
2336                value = self.max_value - 1
2337                self.pause()
2338
2339        if self.value != value:
2340            self.value = value
2341            self.slider.value = value
2342            self._func(value)
2343
2344    def _slider_callback(self, widget: SliderWidget, _: str) -> None:
2345        self.pause()
2346        self.set_frame(int(round(widget.value)))
2347
2348    def _handle_timer(self, evt=None) -> None:
2349        self.set_frame(self.value + 1)
2350
2351    def stop(self) -> "AnimationPlayer":
2352        """
2353        Stop the animation timers, remove buttons and slider.
2354        Behave like a normal `Plotter` after this.
2355        """
2356        # stop timer
2357        if self.timer_id is not None:
2358            self.timer_callback("destroy", self.timer_id)
2359            self.timer_id = None
2360
2361        # remove callbacks
2362        self.remove_callback(self.timer_callback_id)
2363
2364        # remove buttons
2365        self.slider.off()
2366        self.renderer.RemoveActor(self.play_pause_button.actor)
2367        self.renderer.RemoveActor(self.button_oneback.actor)
2368        self.renderer.RemoveActor(self.button_oneforward.actor)
2369        return self
2370
2371
2372########################################################################
2373class Clock(vedo.Assembly):
2374    def __init__(self, h=None, m=None, s=None, font="Quikhand", title="", c="k"):
2375        """
2376        Create a clock with current time or user provided time.
2377
2378        Arguments:
2379            h : (int)
2380                hours in range [0,23]
2381            m : (int)
2382                minutes in range [0,59]
2383            s : (int)
2384                seconds in range [0,59]
2385            font : (str)
2386                font type
2387            title : (str)
2388                some extra text to show on the clock
2389            c : (str)
2390                color of the numbers
2391
2392        Example:
2393            ```python
2394            import time
2395            from vedo import show
2396            from vedo.applications import Clock
2397            clock = Clock()
2398            plt = show(clock, interactive=False)
2399            for i in range(10):
2400                time.sleep(1)
2401                clock.update()
2402                plt.render()
2403            plt.close()
2404            ```
2405            ![](https://vedo.embl.es/images/feats/clock.png)
2406        """
2407        self.elapsed = 0
2408        self._start = time.time()
2409
2410        wd = ""
2411        if h is None and m is None:
2412            t = time.localtime()
2413            h = t.tm_hour
2414            m = t.tm_min
2415            s = t.tm_sec
2416            if not title:
2417                d = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
2418                wd = f"{d[t.tm_wday]} {t.tm_mday}/{t.tm_mon}/{t.tm_year} "
2419
2420        h = int(h) % 24
2421        m = int(m) % 60
2422        t = (h * 60 + m) / 12 / 60
2423
2424        alpha = 2 * np.pi * t + np.pi / 2
2425        beta = 12 * 2 * np.pi * t + np.pi / 2
2426
2427        x1, y1 = np.cos(alpha), np.sin(alpha)
2428        x2, y2 = np.cos(beta), np.sin(beta)
2429        if s is not None:
2430            s = int(s) % 60
2431            gamma = s * 2 * np.pi / 60 + np.pi / 2
2432            x3, y3 = np.cos(gamma), np.sin(gamma)
2433
2434        ore = Line([0, 0], [x1, y1], lw=14, c="red4").scale(0.5).mirror()
2435        minu = Line([0, 0], [x2, y2], lw=7, c="blue3").scale(0.75).mirror()
2436        secs = None
2437        if s is not None:
2438            secs = Line([0, 0], [x3, y3], lw=1, c="k").scale(0.95).mirror()
2439            secs.z(0.003)
2440        back1 = vedo.shapes.Circle(res=180, c="k5")
2441        back2 = vedo.shapes.Circle(res=12).mirror().scale(0.84).rotate_z(-360 / 12)
2442        labels = back2.labels(range(1, 13), justify="center", font=font, c=c, scale=0.14)
2443        txt = vedo.shapes.Text3D(wd + title, font="VictorMono", justify="top-center", s=0.07, c=c)
2444        txt.pos(0, -0.25, 0.001)
2445        labels.z(0.001)
2446        minu.z(0.002)
2447        super().__init__([back1, labels, ore, minu, secs, txt])
2448        self.name = "Clock"
2449
2450    def update(self, h=None, m=None, s=None) -> "Clock":
2451        """Update clock with current or user time."""
2452        parts = self.unpack()
2453        self.elapsed = time.time() - self._start
2454
2455        if h is None and m is None:
2456            t = time.localtime()
2457            h = t.tm_hour
2458            m = t.tm_min
2459            s = t.tm_sec
2460
2461        h = int(h) % 24
2462        m = int(m) % 60
2463        t = (h * 60 + m) / 12 / 60
2464
2465        alpha = 2 * np.pi * t + np.pi / 2
2466        beta = 12 * 2 * np.pi * t + np.pi / 2
2467
2468        x1, y1 = np.cos(alpha), np.sin(alpha)
2469        x2, y2 = np.cos(beta), np.sin(beta)
2470        if s is not None:
2471            s = int(s) % 60
2472            gamma = s * 2 * np.pi / 60 + np.pi / 2
2473            x3, y3 = np.cos(gamma), np.sin(gamma)
2474
2475        pts2 = parts[2].vertices
2476        pts2[1] = [-x1 * 0.5, y1 * 0.5, 0.001]
2477        parts[2].vertices = pts2
2478
2479        pts3 = parts[3].vertices
2480        pts3[1] = [-x2 * 0.75, y2 * 0.75, 0.002]
2481        parts[3].vertices = pts3
2482
2483        if s is not None:
2484            pts4 = parts[4].vertices
2485            pts4[1] = [-x3 * 0.95, y3 * 0.95, 0.003]
2486            parts[4].vertices = pts4
2487
2488        return self
class Browser(vedo.plotter.Plotter):
1253class Browser(Plotter):
1254    """Browse a series of vedo objects by using a simple slider."""
1255
1256    def __init__(
1257        self,
1258        objects=(),
1259        sliderpos=((0.50, 0.07), (0.95, 0.07)),
1260        c=None,  # slider color
1261        slider_title="",
1262        font="Calco",  # slider font
1263        resetcam=False,  # resetcam while using the slider
1264        **kwargs,
1265    ):
1266        """
1267        Browse a series of vedo objects by using a simple slider.
1268
1269        The input object can be a list of objects or a list of lists of objects.
1270
1271        Arguments:
1272            objects : (list)
1273                list of objects to be browsed.
1274            sliderpos : (list)
1275                position of the slider.
1276            c : (str)
1277                color of the slider.
1278            slider_title : (str)
1279                title of the slider.
1280            font : (str)
1281                font of the slider.
1282            resetcam : (bool)
1283                resetcam while using the slider.
1284            **kwargs : (dict)
1285                keyword arguments to pass to Plotter.
1286
1287        Examples:
1288            ```python
1289            from vedo import load, dataurl
1290            from vedo.applications import Browser
1291            meshes = load(dataurl+'timecourse1d.npy') # python list of Meshes
1292            plt = Browser(meshes, bg='k')             # vedo.Plotter
1293            plt.show(interactive=False, zoom='tight') # show the meshes
1294            plt.play(dt=50)                           # delay in milliseconds
1295            plt.close()
1296            ```
1297
1298        - [morphomatics_tube.py](https://github.com/marcomusy/vedo/tree/master/examples/other/morphomatics_tube.py)
1299        """
1300        kwargs.pop("N", 1)
1301        kwargs.pop("shape", [])
1302        kwargs.pop("axes", 1)
1303        super().__init__(**kwargs)
1304
1305        if isinstance(objects, str):
1306            objects = vedo.file_io.load(objects)
1307
1308        self += objects
1309
1310        if len(objects) and is_sequence(objects[0]):
1311            nobs = len(objects[0])
1312            for ob in objects:
1313                n = len(ob)
1314                msg = f"in Browser lists must have the same length but found {n} and {nobs}"
1315                assert len(ob) == nobs, msg
1316        else:
1317            nobs = len(objects)
1318            if nobs:
1319                objects = [objects]
1320
1321        self.slider = None
1322        self.timer_callback_id = None
1323        self._oldk = None
1324
1325        # define the slider func ##########################
1326        def slider_function(widget=None, event=None):
1327
1328            k = int(self.slider.value)
1329
1330            if k == self._oldk:
1331                return  # no change
1332            self._oldk = k
1333
1334            n = len(objects)
1335            m = len(objects[0])
1336            for i in range(n):
1337                for j in range(m):
1338                    ak = objects[i][j]
1339                    try:
1340                        if j == k:
1341                            ak.on()
1342                            akon = ak
1343                        else:
1344                            ak.off()
1345                    except AttributeError:
1346                        pass
1347
1348            try:
1349                tx = str(k)
1350                if slider_title:
1351                    tx = slider_title + " " + tx
1352                elif n == 1 and akon.filename:
1353                    tx = akon.filename.split("/")[-1]
1354                    tx = tx.split("\\")[-1]  # windows os
1355                elif akon.name:
1356                    tx = ak.name + " " + tx
1357            except:
1358                pass
1359            self.slider.title = tx
1360
1361            if resetcam:
1362                self.reset_camera()
1363            self.render()
1364
1365        ##################################################
1366
1367        self.slider_function = slider_function
1368        self.slider = self.add_slider(
1369            slider_function,
1370            0.5,
1371            nobs - 0.5,
1372            pos=sliderpos,
1373            font=font,
1374            c=c,
1375            show_value=False,
1376        )
1377        self.slider.GetRepresentation().SetTitleHeight(0.020)
1378        slider_function()  # init call
1379
1380    def play(self, dt=100):
1381        """Start playing the slides at a given speed."""
1382        self.timer_callback_id = self.add_callback("timer", self.slider_function)
1383        self.timer_callback("start", dt=dt)
1384        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)
1256    def __init__(
1257        self,
1258        objects=(),
1259        sliderpos=((0.50, 0.07), (0.95, 0.07)),
1260        c=None,  # slider color
1261        slider_title="",
1262        font="Calco",  # slider font
1263        resetcam=False,  # resetcam while using the slider
1264        **kwargs,
1265    ):
1266        """
1267        Browse a series of vedo objects by using a simple slider.
1268
1269        The input object can be a list of objects or a list of lists of objects.
1270
1271        Arguments:
1272            objects : (list)
1273                list of objects to be browsed.
1274            sliderpos : (list)
1275                position of the slider.
1276            c : (str)
1277                color of the slider.
1278            slider_title : (str)
1279                title of the slider.
1280            font : (str)
1281                font of the slider.
1282            resetcam : (bool)
1283                resetcam while using the slider.
1284            **kwargs : (dict)
1285                keyword arguments to pass to Plotter.
1286
1287        Examples:
1288            ```python
1289            from vedo import load, dataurl
1290            from vedo.applications import Browser
1291            meshes = load(dataurl+'timecourse1d.npy') # python list of Meshes
1292            plt = Browser(meshes, bg='k')             # vedo.Plotter
1293            plt.show(interactive=False, zoom='tight') # show the meshes
1294            plt.play(dt=50)                           # delay in milliseconds
1295            plt.close()
1296            ```
1297
1298        - [morphomatics_tube.py](https://github.com/marcomusy/vedo/tree/master/examples/other/morphomatics_tube.py)
1299        """
1300        kwargs.pop("N", 1)
1301        kwargs.pop("shape", [])
1302        kwargs.pop("axes", 1)
1303        super().__init__(**kwargs)
1304
1305        if isinstance(objects, str):
1306            objects = vedo.file_io.load(objects)
1307
1308        self += objects
1309
1310        if len(objects) and is_sequence(objects[0]):
1311            nobs = len(objects[0])
1312            for ob in objects:
1313                n = len(ob)
1314                msg = f"in Browser lists must have the same length but found {n} and {nobs}"
1315                assert len(ob) == nobs, msg
1316        else:
1317            nobs = len(objects)
1318            if nobs:
1319                objects = [objects]
1320
1321        self.slider = None
1322        self.timer_callback_id = None
1323        self._oldk = None
1324
1325        # define the slider func ##########################
1326        def slider_function(widget=None, event=None):
1327
1328            k = int(self.slider.value)
1329
1330            if k == self._oldk:
1331                return  # no change
1332            self._oldk = k
1333
1334            n = len(objects)
1335            m = len(objects[0])
1336            for i in range(n):
1337                for j in range(m):
1338                    ak = objects[i][j]
1339                    try:
1340                        if j == k:
1341                            ak.on()
1342                            akon = ak
1343                        else:
1344                            ak.off()
1345                    except AttributeError:
1346                        pass
1347
1348            try:
1349                tx = str(k)
1350                if slider_title:
1351                    tx = slider_title + " " + tx
1352                elif n == 1 and akon.filename:
1353                    tx = akon.filename.split("/")[-1]
1354                    tx = tx.split("\\")[-1]  # windows os
1355                elif akon.name:
1356                    tx = ak.name + " " + tx
1357            except:
1358                pass
1359            self.slider.title = tx
1360
1361            if resetcam:
1362                self.reset_camera()
1363            self.render()
1364
1365        ##################################################
1366
1367        self.slider_function = slider_function
1368        self.slider = self.add_slider(
1369            slider_function,
1370            0.5,
1371            nobs - 0.5,
1372            pos=sliderpos,
1373            font=font,
1374            c=c,
1375            show_value=False,
1376        )
1377        self.slider.GetRepresentation().SetTitleHeight(0.020)
1378        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):
1380    def play(self, dt=100):
1381        """Start playing the slides at a given speed."""
1382        self.timer_callback_id = self.add_callback("timer", self.slider_function)
1383        self.timer_callback("start", dt=dt)
1384        self.interactive()

Start playing the slides at a given speed.

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

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

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

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

Can also be invoked from command line with:

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

Set an initial number of points to define a region

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

Save the resulting mesh to file

def start(self, *args, **kwargs) -> FreeHandCutPlotter:
1627    def start(self, *args, **kwargs) -> "FreeHandCutPlotter":
1628        """Start window interaction (with mouse and keyboard)"""
1629        acts = [self.txt2d, self.mesh, self.points, self.spline, self.jline]
1630        self.show(acts + list(args), **kwargs)
1631        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,
 953                horizontal=True,
 954                pos=[(0.8, 0), (0.97, 0.1)],
 955                font_size=0
 956            )
 957            self.add(self.color_scalarbar)
 958
 959        w1 = self.add_slider(
 960            slider_cmap,
 961            0, Ncols - 1,
 962            value=0,
 963            show_value=False,
 964            c=csl,
 965            pos=[(0.8, 0.05), (0.965, 0.05)],
 966        )
 967        w1.representation.SetTitleHeight(0.018)
 968
 969        ############################## alpha sliders
 970        # Create transfer mapping scalar value to opacity transfer function
 971        def setOTF():
 972            otf = self.properties.GetScalarOpacity()
 973            otf.RemoveAllPoints()
 974            otf.AddPoint(smin, 0.0)
 975            otf.AddPoint(smin + (smax - smin) * 0.1, 0.0)
 976            otf.AddPoint(x0alpha, self.alphaslider0)
 977            otf.AddPoint(x1alpha, self.alphaslider1)
 978            otf.AddPoint(x2alpha, self.alphaslider2)
 979            slider_cmap()
 980
 981        setOTF()  ################
 982
 983        def sliderA0(widget, event):
 984            self.alphaslider0 = widget.value
 985            setOTF()
 986
 987        self.add_slider(
 988            sliderA0,
 989            0, 1,
 990            value=self.alphaslider0,
 991            pos=[(0.84, 0.1), (0.84, 0.26)],
 992            c=csl,
 993            show_value=0,
 994        )
 995
 996        def sliderA1(widget, event):
 997            self.alphaslider1 = widget.value
 998            setOTF()
 999
1000        self.add_slider(
1001            sliderA1,
1002            0, 1,
1003            value=self.alphaslider1,
1004            pos=[(0.89, 0.1), (0.89, 0.26)],
1005            c=csl,
1006            show_value=0,
1007        )
1008
1009        def sliderA2(widget, event):
1010            self.alphaslider2 = widget.value
1011            setOTF()
1012
1013        w2 = self.add_slider(
1014            sliderA2,
1015            0, 1,
1016            value=self.alphaslider2,
1017            pos=[(0.96, 0.1), (0.96, 0.26)],
1018            c=csl,
1019            show_value=0,
1020            title="Opacity Levels",
1021        )
1022        w2.GetRepresentation().SetTitleHeight(0.015)
1023
1024        # add a button
1025        def button_func_mode(_obj, _ename):
1026            s = volume.mode()
1027            snew = (s + 1) % 2
1028            volume.mode(snew)
1029            bum.switch()
1030
1031        bum = self.add_button(
1032            button_func_mode,
1033            pos=(0.89, 0.31),
1034            states=["  composite   ", "max projection"],
1035            c=[ "k3", "k6"],
1036            bc=["k6", "k3"],  # colors of states
1037            font="Calco",
1038            size=18,
1039            bold=0,
1040            italic=False,
1041        )
1042        bum.frame(color="k6")
1043        bum.status(volume.mode())
1044
1045        slider_cmap() ############# init call to create scalarbar
1046
1047        # add histogram of scalar
1048        plot = CornerHistogram(
1049            volume,
1050            bins=25,
1051            logscale=1,
1052            c='k5',
1053            bg='k5',
1054            pos=(0.78, 0.065),
1055            lines=True,
1056            dots=False,
1057            nmax=3.1415e06,  # subsample otherwise is too slow
1058        )
1059
1060        plot.GetPosition2Coordinate().SetValue(0.197, 0.20, 0)
1061        plot.GetXAxisActor2D().SetFontFactor(0.7)
1062        plot.GetProperty().SetOpacity(0.5)
1063        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,
 953                horizontal=True,
 954                pos=[(0.8, 0), (0.97, 0.1)],
 955                font_size=0
 956            )
 957            self.add(self.color_scalarbar)
 958
 959        w1 = self.add_slider(
 960            slider_cmap,
 961            0, Ncols - 1,
 962            value=0,
 963            show_value=False,
 964            c=csl,
 965            pos=[(0.8, 0.05), (0.965, 0.05)],
 966        )
 967        w1.representation.SetTitleHeight(0.018)
 968
 969        ############################## alpha sliders
 970        # Create transfer mapping scalar value to opacity transfer function
 971        def setOTF():
 972            otf = self.properties.GetScalarOpacity()
 973            otf.RemoveAllPoints()
 974            otf.AddPoint(smin, 0.0)
 975            otf.AddPoint(smin + (smax - smin) * 0.1, 0.0)
 976            otf.AddPoint(x0alpha, self.alphaslider0)
 977            otf.AddPoint(x1alpha, self.alphaslider1)
 978            otf.AddPoint(x2alpha, self.alphaslider2)
 979            slider_cmap()
 980
 981        setOTF()  ################
 982
 983        def sliderA0(widget, event):
 984            self.alphaslider0 = widget.value
 985            setOTF()
 986
 987        self.add_slider(
 988            sliderA0,
 989            0, 1,
 990            value=self.alphaslider0,
 991            pos=[(0.84, 0.1), (0.84, 0.26)],
 992            c=csl,
 993            show_value=0,
 994        )
 995
 996        def sliderA1(widget, event):
 997            self.alphaslider1 = widget.value
 998            setOTF()
 999
1000        self.add_slider(
1001            sliderA1,
1002            0, 1,
1003            value=self.alphaslider1,
1004            pos=[(0.89, 0.1), (0.89, 0.26)],
1005            c=csl,
1006            show_value=0,
1007        )
1008
1009        def sliderA2(widget, event):
1010            self.alphaslider2 = widget.value
1011            setOTF()
1012
1013        w2 = self.add_slider(
1014            sliderA2,
1015            0, 1,
1016            value=self.alphaslider2,
1017            pos=[(0.96, 0.1), (0.96, 0.26)],
1018            c=csl,
1019            show_value=0,
1020            title="Opacity Levels",
1021        )
1022        w2.GetRepresentation().SetTitleHeight(0.015)
1023
1024        # add a button
1025        def button_func_mode(_obj, _ename):
1026            s = volume.mode()
1027            snew = (s + 1) % 2
1028            volume.mode(snew)
1029            bum.switch()
1030
1031        bum = self.add_button(
1032            button_func_mode,
1033            pos=(0.89, 0.31),
1034            states=["  composite   ", "max projection"],
1035            c=[ "k3", "k6"],
1036            bc=["k6", "k3"],  # colors of states
1037            font="Calco",
1038            size=18,
1039            bold=0,
1040            italic=False,
1041        )
1042        bum.frame(color="k6")
1043        bum.status(volume.mode())
1044
1045        slider_cmap() ############# init call to create scalarbar
1046
1047        # add histogram of scalar
1048        plot = CornerHistogram(
1049            volume,
1050            bins=25,
1051            logscale=1,
1052            c='k5',
1053            bg='k5',
1054            pos=(0.78, 0.065),
1055            lines=True,
1056            dots=False,
1057            nmax=3.1415e06,  # subsample otherwise is too slow
1058        )
1059
1060        plot.GetPosition2Coordinate().SetValue(0.197, 0.20, 0)
1061        plot.GetXAxisActor2D().SetFontFactor(0.7)
1062        plot.GetProperty().SetOpacity(0.5)
1063        self.add([plot, volume])

Generate a window for Volume rendering using ray casting.

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

vedo.Plotter object.

Examples:

class Slicer2DPlotter(vedo.plotter.Plotter):
662class Slicer2DPlotter(Plotter):
663    """
664    A single slice of a Volume which always faces the camera,
665    but at the same time can be oriented arbitrarily in space.
666    """
667
668    def __init__(self, vol: vedo.Volume, levels=(None, None), histo_color="red4", **kwargs):
669        """
670        A single slice of a Volume which always faces the camera,
671        but at the same time can be oriented arbitrarily in space.
672
673        Arguments:
674            vol : (Volume)
675                the Volume object to be isosurfaced.
676            levels : (list)
677                window and color levels
678            histo_color : (color)
679                histogram color, use `None` to disable it
680            **kwargs : (dict)
681                keyword arguments to pass to `Plotter`.
682
683        <img src="https://vedo.embl.es/images/volumetric/read_volume3.jpg" width="500">
684        """
685
686        if "shape" not in kwargs:
687            custom_shape = [  # define here the 2 rendering rectangle spaces
688                dict(bottomleft=(0.0, 0.0), topright=(1, 1), bg="k9"),  # the full window
689                dict(bottomleft=(0.8, 0.8), topright=(1, 1), bg="k8", bg2="lb"),
690            ]
691            kwargs["shape"] = custom_shape
692
693        if "interactive" not in kwargs:
694            kwargs["interactive"] = True
695
696        super().__init__(**kwargs)
697
698        self.user_mode("image")
699        self.add_callback("KeyPress", self.on_key_press)
700
701        orig_volume = vol.clone(deep=False)
702        self.volume = vol
703
704        self.volume.actor = vtki.new("ImageSlice")
705
706        self.volume.properties = self.volume.actor.GetProperty()
707        self.volume.properties.SetInterpolationTypeToLinear()
708
709        self.volume.mapper = vtki.new("ImageResliceMapper")
710        self.volume.mapper.SetInputData(self.volume.dataset)
711        self.volume.mapper.SliceFacesCameraOn()
712        self.volume.mapper.SliceAtFocalPointOn()
713        self.volume.mapper.SetAutoAdjustImageQuality(False)
714        self.volume.mapper.BorderOff()
715
716        # no argument will grab the existing cmap in vol (or use build_lut())
717        self.lut = None
718        self.cmap()
719
720        if levels[0] and levels[1]:
721            self.lighting(window=levels[0], level=levels[1])
722
723        self.usage_txt = (
724            "H                  :rightarrow Toggle this banner on/off\n"
725            "Left click & drag  :rightarrow Modify luminosity and contrast\n"
726            "SHIFT-Left click   :rightarrow Slice image obliquely\n"
727            "SHIFT-Middle click :rightarrow Slice image perpendicularly\n"
728            "SHIFT-R            :rightarrow Fly to closest cartesian view\n"
729            "SHIFT-U            :rightarrow Toggle parallel projection"
730        )
731
732        self.usage = Text2D(
733            self.usage_txt, font="Calco", pos="top-left", s=0.8, bg="yellow", alpha=0.25
734        )
735
736        hist = None
737        if histo_color is not None:
738            data = self.volume.pointdata[0]
739            arr = data
740            if data.ndim == 1:
741                # try to reduce the number of values to histogram
742                dims = self.volume.dimensions()
743                n = (dims[0] - 1) * (dims[1] - 1) * (dims[2] - 1)
744                n = min(1_000_000, n)
745                arr = np.random.choice(self.volume.pointdata[0], n)
746                hist = vedo.pyplot.histogram(
747                    arr,
748                    bins=12,
749                    logscale=True,
750                    c=histo_color,
751                    ytitle="log_10 (counts)",
752                    axes=dict(text_scale=1.9),
753                ).clone2d(pos="bottom-left", size=0.4)
754
755        axes = kwargs.pop("axes", 7)
756        axe = None
757        if axes == 7:
758            axe = vedo.addons.RulerAxes(
759                orig_volume, xtitle="x - ", ytitle="y - ", ztitle="z - "
760            )
761
762        box = orig_volume.box().alpha(0.25)
763
764        volume_axes_inset = vedo.addons.Axes(
765            box,
766            yzgrid=False,
767            xlabel_size=0,
768            ylabel_size=0,
769            zlabel_size=0,
770            tip_size=0.08,
771            axes_linewidth=3,
772            xline_color="dr",
773            yline_color="dg",
774            zline_color="db",
775            xtitle_color="dr",
776            ytitle_color="dg",
777            ztitle_color="db",
778            xtitle_size=0.1,
779            ytitle_size=0.1,
780            ztitle_size=0.1,
781            title_font="VictorMono",
782        )
783
784        self.at(0).add(self.volume, box, axe, self.usage, hist)
785        self.at(1).add(orig_volume, volume_axes_inset)
786        self.at(0)  # set focus at renderer 0
787
788    ####################################################################
789    def on_key_press(self, evt):
790        if evt.keypress == "q":
791            self.break_interaction()
792        elif evt.keypress.lower() == "h":
793            t = self.usage
794            if len(t.text()) > 50:
795                self.usage.text("Press H to show help")
796            else:
797                self.usage.text(self.usage_txt)
798            self.render()
799
800    def cmap(self, lut=None, fix_scalar_range=False) -> "Slicer2DPlotter":
801        """
802        Assign a LUT (Look Up Table) to colorize the slice, leave it `None`
803        to reuse an existing Volume color map.
804        Use "bw" for automatic black and white.
805        """
806        if lut is None and self.lut:
807            self.volume.properties.SetLookupTable(self.lut)
808        elif isinstance(lut, vtki.vtkLookupTable):
809            self.volume.properties.SetLookupTable(lut)
810        elif lut == "bw":
811            self.volume.properties.SetLookupTable(None)
812        self.volume.properties.SetUseLookupTableScalarRange(fix_scalar_range)
813        return self
814
815    def alpha(self, value: float) -> "Slicer2DPlotter":
816        """Set opacity to the slice"""
817        self.volume.properties.SetOpacity(value)
818        return self
819
820    def auto_adjust_quality(self, value=True) -> "Slicer2DPlotter":
821        """Automatically reduce the rendering quality for greater speed when interacting"""
822        self.volume.mapper.SetAutoAdjustImageQuality(value)
823        return self
824
825    def slab(self, thickness=0, mode=0, sample_factor=2) -> "Slicer2DPlotter":
826        """
827        Make a thick slice (slab).
828
829        Arguments:
830            thickness : (float)
831                set the slab thickness, for thick slicing
832            mode : (int)
833                The slab type:
834                    0 = min
835                    1 = max
836                    2 = mean
837                    3 = sum
838            sample_factor : (float)
839                Set the number of slab samples to use as a factor of the number of input slices
840                within the slab thickness. The default value is 2, but 1 will increase speed
841                with very little loss of quality.
842        """
843        self.volume.mapper.SetSlabThickness(thickness)
844        self.volume.mapper.SetSlabType(mode)
845        self.volume.mapper.SetSlabSampleFactor(sample_factor)
846        return self
847
848    def face_camera(self, value=True) -> "Slicer2DPlotter":
849        """Make the slice always face the camera or not."""
850        self.volume.mapper.SetSliceFacesCameraOn(value)
851        return self
852
853    def jump_to_nearest_slice(self, value=True) -> "Slicer2DPlotter":
854        """
855        This causes the slicing to occur at the closest slice to the focal point,
856        instead of the default behavior where a new slice is interpolated between
857        the original slices.
858        Nothing happens if the plane is oblique to the original slices.
859        """
860        self.volume.mapper.SetJumpToNearestSlice(value)
861        return self
862
863    def fill_background(self, value=True) -> "Slicer2DPlotter":
864        """
865        Instead of rendering only to the image border,
866        render out to the viewport boundary with the background color.
867        The background color will be the lowest color on the lookup
868        table that is being used for the image.
869        """
870        self.volume.mapper.SetBackground(value)
871        return self
872
873    def lighting(self, window, level, ambient=1.0, diffuse=0.0) -> "Slicer2DPlotter":
874        """Assign the values for window and color level."""
875        self.volume.properties.SetColorWindow(window)
876        self.volume.properties.SetColorLevel(level)
877        self.volume.properties.SetAmbient(ambient)
878        self.volume.properties.SetDiffuse(diffuse)
879        return self

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

Slicer2DPlotter( vol: vedo.volume.Volume, levels=(None, None), histo_color='red4', **kwargs)
668    def __init__(self, vol: vedo.Volume, levels=(None, None), histo_color="red4", **kwargs):
669        """
670        A single slice of a Volume which always faces the camera,
671        but at the same time can be oriented arbitrarily in space.
672
673        Arguments:
674            vol : (Volume)
675                the Volume object to be isosurfaced.
676            levels : (list)
677                window and color levels
678            histo_color : (color)
679                histogram color, use `None` to disable it
680            **kwargs : (dict)
681                keyword arguments to pass to `Plotter`.
682
683        <img src="https://vedo.embl.es/images/volumetric/read_volume3.jpg" width="500">
684        """
685
686        if "shape" not in kwargs:
687            custom_shape = [  # define here the 2 rendering rectangle spaces
688                dict(bottomleft=(0.0, 0.0), topright=(1, 1), bg="k9"),  # the full window
689                dict(bottomleft=(0.8, 0.8), topright=(1, 1), bg="k8", bg2="lb"),
690            ]
691            kwargs["shape"] = custom_shape
692
693        if "interactive" not in kwargs:
694            kwargs["interactive"] = True
695
696        super().__init__(**kwargs)
697
698        self.user_mode("image")
699        self.add_callback("KeyPress", self.on_key_press)
700
701        orig_volume = vol.clone(deep=False)
702        self.volume = vol
703
704        self.volume.actor = vtki.new("ImageSlice")
705
706        self.volume.properties = self.volume.actor.GetProperty()
707        self.volume.properties.SetInterpolationTypeToLinear()
708
709        self.volume.mapper = vtki.new("ImageResliceMapper")
710        self.volume.mapper.SetInputData(self.volume.dataset)
711        self.volume.mapper.SliceFacesCameraOn()
712        self.volume.mapper.SliceAtFocalPointOn()
713        self.volume.mapper.SetAutoAdjustImageQuality(False)
714        self.volume.mapper.BorderOff()
715
716        # no argument will grab the existing cmap in vol (or use build_lut())
717        self.lut = None
718        self.cmap()
719
720        if levels[0] and levels[1]:
721            self.lighting(window=levels[0], level=levels[1])
722
723        self.usage_txt = (
724            "H                  :rightarrow Toggle this banner on/off\n"
725            "Left click & drag  :rightarrow Modify luminosity and contrast\n"
726            "SHIFT-Left click   :rightarrow Slice image obliquely\n"
727            "SHIFT-Middle click :rightarrow Slice image perpendicularly\n"
728            "SHIFT-R            :rightarrow Fly to closest cartesian view\n"
729            "SHIFT-U            :rightarrow Toggle parallel projection"
730        )
731
732        self.usage = Text2D(
733            self.usage_txt, font="Calco", pos="top-left", s=0.8, bg="yellow", alpha=0.25
734        )
735
736        hist = None
737        if histo_color is not None:
738            data = self.volume.pointdata[0]
739            arr = data
740            if data.ndim == 1:
741                # try to reduce the number of values to histogram
742                dims = self.volume.dimensions()
743                n = (dims[0] - 1) * (dims[1] - 1) * (dims[2] - 1)
744                n = min(1_000_000, n)
745                arr = np.random.choice(self.volume.pointdata[0], n)
746                hist = vedo.pyplot.histogram(
747                    arr,
748                    bins=12,
749                    logscale=True,
750                    c=histo_color,
751                    ytitle="log_10 (counts)",
752                    axes=dict(text_scale=1.9),
753                ).clone2d(pos="bottom-left", size=0.4)
754
755        axes = kwargs.pop("axes", 7)
756        axe = None
757        if axes == 7:
758            axe = vedo.addons.RulerAxes(
759                orig_volume, xtitle="x - ", ytitle="y - ", ztitle="z - "
760            )
761
762        box = orig_volume.box().alpha(0.25)
763
764        volume_axes_inset = vedo.addons.Axes(
765            box,
766            yzgrid=False,
767            xlabel_size=0,
768            ylabel_size=0,
769            zlabel_size=0,
770            tip_size=0.08,
771            axes_linewidth=3,
772            xline_color="dr",
773            yline_color="dg",
774            zline_color="db",
775            xtitle_color="dr",
776            ytitle_color="dg",
777            ztitle_color="db",
778            xtitle_size=0.1,
779            ytitle_size=0.1,
780            ztitle_size=0.1,
781            title_font="VictorMono",
782        )
783
784        self.at(0).add(self.volume, box, axe, self.usage, hist)
785        self.at(1).add(orig_volume, volume_axes_inset)
786        self.at(0)  # set focus at renderer 0

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

Arguments:
  • vol : (Volume) the Volume object to be isosurfaced.
  • levels : (list) window and color levels
  • histo_color : (color) histogram color, use None to disable it
  • **kwargs : (dict) keyword arguments to pass to Plotter.

def on_key_press(self, evt):
789    def on_key_press(self, evt):
790        if evt.keypress == "q":
791            self.break_interaction()
792        elif evt.keypress.lower() == "h":
793            t = self.usage
794            if len(t.text()) > 50:
795                self.usage.text("Press H to show help")
796            else:
797                self.usage.text(self.usage_txt)
798            self.render()
def cmap( self, lut=None, fix_scalar_range=False) -> Slicer2DPlotter:
800    def cmap(self, lut=None, fix_scalar_range=False) -> "Slicer2DPlotter":
801        """
802        Assign a LUT (Look Up Table) to colorize the slice, leave it `None`
803        to reuse an existing Volume color map.
804        Use "bw" for automatic black and white.
805        """
806        if lut is None and self.lut:
807            self.volume.properties.SetLookupTable(self.lut)
808        elif isinstance(lut, vtki.vtkLookupTable):
809            self.volume.properties.SetLookupTable(lut)
810        elif lut == "bw":
811            self.volume.properties.SetLookupTable(None)
812        self.volume.properties.SetUseLookupTableScalarRange(fix_scalar_range)
813        return self

Assign a LUT (Look Up Table) to colorize the slice, leave it None to reuse an existing Volume color map. Use "bw" for automatic black and white.

def alpha(self, value: float) -> Slicer2DPlotter:
815    def alpha(self, value: float) -> "Slicer2DPlotter":
816        """Set opacity to the slice"""
817        self.volume.properties.SetOpacity(value)
818        return self

Set opacity to the slice

def auto_adjust_quality(self, value=True) -> Slicer2DPlotter:
820    def auto_adjust_quality(self, value=True) -> "Slicer2DPlotter":
821        """Automatically reduce the rendering quality for greater speed when interacting"""
822        self.volume.mapper.SetAutoAdjustImageQuality(value)
823        return self

Automatically reduce the rendering quality for greater speed when interacting

def slab( self, thickness=0, mode=0, sample_factor=2) -> Slicer2DPlotter:
825    def slab(self, thickness=0, mode=0, sample_factor=2) -> "Slicer2DPlotter":
826        """
827        Make a thick slice (slab).
828
829        Arguments:
830            thickness : (float)
831                set the slab thickness, for thick slicing
832            mode : (int)
833                The slab type:
834                    0 = min
835                    1 = max
836                    2 = mean
837                    3 = sum
838            sample_factor : (float)
839                Set the number of slab samples to use as a factor of the number of input slices
840                within the slab thickness. The default value is 2, but 1 will increase speed
841                with very little loss of quality.
842        """
843        self.volume.mapper.SetSlabThickness(thickness)
844        self.volume.mapper.SetSlabType(mode)
845        self.volume.mapper.SetSlabSampleFactor(sample_factor)
846        return self

Make a thick slice (slab).

Arguments:
  • thickness : (float) set the slab thickness, for thick slicing
  • mode : (int) The slab type: 0 = min 1 = max 2 = mean 3 = sum
  • sample_factor : (float) Set the number of slab samples to use as a factor of the number of input slices within the slab thickness. The default value is 2, but 1 will increase speed with very little loss of quality.
def face_camera(self, value=True) -> Slicer2DPlotter:
848    def face_camera(self, value=True) -> "Slicer2DPlotter":
849        """Make the slice always face the camera or not."""
850        self.volume.mapper.SetSliceFacesCameraOn(value)
851        return self

Make the slice always face the camera or not.

def jump_to_nearest_slice(self, value=True) -> Slicer2DPlotter:
853    def jump_to_nearest_slice(self, value=True) -> "Slicer2DPlotter":
854        """
855        This causes the slicing to occur at the closest slice to the focal point,
856        instead of the default behavior where a new slice is interpolated between
857        the original slices.
858        Nothing happens if the plane is oblique to the original slices.
859        """
860        self.volume.mapper.SetJumpToNearestSlice(value)
861        return self

This causes the slicing to occur at the closest slice to the focal point, instead of the default behavior where a new slice is interpolated between the original slices. Nothing happens if the plane is oblique to the original slices.

def fill_background(self, value=True) -> Slicer2DPlotter:
863    def fill_background(self, value=True) -> "Slicer2DPlotter":
864        """
865        Instead of rendering only to the image border,
866        render out to the viewport boundary with the background color.
867        The background color will be the lowest color on the lookup
868        table that is being used for the image.
869        """
870        self.volume.mapper.SetBackground(value)
871        return self

Instead of rendering only to the image border, render out to the viewport boundary with the background color. The background color will be the lowest color on the lookup table that is being used for the image.

def lighting( self, window, level, ambient=1.0, diffuse=0.0) -> Slicer2DPlotter:
873    def lighting(self, window, level, ambient=1.0, diffuse=0.0) -> "Slicer2DPlotter":
874        """Assign the values for window and color level."""
875        self.volume.properties.SetColorWindow(window)
876        self.volume.properties.SetColorLevel(level)
877        self.volume.properties.SetAmbient(ambient)
878        self.volume.properties.SetDiffuse(diffuse)
879        return self

Assign the values for window and color level.

class Slicer3DPlotter(vedo.plotter.Plotter):
 44class Slicer3DPlotter(Plotter):
 45    """
 46    Generate a rendering window with slicing planes for the input Volume.
 47    """
 48
 49    def __init__(
 50        self,
 51        volume: vedo.Volume,
 52        cmaps=("gist_ncar_r", "hot_r", "bone", "bone_r", "jet", "Spectral_r"),
 53        clamp=True,
 54        use_slider3d=False,
 55        show_histo=True,
 56        show_icon=True,
 57        draggable=False,
 58        at=0,
 59        **kwargs,
 60    ):
 61        """
 62        Generate a rendering window with slicing planes for the input Volume.
 63
 64        Arguments:
 65            cmaps : (list)
 66                list of color maps names to cycle when clicking button
 67            clamp : (bool)
 68                clamp scalar range to reduce the effect of tails in color mapping
 69            use_slider3d : (bool)
 70                show sliders attached along the axes
 71            show_histo : (bool)
 72                show histogram on bottom left
 73            show_icon : (bool)
 74                show a small 3D rendering icon of the volume
 75            draggable : (bool)
 76                make the 3D icon draggable
 77            at : (int)
 78                subwindow number to plot to
 79            **kwargs : (dict)
 80                keyword arguments to pass to Plotter.
 81
 82        Examples:
 83            - [slicer1.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/slicer1.py)
 84
 85            <img src="https://vedo.embl.es/images/volumetric/slicer1.jpg" width="500">
 86        """
 87        ################################
 88        super().__init__(**kwargs)
 89        self.at(at)
 90        ################################
 91
 92        cx, cy, cz, ch = "dr", "dg", "db", (0.3, 0.3, 0.3)
 93        if np.sum(self.renderer.GetBackground()) < 1.5:
 94            cx, cy, cz = "lr", "lg", "lb"
 95            ch = (0.8, 0.8, 0.8)
 96
 97        if len(self.renderers) > 1:
 98            # 2d sliders do not work with multiple renderers
 99            use_slider3d = True
100
101        self.volume = volume
102        box = volume.box().alpha(0.2)
103        self.add(box)
104
105        volume_axes_inset = vedo.addons.Axes(
106            box,
107            xtitle=" ",
108            ytitle=" ",
109            ztitle=" ",
110            yzgrid=False,
111            xlabel_size=0,
112            ylabel_size=0,
113            zlabel_size=0,
114            tip_size=0.08,
115            axes_linewidth=3,
116            xline_color="dr",
117            yline_color="dg",
118            zline_color="db",
119        )
120
121        if show_icon:
122            self.add_inset(
123                volume,
124                volume_axes_inset,
125                pos=(0.9, 0.9),
126                size=0.15,
127                c="w",
128                draggable=draggable,
129            )
130
131        # inits
132        la, ld = 0.7, 0.3  # ambient, diffuse
133        dims = volume.dimensions()
134        data = volume.pointdata[0]
135        rmin, rmax = volume.scalar_range()
136        if clamp:
137            hdata, edg = np.histogram(data, bins=50)
138            logdata = np.log(hdata + 1)
139            # mean  of the logscale plot
140            meanlog = np.sum(np.multiply(edg[:-1], logdata)) / np.sum(logdata)
141            rmax = min(rmax, meanlog + (meanlog - rmin) * 0.9)
142            rmin = max(rmin, meanlog - (rmax - meanlog) * 0.9)
143            # print("scalar range clamped to range: ("
144            #       + precision(rmin, 3) + ", " + precision(rmax, 3) + ")")
145
146        self.cmap_slicer = cmaps[0]
147
148        self.current_i = None
149        self.current_j = None
150        self.current_k = int(dims[2] / 2)
151
152        self.xslice = None
153        self.yslice = None
154        self.zslice = None
155
156        self.zslice = volume.zslice(self.current_k).lighting("", la, ld, 0)
157        self.zslice.name = "ZSlice"
158        self.zslice.cmap(self.cmap_slicer, vmin=rmin, vmax=rmax)
159        self.add(self.zslice)
160
161        self.histogram = None
162        data_reduced = data
163        if show_histo:
164            # try to reduce the number of values to histogram
165            dims = self.volume.dimensions()
166            n = (dims[0] - 1) * (dims[1] - 1) * (dims[2] - 1)
167            n = min(1_000_000, n)
168            if data.ndim == 1:
169                data_reduced = np.random.choice(data, n)
170                self.histogram = histogram(
171                    data_reduced,
172                    # title=volume.filename,
173                    bins=20,
174                    logscale=True,
175                    c=self.cmap_slicer,
176                    bg=ch,
177                    alpha=1,
178                    axes=dict(text_scale=2),
179                ).clone2d(pos=[-0.925, -0.88], size=0.4)
180                self.add(self.histogram)
181
182        #################
183        def slider_function_x(widget, event):
184            i = int(self.xslider.value)
185            if i == self.current_i:
186                return
187            self.current_i = i
188            self.xslice = volume.xslice(i).lighting("", la, ld, 0)
189            self.xslice.cmap(self.cmap_slicer, vmin=rmin, vmax=rmax)
190            self.xslice.name = "XSlice"
191            self.remove("XSlice")  # removes the old one
192            if 0 < i < dims[0]:
193                self.add(self.xslice)
194            self.render()
195
196        def slider_function_y(widget, event):
197            j = int(self.yslider.value)
198            if j == self.current_j:
199                return
200            self.current_j = j
201            self.yslice = volume.yslice(j).lighting("", la, ld, 0)
202            self.yslice.cmap(self.cmap_slicer, vmin=rmin, vmax=rmax)
203            self.yslice.name = "YSlice"
204            self.remove("YSlice")
205            if 0 < j < dims[1]:
206                self.add(self.yslice)
207            self.render()
208
209        def slider_function_z(widget, event):
210            k = int(self.zslider.value)
211            if k == self.current_k:
212                return
213            self.current_k = k
214            self.zslice = volume.zslice(k).lighting("", la, ld, 0)
215            self.zslice.cmap(self.cmap_slicer, vmin=rmin, vmax=rmax)
216            self.zslice.name = "ZSlice"
217            self.remove("ZSlice")
218            if 0 < k < dims[2]:
219                self.add(self.zslice)
220            self.render()
221
222        if not use_slider3d:
223            self.xslider = self.add_slider(
224                slider_function_x,
225                0,
226                dims[0],
227                title="",
228                title_size=0.5,
229                pos=[(0.8, 0.12), (0.95, 0.12)],
230                show_value=False,
231                c=cx,
232            )
233            self.yslider = self.add_slider(
234                slider_function_y,
235                0,
236                dims[1],
237                title="",
238                title_size=0.5,
239                pos=[(0.8, 0.08), (0.95, 0.08)],
240                show_value=False,
241                c=cy,
242            )
243            self.zslider = self.add_slider(
244                slider_function_z,
245                0,
246                dims[2],
247                title="",
248                title_size=0.6,
249                value=int(dims[2] / 2),
250                pos=[(0.8, 0.04), (0.95, 0.04)],
251                show_value=False,
252                c=cz,
253            )
254
255        else:  # 3d sliders attached to the axes bounds
256            bs = box.bounds()
257            self.xslider = self.add_slider3d(
258                slider_function_x,
259                pos1=(bs[0], bs[2], bs[4]),
260                pos2=(bs[1], bs[2], bs[4]),
261                xmin=0,
262                xmax=dims[0],
263                t=box.diagonal_size() / mag(box.xbounds()) * 0.6,
264                c=cx,
265                show_value=False,
266            )
267            self.yslider = self.add_slider3d(
268                slider_function_y,
269                pos1=(bs[1], bs[2], bs[4]),
270                pos2=(bs[1], bs[3], bs[4]),
271                xmin=0,
272                xmax=dims[1],
273                t=box.diagonal_size() / mag(box.ybounds()) * 0.6,
274                c=cy,
275                show_value=False,
276            )
277            self.zslider = self.add_slider3d(
278                slider_function_z,
279                pos1=(bs[0], bs[2], bs[4]),
280                pos2=(bs[0], bs[2], bs[5]),
281                xmin=0,
282                xmax=dims[2],
283                value=int(dims[2] / 2),
284                t=box.diagonal_size() / mag(box.zbounds()) * 0.6,
285                c=cz,
286                show_value=False,
287            )
288
289        #################
290        def button_func(obj, ename):
291            bu.switch()
292            self.cmap_slicer = bu.status()
293            for m in self.objects:
294                if "Slice" in m.name:
295                    m.cmap(self.cmap_slicer, vmin=rmin, vmax=rmax)
296            self.remove(self.histogram)
297            if show_histo:
298                self.histogram = histogram(
299                    data_reduced,
300                    # title=volume.filename,
301                    bins=20,
302                    logscale=True,
303                    c=self.cmap_slicer,
304                    bg=ch,
305                    alpha=1,
306                    axes=dict(text_scale=2),
307                ).clone2d(pos=[-0.925, -0.88], size=0.4)
308                self.add(self.histogram)
309            self.render()
310
311        if len(cmaps) > 1:
312            bu = self.add_button(
313                button_func,
314                states=cmaps,
315                c=["k9"] * len(cmaps),
316                bc=["k1"] * len(cmaps),  # colors of states
317                size=16,
318                bold=True,
319            )
320            if bu:
321                bu.pos([0.04, 0.01], "bottom-left")

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

Slicer3DPlotter( volume: vedo.volume.Volume, cmaps=('gist_ncar_r', 'hot_r', 'bone', 'bone_r', 'jet', 'Spectral_r'), clamp=True, use_slider3d=False, show_histo=True, show_icon=True, draggable=False, at=0, **kwargs)
 49    def __init__(
 50        self,
 51        volume: vedo.Volume,
 52        cmaps=("gist_ncar_r", "hot_r", "bone", "bone_r", "jet", "Spectral_r"),
 53        clamp=True,
 54        use_slider3d=False,
 55        show_histo=True,
 56        show_icon=True,
 57        draggable=False,
 58        at=0,
 59        **kwargs,
 60    ):
 61        """
 62        Generate a rendering window with slicing planes for the input Volume.
 63
 64        Arguments:
 65            cmaps : (list)
 66                list of color maps names to cycle when clicking button
 67            clamp : (bool)
 68                clamp scalar range to reduce the effect of tails in color mapping
 69            use_slider3d : (bool)
 70                show sliders attached along the axes
 71            show_histo : (bool)
 72                show histogram on bottom left
 73            show_icon : (bool)
 74                show a small 3D rendering icon of the volume
 75            draggable : (bool)
 76                make the 3D icon draggable
 77            at : (int)
 78                subwindow number to plot to
 79            **kwargs : (dict)
 80                keyword arguments to pass to Plotter.
 81
 82        Examples:
 83            - [slicer1.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/slicer1.py)
 84
 85            <img src="https://vedo.embl.es/images/volumetric/slicer1.jpg" width="500">
 86        """
 87        ################################
 88        super().__init__(**kwargs)
 89        self.at(at)
 90        ################################
 91
 92        cx, cy, cz, ch = "dr", "dg", "db", (0.3, 0.3, 0.3)
 93        if np.sum(self.renderer.GetBackground()) < 1.5:
 94            cx, cy, cz = "lr", "lg", "lb"
 95            ch = (0.8, 0.8, 0.8)
 96
 97        if len(self.renderers) > 1:
 98            # 2d sliders do not work with multiple renderers
 99            use_slider3d = True
100
101        self.volume = volume
102        box = volume.box().alpha(0.2)
103        self.add(box)
104
105        volume_axes_inset = vedo.addons.Axes(
106            box,
107            xtitle=" ",
108            ytitle=" ",
109            ztitle=" ",
110            yzgrid=False,
111            xlabel_size=0,
112            ylabel_size=0,
113            zlabel_size=0,
114            tip_size=0.08,
115            axes_linewidth=3,
116            xline_color="dr",
117            yline_color="dg",
118            zline_color="db",
119        )
120
121        if show_icon:
122            self.add_inset(
123                volume,
124                volume_axes_inset,
125                pos=(0.9, 0.9),
126                size=0.15,
127                c="w",
128                draggable=draggable,
129            )
130
131        # inits
132        la, ld = 0.7, 0.3  # ambient, diffuse
133        dims = volume.dimensions()
134        data = volume.pointdata[0]
135        rmin, rmax = volume.scalar_range()
136        if clamp:
137            hdata, edg = np.histogram(data, bins=50)
138            logdata = np.log(hdata + 1)
139            # mean  of the logscale plot
140            meanlog = np.sum(np.multiply(edg[:-1], logdata)) / np.sum(logdata)
141            rmax = min(rmax, meanlog + (meanlog - rmin) * 0.9)
142            rmin = max(rmin, meanlog - (rmax - meanlog) * 0.9)
143            # print("scalar range clamped to range: ("
144            #       + precision(rmin, 3) + ", " + precision(rmax, 3) + ")")
145
146        self.cmap_slicer = cmaps[0]
147
148        self.current_i = None
149        self.current_j = None
150        self.current_k = int(dims[2] / 2)
151
152        self.xslice = None
153        self.yslice = None
154        self.zslice = None
155
156        self.zslice = volume.zslice(self.current_k).lighting("", la, ld, 0)
157        self.zslice.name = "ZSlice"
158        self.zslice.cmap(self.cmap_slicer, vmin=rmin, vmax=rmax)
159        self.add(self.zslice)
160
161        self.histogram = None
162        data_reduced = data
163        if show_histo:
164            # try to reduce the number of values to histogram
165            dims = self.volume.dimensions()
166            n = (dims[0] - 1) * (dims[1] - 1) * (dims[2] - 1)
167            n = min(1_000_000, n)
168            if data.ndim == 1:
169                data_reduced = np.random.choice(data, n)
170                self.histogram = histogram(
171                    data_reduced,
172                    # title=volume.filename,
173                    bins=20,
174                    logscale=True,
175                    c=self.cmap_slicer,
176                    bg=ch,
177                    alpha=1,
178                    axes=dict(text_scale=2),
179                ).clone2d(pos=[-0.925, -0.88], size=0.4)
180                self.add(self.histogram)
181
182        #################
183        def slider_function_x(widget, event):
184            i = int(self.xslider.value)
185            if i == self.current_i:
186                return
187            self.current_i = i
188            self.xslice = volume.xslice(i).lighting("", la, ld, 0)
189            self.xslice.cmap(self.cmap_slicer, vmin=rmin, vmax=rmax)
190            self.xslice.name = "XSlice"
191            self.remove("XSlice")  # removes the old one
192            if 0 < i < dims[0]:
193                self.add(self.xslice)
194            self.render()
195
196        def slider_function_y(widget, event):
197            j = int(self.yslider.value)
198            if j == self.current_j:
199                return
200            self.current_j = j
201            self.yslice = volume.yslice(j).lighting("", la, ld, 0)
202            self.yslice.cmap(self.cmap_slicer, vmin=rmin, vmax=rmax)
203            self.yslice.name = "YSlice"
204            self.remove("YSlice")
205            if 0 < j < dims[1]:
206                self.add(self.yslice)
207            self.render()
208
209        def slider_function_z(widget, event):
210            k = int(self.zslider.value)
211            if k == self.current_k:
212                return
213            self.current_k = k
214            self.zslice = volume.zslice(k).lighting("", la, ld, 0)
215            self.zslice.cmap(self.cmap_slicer, vmin=rmin, vmax=rmax)
216            self.zslice.name = "ZSlice"
217            self.remove("ZSlice")
218            if 0 < k < dims[2]:
219                self.add(self.zslice)
220            self.render()
221
222        if not use_slider3d:
223            self.xslider = self.add_slider(
224                slider_function_x,
225                0,
226                dims[0],
227                title="",
228                title_size=0.5,
229                pos=[(0.8, 0.12), (0.95, 0.12)],
230                show_value=False,
231                c=cx,
232            )
233            self.yslider = self.add_slider(
234                slider_function_y,
235                0,
236                dims[1],
237                title="",
238                title_size=0.5,
239                pos=[(0.8, 0.08), (0.95, 0.08)],
240                show_value=False,
241                c=cy,
242            )
243            self.zslider = self.add_slider(
244                slider_function_z,
245                0,
246                dims[2],
247                title="",
248                title_size=0.6,
249                value=int(dims[2] / 2),
250                pos=[(0.8, 0.04), (0.95, 0.04)],
251                show_value=False,
252                c=cz,
253            )
254
255        else:  # 3d sliders attached to the axes bounds
256            bs = box.bounds()
257            self.xslider = self.add_slider3d(
258                slider_function_x,
259                pos1=(bs[0], bs[2], bs[4]),
260                pos2=(bs[1], bs[2], bs[4]),
261                xmin=0,
262                xmax=dims[0],
263                t=box.diagonal_size() / mag(box.xbounds()) * 0.6,
264                c=cx,
265                show_value=False,
266            )
267            self.yslider = self.add_slider3d(
268                slider_function_y,
269                pos1=(bs[1], bs[2], bs[4]),
270                pos2=(bs[1], bs[3], bs[4]),
271                xmin=0,
272                xmax=dims[1],
273                t=box.diagonal_size() / mag(box.ybounds()) * 0.6,
274                c=cy,
275                show_value=False,
276            )
277            self.zslider = self.add_slider3d(
278                slider_function_z,
279                pos1=(bs[0], bs[2], bs[4]),
280                pos2=(bs[0], bs[2], bs[5]),
281                xmin=0,
282                xmax=dims[2],
283                value=int(dims[2] / 2),
284                t=box.diagonal_size() / mag(box.zbounds()) * 0.6,
285                c=cz,
286                show_value=False,
287            )
288
289        #################
290        def button_func(obj, ename):
291            bu.switch()
292            self.cmap_slicer = bu.status()
293            for m in self.objects:
294                if "Slice" in m.name:
295                    m.cmap(self.cmap_slicer, vmin=rmin, vmax=rmax)
296            self.remove(self.histogram)
297            if show_histo:
298                self.histogram = histogram(
299                    data_reduced,
300                    # title=volume.filename,
301                    bins=20,
302                    logscale=True,
303                    c=self.cmap_slicer,
304                    bg=ch,
305                    alpha=1,
306                    axes=dict(text_scale=2),
307                ).clone2d(pos=[-0.925, -0.88], size=0.4)
308                self.add(self.histogram)
309            self.render()
310
311        if len(cmaps) > 1:
312            bu = self.add_button(
313                button_func,
314                states=cmaps,
315                c=["k9"] * len(cmaps),
316                bc=["k1"] * len(cmaps),  # colors of states
317                size=16,
318                bold=True,
319            )
320            if bu:
321                bu.pos([0.04, 0.01], "bottom-left")

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

Arguments:
  • cmaps : (list) list of color maps names to cycle when clicking button
  • clamp : (bool) clamp scalar range to reduce the effect of tails in color mapping
  • use_slider3d : (bool) show sliders attached along the axes
  • show_histo : (bool) show histogram on bottom left
  • show_icon : (bool) show a small 3D rendering icon of the volume
  • draggable : (bool) make the 3D icon draggable
  • at : (int) subwindow number to plot to
  • **kwargs : (dict) keyword arguments to pass to Plotter.
Examples:

class Slicer3DTwinPlotter(vedo.plotter.Plotter):
325class Slicer3DTwinPlotter(Plotter):
326    """
327    Create a window with two side-by-side 3D slicers for two Volumes.
328
329    Arguments:
330        vol1 : (Volume)
331            the first Volume object to be isosurfaced.
332        vol2 : (Volume)
333            the second Volume object to be isosurfaced.
334        clamp : (bool)
335            clamp scalar range to reduce the effect of tails in color mapping
336        **kwargs : (dict)
337            keyword arguments to pass to Plotter.
338
339    Example:
340        ```python
341        from vedo import *
342        from vedo.applications import Slicer3DTwinPlotter
343
344        vol1 = Volume(dataurl + "embryo.slc")
345        vol2 = Volume(dataurl + "embryo.slc")
346
347        plt = Slicer3DTwinPlotter(
348            vol1, vol2, 
349            shape=(1, 2), 
350            sharecam=True,
351            bg="white", 
352            bg2="lightblue",
353        )
354
355        plt.at(0).add(Text2D("Volume 1", pos="top-center"))
356        plt.at(1).add(Text2D("Volume 2", pos="top-center"))
357
358        plt.show(viewup='z')
359        plt.at(0).reset_camera()
360        plt.interactive().close()
361        ```
362
363        <img src="https://vedo.embl.es/images/volumetric/slicer3dtwin.png" width="650">
364    """
365
366    def __init__(self, vol1: vedo.Volume, vol2: vedo.Volume, clamp=True, **kwargs):
367
368        super().__init__(**kwargs)
369
370        cmap = "gist_ncar_r"
371        cx, cy, cz = "dr", "dg", "db"  # slider colors
372        ambient, diffuse = 0.7, 0.3  # lighting params
373
374        self.at(0)
375        box1 = vol1.box().alpha(0.1)
376        box2 = vol2.box().alpha(0.1)
377        self.add(box1)
378
379        self.at(1).add(box2)
380        self.add_inset(vol2, pos=(0.85, 0.15), size=0.15, c="white", draggable=0)
381
382        dims = vol1.dimensions()
383        data = vol1.pointdata[0]
384        rmin, rmax = vol1.scalar_range()
385        if clamp:
386            hdata, edg = np.histogram(data, bins=50)
387            logdata = np.log(hdata + 1)
388            meanlog = np.sum(np.multiply(edg[:-1], logdata)) / np.sum(logdata)
389            rmax = min(rmax, meanlog + (meanlog - rmin) * 0.9)
390            rmin = max(rmin, meanlog - (rmax - meanlog) * 0.9)
391
392        def slider_function_x(widget, event):
393            i = int(self.xslider.value)
394            msh1 = vol1.xslice(i).lighting("", ambient, diffuse, 0)
395            msh1.cmap(cmap, vmin=rmin, vmax=rmax)
396            msh1.name = "XSlice"
397            self.at(0).remove("XSlice")  # removes the old one
398            msh2 = vol2.xslice(i).lighting("", ambient, diffuse, 0)
399            msh2.cmap(cmap, vmin=rmin, vmax=rmax)
400            msh2.name = "XSlice"
401            self.at(1).remove("XSlice")
402            if 0 < i < dims[0]:
403                self.at(0).add(msh1)
404                self.at(1).add(msh2)
405
406        def slider_function_y(widget, event):
407            i = int(self.yslider.value)
408            msh1 = vol1.yslice(i).lighting("", ambient, diffuse, 0)
409            msh1.cmap(cmap, vmin=rmin, vmax=rmax)
410            msh1.name = "YSlice"
411            self.at(0).remove("YSlice")
412            msh2 = vol2.yslice(i).lighting("", ambient, diffuse, 0)
413            msh2.cmap(cmap, vmin=rmin, vmax=rmax)
414            msh2.name = "YSlice"
415            self.at(1).remove("YSlice")
416            if 0 < i < dims[1]:
417                self.at(0).add(msh1)
418                self.at(1).add(msh2)
419
420        def slider_function_z(widget, event):
421            i = int(self.zslider.value)
422            msh1 = vol1.zslice(i).lighting("", ambient, diffuse, 0)
423            msh1.cmap(cmap, vmin=rmin, vmax=rmax)
424            msh1.name = "ZSlice"
425            self.at(0).remove("ZSlice")
426            msh2 = vol2.zslice(i).lighting("", ambient, diffuse, 0)
427            msh2.cmap(cmap, vmin=rmin, vmax=rmax)
428            msh2.name = "ZSlice"
429            self.at(1).remove("ZSlice")
430            if 0 < i < dims[2]:
431                self.at(0).add(msh1)
432                self.at(1).add(msh2)
433
434        self.at(0)
435        bs = box1.bounds()
436        self.xslider = self.add_slider3d(
437            slider_function_x,
438            pos1=(bs[0], bs[2], bs[4]),
439            pos2=(bs[1], bs[2], bs[4]),
440            xmin=0,
441            xmax=dims[0],
442            t=box1.diagonal_size() / mag(box1.xbounds()) * 0.6,
443            c=cx,
444            show_value=False,
445        )
446        self.yslider = self.add_slider3d(
447            slider_function_y,
448            pos1=(bs[1], bs[2], bs[4]),
449            pos2=(bs[1], bs[3], bs[4]),
450            xmin=0,
451            xmax=dims[1],
452            t=box1.diagonal_size() / mag(box1.ybounds()) * 0.6,
453            c=cy,
454            show_value=False,
455        )
456        self.zslider = self.add_slider3d(
457            slider_function_z,
458            pos1=(bs[0], bs[2], bs[4]),
459            pos2=(bs[0], bs[2], bs[5]),
460            xmin=0,
461            xmax=dims[2],
462            value=int(dims[2] / 2),
463            t=box1.diagonal_size() / mag(box1.zbounds()) * 0.6,
464            c=cz,
465            show_value=False,
466        )
467
468        #################
469        hist = CornerHistogram(data, s=0.2, bins=25, logscale=True, c="k")
470        self.add(hist)
471        slider_function_z(0, 0)  ## init call

Create a window with two side-by-side 3D slicers for two Volumes.

Arguments:
  • vol1 : (Volume) the first Volume object to be isosurfaced.
  • vol2 : (Volume) the second Volume object to be isosurfaced.
  • clamp : (bool) clamp scalar range to reduce the effect of tails in color mapping
  • **kwargs : (dict) keyword arguments to pass to Plotter.
Example:
from vedo import *
from vedo.applications import Slicer3DTwinPlotter

vol1 = Volume(dataurl + "embryo.slc")
vol2 = Volume(dataurl + "embryo.slc")

plt = Slicer3DTwinPlotter(
    vol1, vol2, 
    shape=(1, 2), 
    sharecam=True,
    bg="white", 
    bg2="lightblue",
)

plt.at(0).add(Text2D("Volume 1", pos="top-center"))
plt.at(1).add(Text2D("Volume 2", pos="top-center"))

plt.show(viewup='z')
plt.at(0).reset_camera()
plt.interactive().close()

Slicer3DTwinPlotter( vol1: vedo.volume.Volume, vol2: vedo.volume.Volume, clamp=True, **kwargs)
366    def __init__(self, vol1: vedo.Volume, vol2: vedo.Volume, clamp=True, **kwargs):
367
368        super().__init__(**kwargs)
369
370        cmap = "gist_ncar_r"
371        cx, cy, cz = "dr", "dg", "db"  # slider colors
372        ambient, diffuse = 0.7, 0.3  # lighting params
373
374        self.at(0)
375        box1 = vol1.box().alpha(0.1)
376        box2 = vol2.box().alpha(0.1)
377        self.add(box1)
378
379        self.at(1).add(box2)
380        self.add_inset(vol2, pos=(0.85, 0.15), size=0.15, c="white", draggable=0)
381
382        dims = vol1.dimensions()
383        data = vol1.pointdata[0]
384        rmin, rmax = vol1.scalar_range()
385        if clamp:
386            hdata, edg = np.histogram(data, bins=50)
387            logdata = np.log(hdata + 1)
388            meanlog = np.sum(np.multiply(edg[:-1], logdata)) / np.sum(logdata)
389            rmax = min(rmax, meanlog + (meanlog - rmin) * 0.9)
390            rmin = max(rmin, meanlog - (rmax - meanlog) * 0.9)
391
392        def slider_function_x(widget, event):
393            i = int(self.xslider.value)
394            msh1 = vol1.xslice(i).lighting("", ambient, diffuse, 0)
395            msh1.cmap(cmap, vmin=rmin, vmax=rmax)
396            msh1.name = "XSlice"
397            self.at(0).remove("XSlice")  # removes the old one
398            msh2 = vol2.xslice(i).lighting("", ambient, diffuse, 0)
399            msh2.cmap(cmap, vmin=rmin, vmax=rmax)
400            msh2.name = "XSlice"
401            self.at(1).remove("XSlice")
402            if 0 < i < dims[0]:
403                self.at(0).add(msh1)
404                self.at(1).add(msh2)
405
406        def slider_function_y(widget, event):
407            i = int(self.yslider.value)
408            msh1 = vol1.yslice(i).lighting("", ambient, diffuse, 0)
409            msh1.cmap(cmap, vmin=rmin, vmax=rmax)
410            msh1.name = "YSlice"
411            self.at(0).remove("YSlice")
412            msh2 = vol2.yslice(i).lighting("", ambient, diffuse, 0)
413            msh2.cmap(cmap, vmin=rmin, vmax=rmax)
414            msh2.name = "YSlice"
415            self.at(1).remove("YSlice")
416            if 0 < i < dims[1]:
417                self.at(0).add(msh1)
418                self.at(1).add(msh2)
419
420        def slider_function_z(widget, event):
421            i = int(self.zslider.value)
422            msh1 = vol1.zslice(i).lighting("", ambient, diffuse, 0)
423            msh1.cmap(cmap, vmin=rmin, vmax=rmax)
424            msh1.name = "ZSlice"
425            self.at(0).remove("ZSlice")
426            msh2 = vol2.zslice(i).lighting("", ambient, diffuse, 0)
427            msh2.cmap(cmap, vmin=rmin, vmax=rmax)
428            msh2.name = "ZSlice"
429            self.at(1).remove("ZSlice")
430            if 0 < i < dims[2]:
431                self.at(0).add(msh1)
432                self.at(1).add(msh2)
433
434        self.at(0)
435        bs = box1.bounds()
436        self.xslider = self.add_slider3d(
437            slider_function_x,
438            pos1=(bs[0], bs[2], bs[4]),
439            pos2=(bs[1], bs[2], bs[4]),
440            xmin=0,
441            xmax=dims[0],
442            t=box1.diagonal_size() / mag(box1.xbounds()) * 0.6,
443            c=cx,
444            show_value=False,
445        )
446        self.yslider = self.add_slider3d(
447            slider_function_y,
448            pos1=(bs[1], bs[2], bs[4]),
449            pos2=(bs[1], bs[3], bs[4]),
450            xmin=0,
451            xmax=dims[1],
452            t=box1.diagonal_size() / mag(box1.ybounds()) * 0.6,
453            c=cy,
454            show_value=False,
455        )
456        self.zslider = self.add_slider3d(
457            slider_function_z,
458            pos1=(bs[0], bs[2], bs[4]),
459            pos2=(bs[0], bs[2], bs[5]),
460            xmin=0,
461            xmax=dims[2],
462            value=int(dims[2] / 2),
463            t=box1.diagonal_size() / mag(box1.zbounds()) * 0.6,
464            c=cz,
465            show_value=False,
466        )
467
468        #################
469        hist = CornerHistogram(data, s=0.2, bins=25, logscale=True, c="k")
470        self.add(hist)
471        slider_function_z(0, 0)  ## init call
Arguments:
  • shape : (str, list) shape of the grid of renderers in format (rows, columns). Ignored if N is specified.
  • N : (int) number of desired renderers arranged in a grid automatically.
  • pos : (list) (x,y) position in pixels of top-left corner of the rendering window on the screen
  • size : (str, list) size of the rendering window. If 'auto', guess it based on screensize.
  • screensize : (list) physical size of the monitor screen in pixels
  • bg : (color, str) background color or specify jpg image file name with path
  • bg2 : (color) background color of a gradient towards the top
  • title : (str) window title
  • axes : (int)

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

    - 0,  no axes
    - 1,  draw three gray grid walls
    - 2,  show cartesian axes from (0,0,0)
    - 3,  show positive range of cartesian axes from (0,0,0)
    - 4,  show a triad at bottom left
    - 5,  show a cube at bottom left
    - 6,  mark the corners of the bounding box
    - 7,  draw a 3D ruler at each side of the cartesian axes
    - 8,  show the VTK CubeAxesActor object
    - 9,  show the bounding box outLine
    - 10, show three circles representing the maximum bounding box
    - 11, show a large grid on the x-y plane (use with zoom=8)
    - 12, show polar axes
    - 13, draw a simple ruler at the bottom of the window
    - 14: draw a camera orientation widget
    
  • sharecam : (bool) if False each renderer will have an independent camera
  • interactive : (bool) if True will stop after show() to allow interaction with the 3d scene
  • offscreen : (bool) if True will not show the rendering window
  • qt_widget : (QVTKRenderWindowInteractor) render in a Qt-Widget using an QVTKRenderWindowInteractor. See examples qt_windows[1,2,3].py and qt_cutter.py.
class MorphPlotter(vedo.plotter.Plotter):
475class MorphPlotter(Plotter):
476    """
477    A Plotter with 3 renderers to show the source, target and warped meshes.
478
479    Examples:
480        - [warp4b.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp4b.py)
481
482            ![](https://vedo.embl.es/images/advanced/warp4b.jpg)
483    """
484        
485    def __init__(self, source, target, **kwargs):
486
487        vedo.settings.enable_default_keyboard_callbacks = False
488        vedo.settings.enable_default_mouse_callbacks = False
489
490        kwargs.update({"N": 3})
491        kwargs.update({"sharecam": 0})
492        super().__init__(**kwargs)
493
494        self.source = source.pickable(True)
495        self.target = target.pickable(False)
496        self.clicked = []
497        self.sources = []
498        self.targets = []
499        self.warped = None
500        self.source_labels = None
501        self.target_labels = None
502        self.automatic_picking_distance = 0.075
503        self.cmap_name = "coolwarm"
504        self.nbins = 25
505        self.msg0 = Text2D("Pick a point on the surface",
506                           pos="bottom-center", c='white', bg="blue4", alpha=1, font="Calco")
507        self.msg1 = Text2D(pos="bottom-center", c='white', bg="blue4", alpha=1, font="Calco")
508        self.instructions = Text2D(s=0.7, bg="blue4", alpha=0.1, font="Calco")
509        self.instructions.text(
510            "  Morphological alignment of 3D surfaces\n\n"
511            "Pick a point on the source surface, then\n"
512            "pick the corresponding point on the target \n"
513            "Pick at least 4 point pairs. Press:\n"
514            "- c to clear all landmarks\n"
515            "- d to delete the last landmark pair\n"
516            "- a to auto-pick additional landmarks\n"
517            "- z to compute and show the residuals\n"
518            "- q to quit and proceed"
519        )
520        self.at(0).add_renderer_frame()
521        self.add(source, self.msg0, self.instructions).reset_camera()
522        self.at(1).add_renderer_frame()
523        self.add(Text2D(f"Target: {target.filename[-35:]}", bg="blue4", alpha=0.1, font="Calco"))
524        self.add(self.msg1, target)
525        cam1 = self.camera  # save camera at 1
526        self.at(2).background("k9")
527        self.add(target, Text2D("Morphing Output", font="Calco"))
528        self.camera = cam1  # use the same camera of renderer1
529
530        self.add_renderer_frame()
531    
532        self.callid1 = self.add_callback("KeyPress", self.on_keypress)
533        self.callid2 = self.add_callback("LeftButtonPress", self.on_click)
534        self._interactive = True
535
536    ################################################
537    def update(self):
538        source_pts = Points(self.sources).color("purple5").ps(12)
539        target_pts = Points(self.targets).color("purple5").ps(12)
540        source_pts.name = "source_pts"
541        target_pts.name = "target_pts"
542        self.source_labels = source_pts.labels2d("id", c="purple3")
543        self.target_labels = target_pts.labels2d("id", c="purple3")
544        self.source_labels.name = "source_pts"
545        self.target_labels.name = "target_pts"
546        self.at(0).remove("source_pts").add(source_pts, self.source_labels)
547        self.at(1).remove("target_pts").add(target_pts, self.target_labels)
548        self.render()
549
550        if len(self.sources) == len(self.targets) and len(self.sources) > 3:
551            self.warped = self.source.clone().warp(self.sources, self.targets)
552            self.warped.name = "warped"
553            self.at(2).remove("warped").add(self.warped)
554            self.render()
555
556    def on_click(self, evt):
557        if evt.object == self.source:
558            self.sources.append(evt.picked3d)
559            self.source.pickable(False)
560            self.target.pickable(True)
561            self.msg0.text("--->")
562            self.msg1.text("now pick a target point")
563            self.update()
564        elif evt.object == self.target:
565            self.targets.append(evt.picked3d)
566            self.source.pickable(True)
567            self.target.pickable(False)
568            self.msg0.text("now pick a source point")
569            self.msg1.text("<---")
570            self.update()
571
572    def on_keypress(self, evt):
573        if evt.keypress == "c":
574            self.sources.clear()
575            self.targets.clear()
576            self.at(0).remove("source_pts")
577            self.at(1).remove("target_pts")
578            self.at(2).remove("warped")
579            self.msg0.text("CLEARED! Pick a point here")
580            self.msg1.text("")
581            self.source.pickable(True)
582            self.target.pickable(False)
583            self.update()
584        if evt.keypress == "w":
585            rep = (self.warped.properties.GetRepresentation() == 1)
586            self.warped.wireframe(not rep)
587            self.render()
588        if evt.keypress == "d":
589            n = min(len(self.sources), len(self.targets))
590            self.sources = self.sources[:n-1]
591            self.targets = self.targets[:n-1]
592            self.msg0.text("Last point deleted! Pick a point here")
593            self.msg1.text("")
594            self.source.pickable(True)
595            self.target.pickable(False)
596            self.update()
597        if evt.keypress == "a":
598            # auto-pick points on the target surface
599            if not self.warped:
600                vedo.printc("At least 4 points are needed.", c="r")
601                return
602            pts = self.target.clone().subsample(self.automatic_picking_distance)
603            if len(self.sources) > len(self.targets):
604                self.sources.pop()
605            d = self.target.diagonal_size()
606            r = d * self.automatic_picking_distance
607            TI = self.warped.transform.compute_inverse()
608            for p in pts.coordinates:
609                pp = vedo.utils.closest(p, self.targets)[1]
610                if vedo.mag(pp - p) < r:
611                    continue
612                q = self.warped.closest_point(p)
613                self.sources.append(TI(q))
614                self.targets.append(p)
615            self.source.pickable(True)
616            self.target.pickable(False)
617            self.update()            
618        if evt.keypress == "z" or evt.keypress == "a":
619            dists = self.warped.distance_to(self.target, signed=True)
620            v = np.std(dists) * 2
621            self.warped.cmap(self.cmap_name, dists, vmin=-v, vmax=+v)
622
623            h = vedo.pyplot.histogram(
624                dists, 
625                bins=self.nbins,
626                title=" ",
627                xtitle=f"STD = {v/2:.2f}",
628                ytitle="",
629                c=self.cmap_name, 
630                xlim=(-v, v),
631                aspect=16/9,
632                axes=dict(
633                    number_of_divisions=5,
634                    text_scale=2,
635                    xtitle_offset=0.075,
636                    xlabel_justify="top-center"),
637            )
638
639            # try to fit a gaussian to the histogram
640            def gauss(x, A, B, sigma):
641                return A + B * np.exp(-x**2 / (2 * sigma**2))
642            try:
643                from scipy.optimize import curve_fit
644                inits = [0, len(dists)/self.nbins*2.5, v/2]
645                popt, _ = curve_fit(gauss, xdata=h.centers, ydata=h.frequencies, p0=inits)
646                x = np.linspace(-v, v, 300)
647                h += vedo.pyplot.plot(x, gauss(x, *popt), like=h, lw=1, lc="k2")
648                h["Axes"]["xtitle"].text(f":sigma = {abs(popt[2]):.3f}", font="VictorMono")
649            except:
650                pass
651
652            h = h.clone2d(pos="bottom-left", size=0.575)
653            h.name = "warped"
654            self.at(2).add(h)
655            self.render()
656    
657        if evt.keypress == "q":
658            self.break_interaction()

A Plotter with 3 renderers to show the source, target and warped meshes.

Examples:
MorphPlotter(source, target, **kwargs)
485    def __init__(self, source, target, **kwargs):
486
487        vedo.settings.enable_default_keyboard_callbacks = False
488        vedo.settings.enable_default_mouse_callbacks = False
489
490        kwargs.update({"N": 3})
491        kwargs.update({"sharecam": 0})
492        super().__init__(**kwargs)
493
494        self.source = source.pickable(True)
495        self.target = target.pickable(False)
496        self.clicked = []
497        self.sources = []
498        self.targets = []
499        self.warped = None
500        self.source_labels = None
501        self.target_labels = None
502        self.automatic_picking_distance = 0.075
503        self.cmap_name = "coolwarm"
504        self.nbins = 25
505        self.msg0 = Text2D("Pick a point on the surface",
506                           pos="bottom-center", c='white', bg="blue4", alpha=1, font="Calco")
507        self.msg1 = Text2D(pos="bottom-center", c='white', bg="blue4", alpha=1, font="Calco")
508        self.instructions = Text2D(s=0.7, bg="blue4", alpha=0.1, font="Calco")
509        self.instructions.text(
510            "  Morphological alignment of 3D surfaces\n\n"
511            "Pick a point on the source surface, then\n"
512            "pick the corresponding point on the target \n"
513            "Pick at least 4 point pairs. Press:\n"
514            "- c to clear all landmarks\n"
515            "- d to delete the last landmark pair\n"
516            "- a to auto-pick additional landmarks\n"
517            "- z to compute and show the residuals\n"
518            "- q to quit and proceed"
519        )
520        self.at(0).add_renderer_frame()
521        self.add(source, self.msg0, self.instructions).reset_camera()
522        self.at(1).add_renderer_frame()
523        self.add(Text2D(f"Target: {target.filename[-35:]}", bg="blue4", alpha=0.1, font="Calco"))
524        self.add(self.msg1, target)
525        cam1 = self.camera  # save camera at 1
526        self.at(2).background("k9")
527        self.add(target, Text2D("Morphing Output", font="Calco"))
528        self.camera = cam1  # use the same camera of renderer1
529
530        self.add_renderer_frame()
531    
532        self.callid1 = self.add_callback("KeyPress", self.on_keypress)
533        self.callid2 = self.add_callback("LeftButtonPress", self.on_click)
534        self._interactive = True
Arguments:
  • shape : (str, list) shape of the grid of renderers in format (rows, columns). Ignored if N is specified.
  • N : (int) number of desired renderers arranged in a grid automatically.
  • pos : (list) (x,y) position in pixels of top-left corner of the rendering window on the screen
  • size : (str, list) size of the rendering window. If 'auto', guess it based on screensize.
  • screensize : (list) physical size of the monitor screen in pixels
  • bg : (color, str) background color or specify jpg image file name with path
  • bg2 : (color) background color of a gradient towards the top
  • title : (str) window title
  • axes : (int)

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

    - 0,  no axes
    - 1,  draw three gray grid walls
    - 2,  show cartesian axes from (0,0,0)
    - 3,  show positive range of cartesian axes from (0,0,0)
    - 4,  show a triad at bottom left
    - 5,  show a cube at bottom left
    - 6,  mark the corners of the bounding box
    - 7,  draw a 3D ruler at each side of the cartesian axes
    - 8,  show the VTK CubeAxesActor object
    - 9,  show the bounding box outLine
    - 10, show three circles representing the maximum bounding box
    - 11, show a large grid on the x-y plane (use with zoom=8)
    - 12, show polar axes
    - 13, draw a simple ruler at the bottom of the window
    - 14: draw a camera orientation widget
    
  • sharecam : (bool) if False each renderer will have an independent camera
  • interactive : (bool) if True will stop after show() to allow interaction with the 3d scene
  • offscreen : (bool) if True will not show the rendering window
  • qt_widget : (QVTKRenderWindowInteractor) render in a Qt-Widget using an QVTKRenderWindowInteractor. See examples qt_windows[1,2,3].py and qt_cutter.py.
camera
3704    @property
3705    def camera(self):
3706        """Return the current active camera."""
3707        if self.renderer:
3708            return self.renderer.GetActiveCamera()

Return the current active camera.

def update(self):
537    def update(self):
538        source_pts = Points(self.sources).color("purple5").ps(12)
539        target_pts = Points(self.targets).color("purple5").ps(12)
540        source_pts.name = "source_pts"
541        target_pts.name = "target_pts"
542        self.source_labels = source_pts.labels2d("id", c="purple3")
543        self.target_labels = target_pts.labels2d("id", c="purple3")
544        self.source_labels.name = "source_pts"
545        self.target_labels.name = "target_pts"
546        self.at(0).remove("source_pts").add(source_pts, self.source_labels)
547        self.at(1).remove("target_pts").add(target_pts, self.target_labels)
548        self.render()
549
550        if len(self.sources) == len(self.targets) and len(self.sources) > 3:
551            self.warped = self.source.clone().warp(self.sources, self.targets)
552            self.warped.name = "warped"
553            self.at(2).remove("warped").add(self.warped)
554            self.render()
def on_click(self, evt):
556    def on_click(self, evt):
557        if evt.object == self.source:
558            self.sources.append(evt.picked3d)
559            self.source.pickable(False)
560            self.target.pickable(True)
561            self.msg0.text("--->")
562            self.msg1.text("now pick a target point")
563            self.update()
564        elif evt.object == self.target:
565            self.targets.append(evt.picked3d)
566            self.source.pickable(True)
567            self.target.pickable(False)
568            self.msg0.text("now pick a source point")
569            self.msg1.text("<---")
570            self.update()
def on_keypress(self, evt):
572    def on_keypress(self, evt):
573        if evt.keypress == "c":
574            self.sources.clear()
575            self.targets.clear()
576            self.at(0).remove("source_pts")
577            self.at(1).remove("target_pts")
578            self.at(2).remove("warped")
579            self.msg0.text("CLEARED! Pick a point here")
580            self.msg1.text("")
581            self.source.pickable(True)
582            self.target.pickable(False)
583            self.update()
584        if evt.keypress == "w":
585            rep = (self.warped.properties.GetRepresentation() == 1)
586            self.warped.wireframe(not rep)
587            self.render()
588        if evt.keypress == "d":
589            n = min(len(self.sources), len(self.targets))
590            self.sources = self.sources[:n-1]
591            self.targets = self.targets[:n-1]
592            self.msg0.text("Last point deleted! Pick a point here")
593            self.msg1.text("")
594            self.source.pickable(True)
595            self.target.pickable(False)
596            self.update()
597        if evt.keypress == "a":
598            # auto-pick points on the target surface
599            if not self.warped:
600                vedo.printc("At least 4 points are needed.", c="r")
601                return
602            pts = self.target.clone().subsample(self.automatic_picking_distance)
603            if len(self.sources) > len(self.targets):
604                self.sources.pop()
605            d = self.target.diagonal_size()
606            r = d * self.automatic_picking_distance
607            TI = self.warped.transform.compute_inverse()
608            for p in pts.coordinates:
609                pp = vedo.utils.closest(p, self.targets)[1]
610                if vedo.mag(pp - p) < r:
611                    continue
612                q = self.warped.closest_point(p)
613                self.sources.append(TI(q))
614                self.targets.append(p)
615            self.source.pickable(True)
616            self.target.pickable(False)
617            self.update()            
618        if evt.keypress == "z" or evt.keypress == "a":
619            dists = self.warped.distance_to(self.target, signed=True)
620            v = np.std(dists) * 2
621            self.warped.cmap(self.cmap_name, dists, vmin=-v, vmax=+v)
622
623            h = vedo.pyplot.histogram(
624                dists, 
625                bins=self.nbins,
626                title=" ",
627                xtitle=f"STD = {v/2:.2f}",
628                ytitle="",
629                c=self.cmap_name, 
630                xlim=(-v, v),
631                aspect=16/9,
632                axes=dict(
633                    number_of_divisions=5,
634                    text_scale=2,
635                    xtitle_offset=0.075,
636                    xlabel_justify="top-center"),
637            )
638
639            # try to fit a gaussian to the histogram
640            def gauss(x, A, B, sigma):
641                return A + B * np.exp(-x**2 / (2 * sigma**2))
642            try:
643                from scipy.optimize import curve_fit
644                inits = [0, len(dists)/self.nbins*2.5, v/2]
645                popt, _ = curve_fit(gauss, xdata=h.centers, ydata=h.frequencies, p0=inits)
646                x = np.linspace(-v, v, 300)
647                h += vedo.pyplot.plot(x, gauss(x, *popt), like=h, lw=1, lc="k2")
648                h["Axes"]["xtitle"].text(f":sigma = {abs(popt[2]):.3f}", font="VictorMono")
649            except:
650                pass
651
652            h = h.clone2d(pos="bottom-left", size=0.575)
653            h.name = "warped"
654            self.at(2).add(h)
655            self.render()
656    
657        if evt.keypress == "q":
658            self.break_interaction()
class SplinePlotter(vedo.plotter.Plotter):
1635class SplinePlotter(Plotter):
1636    """
1637    Interactive drawing of splined curves on meshes.
1638    """
1639
1640    def __init__(self, obj, init_points=(), closed=False, splined=True, mode="auto", **kwargs):
1641        """
1642        Create an interactive application that allows the user to click points and
1643        retrieve the coordinates of such points and optionally a spline or line
1644        (open or closed).
1645        Input object can be a image file name or a 3D mesh.
1646
1647        Arguments:
1648            obj : (Mesh, str)
1649                The input object can be a image file name or a 3D mesh.
1650            init_points : (list)
1651                Set an initial number of points to define a region.
1652            closed : (bool)
1653                Close the spline or line.
1654            splined : (bool)
1655                Join points with a spline or a simple line.
1656            mode : (str)
1657                Set the mode of interaction.
1658            **kwargs : (dict)
1659                keyword arguments to pass to Plotter.
1660        """
1661        super().__init__(**kwargs)
1662
1663        self.verbose = True
1664        self.splined = splined
1665        self.resolution = None  # spline resolution (None = automatic)
1666        self.closed = closed
1667        self.lcolor = "yellow4"
1668        self.lwidth = 3
1669        self.pcolor = "purple5"
1670        self.psize = 10
1671
1672        self.cpoints = list(init_points)
1673        self.vpoints = None
1674        self.line = None
1675
1676        if isinstance(obj, str):
1677            self.object = vedo.file_io.load(obj)
1678        else:
1679            self.object = obj
1680
1681        self.mode = mode
1682        if self.mode == "auto":
1683            if isinstance(self.object, vedo.Image):
1684                self.mode = "image"
1685                self.parallel_projection(True)
1686            else:
1687                self.mode = "TrackballCamera"
1688
1689        t = (
1690            "Click to add a point\n"
1691            "Right-click to remove it\n"
1692            "Drag mouse to change contrast\n"
1693            "Press c to clear points\n"
1694            "Press q to continue"
1695        )
1696        self.instructions = Text2D(t, pos="bottom-left", c="white", bg="green", font="Calco")
1697
1698        self += [self.object, self.instructions]
1699
1700        self.callid1 = self.add_callback("KeyPress", self._key_press)
1701        self.callid2 = self.add_callback("LeftButtonPress", self._on_left_click)
1702        self.callid3 = self.add_callback("RightButtonPress", self._on_right_click)
1703
1704
1705    def points(self, newpts=None) -> Union["SplinePlotter", np.ndarray]:
1706        """Retrieve the 3D coordinates of the clicked points"""
1707        if newpts is not None:
1708            self.cpoints = newpts
1709            self.update()
1710            return self
1711        return np.array(self.cpoints)
1712
1713    def _on_left_click(self, evt):
1714        if not evt.actor:
1715            return
1716        if evt.actor.name == "points":
1717            # remove clicked point if clicked twice
1718            pid = self.vpoints.closest_point(evt.picked3d, return_point_id=True)
1719            self.cpoints.pop(pid)
1720            self.update()
1721            return
1722        p = evt.picked3d
1723        self.cpoints.append(p)
1724        self.update()
1725        if self.verbose:
1726            vedo.colors.printc("Added point:", precision(p, 4), c="g")
1727
1728    def _on_right_click(self, evt):
1729        if evt.actor and len(self.cpoints) > 0:
1730            self.cpoints.pop()  # pop removes from the list the last pt
1731            self.update()
1732            if self.verbose:
1733                vedo.colors.printc("Deleted last point", c="r")
1734
1735    def update(self):
1736        self.remove(self.line, self.vpoints)  # remove old points and spline
1737        self.vpoints = Points(self.cpoints).ps(self.psize).c(self.pcolor)
1738        self.vpoints.name = "points"
1739        self.vpoints.pickable(True)  # to allow toggle
1740        minnr = 1
1741        if self.splined:
1742            minnr = 2
1743        if self.lwidth and len(self.cpoints) > minnr:
1744            if self.splined:
1745                try:
1746                    self.line = Spline(self.cpoints, closed=self.closed, res=self.resolution)
1747                except ValueError:
1748                    # if clicking too close splining might fail
1749                    self.cpoints.pop()
1750                    return
1751            else:
1752                self.line = Line(self.cpoints, closed=self.closed)
1753            self.line.c(self.lcolor).lw(self.lwidth).pickable(False)
1754            self.add(self.vpoints, self.line)
1755        else:
1756            self.add(self.vpoints)
1757
1758    def _key_press(self, evt):
1759        if evt.keypress == "c":
1760            self.cpoints = []
1761            self.remove(self.line, self.vpoints).render()
1762            if self.verbose:
1763                vedo.colors.printc("==== Cleared all points ====", c="r", invert=True)
1764
1765    def start(self) -> "SplinePlotter":
1766        """Start the interaction"""
1767        self.update()
1768        self.show(self.object, self.instructions, mode=self.mode)
1769        return self

Interactive drawing of splined curves on meshes.

SplinePlotter( obj, init_points=(), closed=False, splined=True, mode='auto', **kwargs)
1640    def __init__(self, obj, init_points=(), closed=False, splined=True, mode="auto", **kwargs):
1641        """
1642        Create an interactive application that allows the user to click points and
1643        retrieve the coordinates of such points and optionally a spline or line
1644        (open or closed).
1645        Input object can be a image file name or a 3D mesh.
1646
1647        Arguments:
1648            obj : (Mesh, str)
1649                The input object can be a image file name or a 3D mesh.
1650            init_points : (list)
1651                Set an initial number of points to define a region.
1652            closed : (bool)
1653                Close the spline or line.
1654            splined : (bool)
1655                Join points with a spline or a simple line.
1656            mode : (str)
1657                Set the mode of interaction.
1658            **kwargs : (dict)
1659                keyword arguments to pass to Plotter.
1660        """
1661        super().__init__(**kwargs)
1662
1663        self.verbose = True
1664        self.splined = splined
1665        self.resolution = None  # spline resolution (None = automatic)
1666        self.closed = closed
1667        self.lcolor = "yellow4"
1668        self.lwidth = 3
1669        self.pcolor = "purple5"
1670        self.psize = 10
1671
1672        self.cpoints = list(init_points)
1673        self.vpoints = None
1674        self.line = None
1675
1676        if isinstance(obj, str):
1677            self.object = vedo.file_io.load(obj)
1678        else:
1679            self.object = obj
1680
1681        self.mode = mode
1682        if self.mode == "auto":
1683            if isinstance(self.object, vedo.Image):
1684                self.mode = "image"
1685                self.parallel_projection(True)
1686            else:
1687                self.mode = "TrackballCamera"
1688
1689        t = (
1690            "Click to add a point\n"
1691            "Right-click to remove it\n"
1692            "Drag mouse to change contrast\n"
1693            "Press c to clear points\n"
1694            "Press q to continue"
1695        )
1696        self.instructions = Text2D(t, pos="bottom-left", c="white", bg="green", font="Calco")
1697
1698        self += [self.object, self.instructions]
1699
1700        self.callid1 = self.add_callback("KeyPress", self._key_press)
1701        self.callid2 = self.add_callback("LeftButtonPress", self._on_left_click)
1702        self.callid3 = self.add_callback("RightButtonPress", self._on_right_click)

Create an interactive application that allows the user to click points and retrieve the coordinates of such points and optionally a spline or line (open or closed). Input object can be a image file name or a 3D mesh.

Arguments:
  • obj : (Mesh, str) The input object can be a image file name or a 3D mesh.
  • init_points : (list) Set an initial number of points to define a region.
  • closed : (bool) Close the spline or line.
  • splined : (bool) Join points with a spline or a simple line.
  • mode : (str) Set the mode of interaction.
  • **kwargs : (dict) keyword arguments to pass to Plotter.
def points( self, newpts=None) -> Union[SplinePlotter, numpy.ndarray]:
1705    def points(self, newpts=None) -> Union["SplinePlotter", np.ndarray]:
1706        """Retrieve the 3D coordinates of the clicked points"""
1707        if newpts is not None:
1708            self.cpoints = newpts
1709            self.update()
1710            return self
1711        return np.array(self.cpoints)

Retrieve the 3D coordinates of the clicked points

def update(self):
1735    def update(self):
1736        self.remove(self.line, self.vpoints)  # remove old points and spline
1737        self.vpoints = Points(self.cpoints).ps(self.psize).c(self.pcolor)
1738        self.vpoints.name = "points"
1739        self.vpoints.pickable(True)  # to allow toggle
1740        minnr = 1
1741        if self.splined:
1742            minnr = 2
1743        if self.lwidth and len(self.cpoints) > minnr:
1744            if self.splined:
1745                try:
1746                    self.line = Spline(self.cpoints, closed=self.closed, res=self.resolution)
1747                except ValueError:
1748                    # if clicking too close splining might fail
1749                    self.cpoints.pop()
1750                    return
1751            else:
1752                self.line = Line(self.cpoints, closed=self.closed)
1753            self.line.c(self.lcolor).lw(self.lwidth).pickable(False)
1754            self.add(self.vpoints, self.line)
1755        else:
1756            self.add(self.vpoints)
def start(self) -> SplinePlotter:
1765    def start(self) -> "SplinePlotter":
1766        """Start the interaction"""
1767        self.update()
1768        self.show(self.object, self.instructions, mode=self.mode)
1769        return self

Start the interaction

class AnimationPlayer(vedo.plotter.Plotter):
2176class AnimationPlayer(vedo.Plotter):
2177    """
2178    A Plotter with play/pause, step forward/backward and slider functionalties.
2179    Useful for inspecting time series.
2180
2181    The user has the responsibility to update all actors in the callback function.
2182
2183    Arguments:
2184        func :  (Callable)
2185            a function that passes an integer as input and updates the scene
2186        irange : (tuple)
2187            the range of the integer input representing the time series index
2188        dt : (float)
2189            the time interval between two calls to `func` in milliseconds
2190        loop : (bool)
2191            whether to loop the animation
2192        c : (list, str)
2193            the color of the play/pause button
2194        bc : (list)
2195            the background color of the play/pause button and the slider
2196        button_size : (int)
2197            the size of the play/pause buttons
2198        button_pos : (float, float)
2199            the position of the play/pause buttons as a fraction of the window size
2200        button_gap : (float)
2201            the gap between the buttons
2202        slider_length : (float)
2203            the length of the slider as a fraction of the window size
2204        slider_pos : (float, float)
2205            the position of the slider as a fraction of the window size
2206        kwargs: (dict)
2207            keyword arguments to be passed to `Plotter`
2208
2209    Examples:
2210        - [aspring2_player.py](https://vedo.embl.es/images/simulations/spring_player.gif)
2211    """
2212
2213    # Original class contributed by @mikaeltulldahl (Mikael Tulldahl)
2214
2215    PLAY_SYMBOL        = "    \u23F5   "
2216    PAUSE_SYMBOL       = "   \u23F8   "
2217    ONE_BACK_SYMBOL    = " \u29CF"
2218    ONE_FORWARD_SYMBOL = "\u29D0 "
2219
2220    def __init__(
2221        self,
2222        func,
2223        irange: tuple,
2224        dt: float = 1.0,
2225        loop: bool = True,
2226        c=("white", "white"),
2227        bc=("green3", "red4"),
2228        button_size=25,
2229        button_pos=(0.5, 0.04),
2230        button_gap=0.055,
2231        slider_length=0.5,
2232        slider_pos=(0.5, 0.055),
2233        **kwargs,
2234    ):
2235        super().__init__(**kwargs)
2236
2237        min_value, max_value = np.array(irange).astype(int)
2238        button_pos = np.array(button_pos)
2239        slider_pos = np.array(slider_pos)
2240
2241        self._func = func
2242
2243        self.value = min_value - 1
2244        self.min_value = min_value
2245        self.max_value = max_value
2246        self.dt = max(dt, 1)
2247        self.is_playing = False
2248        self._loop = loop
2249
2250        self.timer_callback_id = self.add_callback(
2251            "timer", self._handle_timer, enable_picking=False
2252        )
2253        self.timer_id = None
2254
2255        self.play_pause_button = self.add_button(
2256            self.toggle,
2257            pos=button_pos,  # x,y fraction from bottom left corner
2258            states=[self.PLAY_SYMBOL, self.PAUSE_SYMBOL],
2259            font="Kanopus",
2260            size=button_size,
2261            bc=bc,
2262        )
2263        self.button_oneback = self.add_button(
2264            self.onebackward,
2265            pos=(-button_gap, 0) + button_pos,
2266            states=[self.ONE_BACK_SYMBOL],
2267            font="Kanopus",
2268            size=button_size,
2269            c=c,
2270            bc=bc,
2271        )
2272        self.button_oneforward = self.add_button(
2273            self.oneforward,
2274            pos=(button_gap, 0) + button_pos,
2275            states=[self.ONE_FORWARD_SYMBOL],
2276            font="Kanopus",
2277            size=button_size,
2278            bc=bc,
2279        )
2280        d = (1 - slider_length) / 2
2281        self.slider: SliderWidget = self.add_slider(
2282            self._slider_callback,
2283            self.min_value,
2284            self.max_value - 1,
2285            value=self.min_value,
2286            pos=[(d - 0.5, 0) + slider_pos, (0.5 - d, 0) + slider_pos],
2287            show_value=False,
2288            c=bc[0],
2289            alpha=1,
2290        )
2291
2292    def pause(self) -> None:
2293        """Pause the animation."""
2294        self.is_playing = False
2295        if self.timer_id is not None:
2296            self.timer_callback("destroy", self.timer_id)
2297            self.timer_id = None
2298        self.play_pause_button.status(self.PLAY_SYMBOL)
2299
2300    def resume(self) -> None:
2301        """Resume the animation."""
2302        if self.timer_id is not None:
2303            self.timer_callback("destroy", self.timer_id)
2304        self.timer_id = self.timer_callback("create", dt=int(self.dt))
2305        self.is_playing = True
2306        self.play_pause_button.status(self.PAUSE_SYMBOL)
2307
2308    def toggle(self, _obj, _evt) -> None:
2309        """Toggle between play and pause."""
2310        if not self.is_playing:
2311            self.resume()
2312        else:
2313            self.pause()
2314
2315    def oneforward(self, _obj, _evt) -> None:
2316        """Advance the animation by one frame."""
2317        self.pause()
2318        self.set_frame(self.value + 1)
2319
2320    def onebackward(self, _obj, _evt) -> None:
2321        """Go back one frame in the animation."""
2322        self.pause()
2323        self.set_frame(self.value - 1)
2324
2325    def set_frame(self, value: int) -> None:
2326        """Set the current value of the animation."""
2327        if self._loop:
2328            if value < self.min_value:
2329                value = self.max_value - 1
2330            elif value >= self.max_value:
2331                value = self.min_value
2332        else:
2333            if value < self.min_value:
2334                self.pause()
2335                value = self.min_value
2336            elif value >= self.max_value - 1:
2337                value = self.max_value - 1
2338                self.pause()
2339
2340        if self.value != value:
2341            self.value = value
2342            self.slider.value = value
2343            self._func(value)
2344
2345    def _slider_callback(self, widget: SliderWidget, _: str) -> None:
2346        self.pause()
2347        self.set_frame(int(round(widget.value)))
2348
2349    def _handle_timer(self, evt=None) -> None:
2350        self.set_frame(self.value + 1)
2351
2352    def stop(self) -> "AnimationPlayer":
2353        """
2354        Stop the animation timers, remove buttons and slider.
2355        Behave like a normal `Plotter` after this.
2356        """
2357        # stop timer
2358        if self.timer_id is not None:
2359            self.timer_callback("destroy", self.timer_id)
2360            self.timer_id = None
2361
2362        # remove callbacks
2363        self.remove_callback(self.timer_callback_id)
2364
2365        # remove buttons
2366        self.slider.off()
2367        self.renderer.RemoveActor(self.play_pause_button.actor)
2368        self.renderer.RemoveActor(self.button_oneback.actor)
2369        self.renderer.RemoveActor(self.button_oneforward.actor)
2370        return self

A Plotter with play/pause, step forward/backward and slider functionalties. Useful for inspecting time series.

The user has the responsibility to update all actors in the callback function.

Arguments:
  • func : (Callable) a function that passes an integer as input and updates the scene
  • irange : (tuple) the range of the integer input representing the time series index
  • dt : (float) the time interval between two calls to func in milliseconds
  • loop : (bool) whether to loop the animation
  • c : (list, str) the color of the play/pause button
  • bc : (list) the background color of the play/pause button and the slider
  • button_size : (int) the size of the play/pause buttons
  • button_pos : (float, float) the position of the play/pause buttons as a fraction of the window size
  • button_gap : (float) the gap between the buttons
  • slider_length : (float) the length of the slider as a fraction of the window size
  • slider_pos : (float, float) the position of the slider as a fraction of the window size
  • kwargs: (dict) keyword arguments to be passed to Plotter
Examples:
AnimationPlayer( func, irange: tuple, dt: float = 1.0, loop: bool = True, c=('white', 'white'), bc=('green3', 'red4'), button_size=25, button_pos=(0.5, 0.04), button_gap=0.055, slider_length=0.5, slider_pos=(0.5, 0.055), **kwargs)
2220    def __init__(
2221        self,
2222        func,
2223        irange: tuple,
2224        dt: float = 1.0,
2225        loop: bool = True,
2226        c=("white", "white"),
2227        bc=("green3", "red4"),
2228        button_size=25,
2229        button_pos=(0.5, 0.04),
2230        button_gap=0.055,
2231        slider_length=0.5,
2232        slider_pos=(0.5, 0.055),
2233        **kwargs,
2234    ):
2235        super().__init__(**kwargs)
2236
2237        min_value, max_value = np.array(irange).astype(int)
2238        button_pos = np.array(button_pos)
2239        slider_pos = np.array(slider_pos)
2240
2241        self._func = func
2242
2243        self.value = min_value - 1
2244        self.min_value = min_value
2245        self.max_value = max_value
2246        self.dt = max(dt, 1)
2247        self.is_playing = False
2248        self._loop = loop
2249
2250        self.timer_callback_id = self.add_callback(
2251            "timer", self._handle_timer, enable_picking=False
2252        )
2253        self.timer_id = None
2254
2255        self.play_pause_button = self.add_button(
2256            self.toggle,
2257            pos=button_pos,  # x,y fraction from bottom left corner
2258            states=[self.PLAY_SYMBOL, self.PAUSE_SYMBOL],
2259            font="Kanopus",
2260            size=button_size,
2261            bc=bc,
2262        )
2263        self.button_oneback = self.add_button(
2264            self.onebackward,
2265            pos=(-button_gap, 0) + button_pos,
2266            states=[self.ONE_BACK_SYMBOL],
2267            font="Kanopus",
2268            size=button_size,
2269            c=c,
2270            bc=bc,
2271        )
2272        self.button_oneforward = self.add_button(
2273            self.oneforward,
2274            pos=(button_gap, 0) + button_pos,
2275            states=[self.ONE_FORWARD_SYMBOL],
2276            font="Kanopus",
2277            size=button_size,
2278            bc=bc,
2279        )
2280        d = (1 - slider_length) / 2
2281        self.slider: SliderWidget = self.add_slider(
2282            self._slider_callback,
2283            self.min_value,
2284            self.max_value - 1,
2285            value=self.min_value,
2286            pos=[(d - 0.5, 0) + slider_pos, (0.5 - d, 0) + slider_pos],
2287            show_value=False,
2288            c=bc[0],
2289            alpha=1,
2290        )
Arguments:
  • shape : (str, list) shape of the grid of renderers in format (rows, columns). Ignored if N is specified.
  • N : (int) number of desired renderers arranged in a grid automatically.
  • pos : (list) (x,y) position in pixels of top-left corner of the rendering window on the screen
  • size : (str, list) size of the rendering window. If 'auto', guess it based on screensize.
  • screensize : (list) physical size of the monitor screen in pixels
  • bg : (color, str) background color or specify jpg image file name with path
  • bg2 : (color) background color of a gradient towards the top
  • title : (str) window title
  • axes : (int)

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

    - 0,  no axes
    - 1,  draw three gray grid walls
    - 2,  show cartesian axes from (0,0,0)
    - 3,  show positive range of cartesian axes from (0,0,0)
    - 4,  show a triad at bottom left
    - 5,  show a cube at bottom left
    - 6,  mark the corners of the bounding box
    - 7,  draw a 3D ruler at each side of the cartesian axes
    - 8,  show the VTK CubeAxesActor object
    - 9,  show the bounding box outLine
    - 10, show three circles representing the maximum bounding box
    - 11, show a large grid on the x-y plane (use with zoom=8)
    - 12, show polar axes
    - 13, draw a simple ruler at the bottom of the window
    - 14: draw a camera orientation widget
    
  • sharecam : (bool) if False each renderer will have an independent camera
  • interactive : (bool) if True will stop after show() to allow interaction with the 3d scene
  • offscreen : (bool) if True will not show the rendering window
  • qt_widget : (QVTKRenderWindowInteractor) render in a Qt-Widget using an QVTKRenderWindowInteractor. See examples qt_windows[1,2,3].py and qt_cutter.py.
def pause(self) -> None:
2292    def pause(self) -> None:
2293        """Pause the animation."""
2294        self.is_playing = False
2295        if self.timer_id is not None:
2296            self.timer_callback("destroy", self.timer_id)
2297            self.timer_id = None
2298        self.play_pause_button.status(self.PLAY_SYMBOL)

Pause the animation.

def resume(self) -> None:
2300    def resume(self) -> None:
2301        """Resume the animation."""
2302        if self.timer_id is not None:
2303            self.timer_callback("destroy", self.timer_id)
2304        self.timer_id = self.timer_callback("create", dt=int(self.dt))
2305        self.is_playing = True
2306        self.play_pause_button.status(self.PAUSE_SYMBOL)

Resume the animation.

def toggle(self, _obj, _evt) -> None:
2308    def toggle(self, _obj, _evt) -> None:
2309        """Toggle between play and pause."""
2310        if not self.is_playing:
2311            self.resume()
2312        else:
2313            self.pause()

Toggle between play and pause.

def oneforward(self, _obj, _evt) -> None:
2315    def oneforward(self, _obj, _evt) -> None:
2316        """Advance the animation by one frame."""
2317        self.pause()
2318        self.set_frame(self.value + 1)

Advance the animation by one frame.

def onebackward(self, _obj, _evt) -> None:
2320    def onebackward(self, _obj, _evt) -> None:
2321        """Go back one frame in the animation."""
2322        self.pause()
2323        self.set_frame(self.value - 1)

Go back one frame in the animation.

def set_frame(self, value: int) -> None:
2325    def set_frame(self, value: int) -> None:
2326        """Set the current value of the animation."""
2327        if self._loop:
2328            if value < self.min_value:
2329                value = self.max_value - 1
2330            elif value >= self.max_value:
2331                value = self.min_value
2332        else:
2333            if value < self.min_value:
2334                self.pause()
2335                value = self.min_value
2336            elif value >= self.max_value - 1:
2337                value = self.max_value - 1
2338                self.pause()
2339
2340        if self.value != value:
2341            self.value = value
2342            self.slider.value = value
2343            self._func(value)

Set the current value of the animation.

def stop(self) -> AnimationPlayer:
2352    def stop(self) -> "AnimationPlayer":
2353        """
2354        Stop the animation timers, remove buttons and slider.
2355        Behave like a normal `Plotter` after this.
2356        """
2357        # stop timer
2358        if self.timer_id is not None:
2359            self.timer_callback("destroy", self.timer_id)
2360            self.timer_id = None
2361
2362        # remove callbacks
2363        self.remove_callback(self.timer_callback_id)
2364
2365        # remove buttons
2366        self.slider.off()
2367        self.renderer.RemoveActor(self.play_pause_button.actor)
2368        self.renderer.RemoveActor(self.button_oneback.actor)
2369        self.renderer.RemoveActor(self.button_oneforward.actor)
2370        return self

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