vedo.applications

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

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

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

Browser( objects=(), sliderpos=((0.5, 0.07), (0.95, 0.07)), c=None, slider_title='', font='Calco', resetcam=False, **kwargs)
1253    def __init__(
1254        self,
1255        objects=(),
1256        sliderpos=((0.50, 0.07), (0.95, 0.07)),
1257        c=None,  # slider color
1258        slider_title="",
1259        font="Calco",  # slider font
1260        resetcam=False,  # resetcam while using the slider
1261        **kwargs,
1262    ):
1263        """
1264        Browse a series of vedo objects by using a simple slider.
1265
1266        The input object can be a list of objects or a list of lists of objects.
1267
1268        Arguments:
1269            objects : (list)
1270                list of objects to be browsed.
1271            sliderpos : (list)
1272                position of the slider.
1273            c : (str)
1274                color of the slider.
1275            slider_title : (str)
1276                title of the slider.
1277            font : (str)
1278                font of the slider.
1279            resetcam : (bool)
1280                resetcam while using the slider.
1281            **kwargs : (dict)
1282                keyword arguments to pass to Plotter.
1283
1284        Examples:
1285            ```python
1286            from vedo import load, dataurl
1287            from vedo.applications import Browser
1288            meshes = load(dataurl+'timecourse1d.npy') # python list of Meshes
1289            plt = Browser(meshes, bg='k')             # vedo.Plotter
1290            plt.show(interactive=False, zoom='tight') # show the meshes
1291            plt.play(dt=50)                           # delay in milliseconds
1292            plt.close()
1293            ```
1294
1295        - [morphomatics_tube.py](https://github.com/marcomusy/vedo/tree/master/examples/other/morphomatics_tube.py)
1296        """
1297        kwargs.pop("N", 1)
1298        kwargs.pop("shape", [])
1299        kwargs.pop("axes", 1)
1300        super().__init__(**kwargs)
1301
1302        if isinstance(objects, str):
1303            objects = vedo.file_io.load(objects)
1304
1305        self += objects
1306
1307        if is_sequence(objects[0]):
1308            nobs = len(objects[0])
1309            for ob in objects:
1310                n = len(ob)
1311                msg = f"in Browser lists must have the same length but found {n} and {nobs}"
1312                assert len(ob) == nobs, msg
1313        else:
1314            nobs = len(objects)
1315            objects = [objects]
1316
1317        self.slider = None
1318        self.timer_callback_id = None
1319        self._oldk = None
1320
1321        # define the slider func ##########################
1322        def slider_function(widget=None, event=None):
1323
1324            k = int(self.slider.value)
1325
1326            if k == self._oldk:
1327                return  # no change
1328            self._oldk = k
1329
1330            n = len(objects)
1331            m = len(objects[0])
1332            for i in range(n):
1333                for j in range(m):
1334                    ak = objects[i][j]
1335                    try:
1336                        if j == k:
1337                            ak.on()
1338                            akon = ak
1339                        else:
1340                            ak.off()
1341                    except AttributeError:
1342                        pass
1343
1344            try:
1345                tx = str(k)
1346                if slider_title:
1347                    tx = slider_title + " " + tx
1348                elif n == 1 and akon.filename:
1349                    tx = akon.filename.split("/")[-1]
1350                    tx = tx.split("\\")[-1]  # windows os
1351                elif akon.name:
1352                    tx = ak.name + " " + tx
1353            except:
1354                pass
1355            self.slider.title = tx
1356
1357            if resetcam:
1358                self.reset_camera()
1359            self.render()
1360
1361        ##################################################
1362
1363        self.slider_function = slider_function
1364        self.slider = self.add_slider(
1365            slider_function,
1366            0.5,
1367            nobs - 0.5,
1368            pos=sliderpos,
1369            font=font,
1370            c=c,
1371            show_value=False,
1372        )
1373        self.slider.GetRepresentation().SetTitleHeight(0.020)
1374        slider_function()  # init call

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

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

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

Start playing the slides at a given speed.

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

Generate a Volume isosurfacing controlled by a slider.

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

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

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

A tool to edit meshes interactively.

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

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

Can also be invoked from command line with:

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

Set an initial number of points to define a region

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

Save the resulting mesh to file

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

Start window interaction (with mouse and keyboard)

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

Generate Volume rendering using ray casting.

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

Generate 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
3675    @property
3676    def camera(self):
3677        """Return the current active camera."""
3678        if self.renderer:
3679            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):
1631class SplinePlotter(Plotter):
1632    """
1633    Interactive drawing of splined curves on meshes.
1634    """
1635
1636    def __init__(self, obj, init_points=(), closed=False, splined=True, **kwargs):
1637        """
1638        Create an interactive application that allows the user to click points and
1639        retrieve the coordinates of such points and optionally a spline or line
1640        (open or closed).
1641        Input object can be a image file name or a 3D mesh.
1642
1643        Arguments:
1644            obj : (Mesh, str)
1645                The input object can be a image file name or a 3D mesh.
1646            init_points : (list)
1647                Set an initial number of points to define a region.
1648            closed : (bool)
1649                Close the spline or line.
1650            splined : (bool)
1651                Join points with a spline or a simple line.
1652            **kwargs : (dict)
1653                keyword arguments to pass to Plotter.
1654        """
1655        super().__init__(**kwargs)
1656
1657        self.mode = "trackball"
1658        self.verbose = True
1659        self.splined = splined
1660        self.resolution = None  # spline resolution (None = automatic)
1661        self.closed = closed
1662        self.lcolor = "yellow4"
1663        self.lwidth = 3
1664        self.pcolor = "purple5"
1665        self.psize = 10
1666
1667        self.cpoints = list(init_points)
1668        self.vpoints = None
1669        self.line = None
1670
1671        if isinstance(obj, str):
1672            self.object = vedo.file_io.load(obj)
1673        else:
1674            self.object = obj
1675
1676        if isinstance(self.object, vedo.Image):
1677            self.mode = "image"
1678            self.parallel_projection(True)
1679
1680        t = (
1681            "Click to add a point\n"
1682            "Right-click to remove it\n"
1683            "Drag mouse to change contrast\n"
1684            "Press c to clear points\n"
1685            "Press q to continue"
1686        )
1687        self.instructions = Text2D(t, pos="bottom-left", c="white", bg="green", font="Calco")
1688
1689        self += [self.object, self.instructions]
1690
1691        self.callid1 = self.add_callback("KeyPress", self._key_press)
1692        self.callid2 = self.add_callback("LeftButtonPress", self._on_left_click)
1693        self.callid3 = self.add_callback("RightButtonPress", self._on_right_click)
1694
1695    def points(self, newpts=None) -> Union["SplinePlotter", np.ndarray]:
1696        """Retrieve the 3D coordinates of the clicked points"""
1697        if newpts is not None:
1698            self.cpoints = newpts
1699            self._update()
1700            return self
1701        return np.array(self.cpoints)
1702
1703    def _on_left_click(self, evt):
1704        if not evt.actor:
1705            return
1706        if evt.actor.name == "points":
1707            # remove clicked point if clicked twice
1708            pid = self.vpoints.closest_point(evt.picked3d, return_point_id=True)
1709            self.cpoints.pop(pid)
1710            self._update()
1711            return
1712        p = evt.picked3d
1713        self.cpoints.append(p)
1714        self._update()
1715        if self.verbose:
1716            vedo.colors.printc("Added point:", precision(p, 4), c="g")
1717
1718    def _on_right_click(self, evt):
1719        if evt.actor and len(self.cpoints) > 0:
1720            self.cpoints.pop()  # pop removes from the list the last pt
1721            self._update()
1722            if self.verbose:
1723                vedo.colors.printc("Deleted last point", c="r")
1724
1725    def _update(self):
1726        self.remove(self.line, self.vpoints)  # remove old points and spline
1727        self.vpoints = Points(self.cpoints).ps(self.psize).c(self.pcolor)
1728        self.vpoints.name = "points"
1729        self.vpoints.pickable(True)  # to allow toggle
1730        minnr = 1
1731        if self.splined:
1732            minnr = 2
1733        if self.lwidth and len(self.cpoints) > minnr:
1734            if self.splined:
1735                try:
1736                    self.line = Spline(self.cpoints, closed=self.closed, res=self.resolution)
1737                except ValueError:
1738                    # if clicking too close splining might fail
1739                    self.cpoints.pop()
1740                    return
1741            else:
1742                self.line = Line(self.cpoints, closed=self.closed)
1743            self.line.c(self.lcolor).lw(self.lwidth).pickable(False)
1744            self.add(self.vpoints, self.line)
1745        else:
1746            self.add(self.vpoints)
1747
1748    def _key_press(self, evt):
1749        if evt.keypress == "c":
1750            self.cpoints = []
1751            self.remove(self.line, self.vpoints).render()
1752            if self.verbose:
1753                vedo.colors.printc("==== Cleared all points ====", c="r", invert=True)
1754
1755    def start(self) -> "SplinePlotter":
1756        """Start the interaction"""
1757        self.show(self.object, self.instructions, mode=self.mode)
1758        return self

Interactive drawing of splined curves on meshes.

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

Retrieve the 3D coordinates of the clicked points

def start(self) -> SplinePlotter:
1755    def start(self) -> "SplinePlotter":
1756        """Start the interaction"""
1757        self.show(self.object, self.instructions, mode=self.mode)
1758        return self

Start the interaction

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

Pause the animation.

def resume(self) -> None:
2289    def resume(self) -> None:
2290        """Resume the animation."""
2291        if self.timer_id is not None:
2292            self.timer_callback("destroy", self.timer_id)
2293        self.timer_id = self.timer_callback("create", dt=int(self.dt))
2294        self.is_playing = True
2295        self.play_pause_button.status(self.PAUSE_SYMBOL)

Resume the animation.

def toggle(self, _obj, _evt) -> None:
2297    def toggle(self, _obj, _evt) -> None:
2298        """Toggle between play and pause."""
2299        if not self.is_playing:
2300            self.resume()
2301        else:
2302            self.pause()

Toggle between play and pause.

def oneforward(self, _obj, _evt) -> None:
2304    def oneforward(self, _obj, _evt) -> None:
2305        """Advance the animation by one frame."""
2306        self.pause()
2307        self.set_frame(self.value + 1)

Advance the animation by one frame.

def onebackward(self, _obj, _evt) -> None:
2309    def onebackward(self, _obj, _evt) -> None:
2310        """Go back one frame in the animation."""
2311        self.pause()
2312        self.set_frame(self.value - 1)

Go back one frame in the animation.

def set_frame(self, value: int) -> None:
2314    def set_frame(self, value: int) -> None:
2315        """Set the current value of the animation."""
2316        if self._loop:
2317            if value < self.min_value:
2318                value = self.max_value - 1
2319            elif value >= self.max_value:
2320                value = self.min_value
2321        else:
2322            if value < self.min_value:
2323                self.pause()
2324                value = self.min_value
2325            elif value >= self.max_value - 1:
2326                value = self.max_value - 1
2327                self.pause()
2328
2329        if self.value != value:
2330            self.value = value
2331            self.slider.value = value
2332            self._func(value)

Set the current value of the animation.

def stop(self) -> AnimationPlayer:
2341    def stop(self) -> "AnimationPlayer":
2342        """
2343        Stop the animation timers, remove buttons and slider.
2344        Behave like a normal `Plotter` after this.
2345        """
2346        # stop timer
2347        if self.timer_id is not None:
2348            self.timer_callback("destroy", self.timer_id)
2349            self.timer_id = None
2350
2351        # remove callbacks
2352        self.remove_callback(self.timer_callback_id)
2353
2354        # remove buttons
2355        self.slider.off()
2356        self.renderer.RemoveActor(self.play_pause_button.actor)
2357        self.renderer.RemoveActor(self.button_oneback.actor)
2358        self.renderer.RemoveActor(self.button_oneforward.actor)
2359        return self

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