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        def setOTF():
 968            otf = self.properties.GetScalarOpacity()
 969            otf.RemoveAllPoints()
 970            otf.AddPoint(smin, 0.0)
 971            otf.AddPoint(smin + (smax - smin) * 0.1, 0.0)
 972            otf.AddPoint(x0alpha, self.alphaslider0)
 973            otf.AddPoint(x1alpha, self.alphaslider1)
 974            otf.AddPoint(x2alpha, self.alphaslider2)
 975            slider_cmap()
 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 = []
1480        self.points = None
1481        self.spline = None
1482        self.jline = None
1483        self.topline = None
1484        self.top_pts = []
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, mode="auto", **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            mode : (str)
1652                Set the mode of interaction.
1653            **kwargs : (dict)
1654                keyword arguments to pass to Plotter.
1655        """
1656        super().__init__(**kwargs)
1657
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        self.mode = mode
1677        if self.mode == "auto":
1678            if isinstance(self.object, vedo.Image):
1679                self.mode = "image"
1680                self.parallel_projection(True)
1681            else:
1682                self.mode = "TrackballCamera"
1683
1684        t = (
1685            "Click to add a point\n"
1686            "Right-click to remove it\n"
1687            "Drag mouse to change contrast\n"
1688            "Press c to clear points\n"
1689            "Press q to continue"
1690        )
1691        self.instructions = Text2D(t, pos="bottom-left", c="white", bg="green", font="Calco")
1692
1693        self += [self.object, self.instructions]
1694
1695        self.callid1 = self.add_callback("KeyPress", self._key_press)
1696        self.callid2 = self.add_callback("LeftButtonPress", self._on_left_click)
1697        self.callid3 = self.add_callback("RightButtonPress", self._on_right_click)
1698
1699
1700    def points(self, newpts=None) -> Union["SplinePlotter", np.ndarray]:
1701        """Retrieve the 3D coordinates of the clicked points"""
1702        if newpts is not None:
1703            self.cpoints = newpts
1704            self.update()
1705            return self
1706        return np.array(self.cpoints)
1707
1708    def _on_left_click(self, evt):
1709        if not evt.actor:
1710            return
1711        if evt.actor.name == "points":
1712            # remove clicked point if clicked twice
1713            pid = self.vpoints.closest_point(evt.picked3d, return_point_id=True)
1714            self.cpoints.pop(pid)
1715            self.update()
1716            return
1717        p = evt.picked3d
1718        self.cpoints.append(p)
1719        self.update()
1720        if self.verbose:
1721            vedo.colors.printc("Added point:", precision(p, 4), c="g")
1722
1723    def _on_right_click(self, evt):
1724        if evt.actor and len(self.cpoints) > 0:
1725            self.cpoints.pop()  # pop removes from the list the last pt
1726            self.update()
1727            if self.verbose:
1728                vedo.colors.printc("Deleted last point", c="r")
1729
1730    def update(self):
1731        self.remove(self.line, self.vpoints)  # remove old points and spline
1732        self.vpoints = Points(self.cpoints).ps(self.psize).c(self.pcolor)
1733        self.vpoints.name = "points"
1734        self.vpoints.pickable(True)  # to allow toggle
1735        minnr = 1
1736        if self.splined:
1737            minnr = 2
1738        if self.lwidth and len(self.cpoints) > minnr:
1739            if self.splined:
1740                try:
1741                    self.line = Spline(self.cpoints, closed=self.closed, res=self.resolution)
1742                except ValueError:
1743                    # if clicking too close splining might fail
1744                    self.cpoints.pop()
1745                    return
1746            else:
1747                self.line = Line(self.cpoints, closed=self.closed)
1748            self.line.c(self.lcolor).lw(self.lwidth).pickable(False)
1749            self.add(self.vpoints, self.line)
1750        else:
1751            self.add(self.vpoints)
1752
1753    def _key_press(self, evt):
1754        if evt.keypress == "c":
1755            self.cpoints = []
1756            self.remove(self.line, self.vpoints).render()
1757            if self.verbose:
1758                vedo.colors.printc("==== Cleared all points ====", c="r", invert=True)
1759
1760    def start(self) -> "SplinePlotter":
1761        """Start the interaction"""
1762        self.update()
1763        self.show(self.object, self.instructions, mode=self.mode)
1764        return self
1765
1766
1767########################################################################
1768class Animation(Plotter):
1769    """
1770    A `Plotter` derived class that allows to animate simultaneously various objects
1771    by specifying event times and durations of different visual effects.
1772
1773    Arguments:
1774        total_duration : (float)
1775            expand or shrink the total duration of video to this value
1776        time_resolution : (float)
1777            in seconds, save a frame at this rate
1778        show_progressbar : (bool)
1779            whether to show a progress bar or not
1780        video_filename : (str)
1781            output file name of the video
1782        video_fps : (int)
1783            desired value of the nr of frames per second
1784
1785    .. warning:: this is still very experimental at the moment.
1786    """
1787
1788    def __init__(
1789        self,
1790        total_duration=None,
1791        time_resolution=0.02,
1792        show_progressbar=True,
1793        video_filename="animation.mp4",
1794        video_fps=12,
1795    ):
1796        super().__init__()
1797        self.resetcam = True
1798
1799        self.events = []
1800        self.time_resolution = time_resolution
1801        self.total_duration = total_duration
1802        self.show_progressbar = show_progressbar
1803        self.video_filename = video_filename
1804        self.video_fps = video_fps
1805        self.bookingMode = True
1806        self._inputvalues = []
1807        self._performers = []
1808        self._lastT = None
1809        self._lastDuration = None
1810        self._lastActs = None
1811        self.eps = 0.00001
1812
1813    def _parse(self, objs, t, duration):
1814        if t is None:
1815            if self._lastT:
1816                t = self._lastT
1817            else:
1818                t = 0.0
1819        if duration is None:
1820            if self._lastDuration:
1821                duration = self._lastDuration
1822            else:
1823                duration = 0.0
1824        if objs is None:
1825            if self._lastActs:
1826                objs = self._lastActs
1827            else:
1828                vedo.logger.error("Need to specify actors!")
1829                raise RuntimeError
1830
1831        objs2 = objs
1832
1833        if is_sequence(objs):
1834            objs2 = objs
1835        else:
1836            objs2 = [objs]
1837
1838        # quantize time steps and duration
1839        t = int(t / self.time_resolution + 0.5) * self.time_resolution
1840        nsteps = int(duration / self.time_resolution + 0.5)
1841        duration = nsteps * self.time_resolution
1842
1843        rng = np.linspace(t, t + duration, nsteps + 1)
1844
1845        self._lastT = t
1846        self._lastDuration = duration
1847        self._lastActs = objs2
1848
1849        for a in objs2:
1850            if a not in self.objects:
1851                self.objects.append(a)
1852
1853        return objs2, t, duration, rng
1854
1855    def switch_on(self, acts=None, t=None):
1856        """Switch on the input list of meshes."""
1857        return self.fade_in(acts, t, 0)
1858
1859    def switch_off(self, acts=None, t=None):
1860        """Switch off the input list of meshes."""
1861        return self.fade_out(acts, t, 0)
1862
1863    def fade_in(self, acts=None, t=None, duration=None):
1864        """Gradually switch on the input list of meshes by increasing opacity."""
1865        if self.bookingMode:
1866            acts, t, duration, rng = self._parse(acts, t, duration)
1867            for tt in rng:
1868                alpha = lin_interpolate(tt, [t, t + duration], [0, 1])
1869                self.events.append((tt, self.fade_in, acts, alpha))
1870        else:
1871            for a in self._performers:
1872                if hasattr(a, "alpha"):
1873                    if a.alpha() >= self._inputvalues:
1874                        continue
1875                    a.alpha(self._inputvalues)
1876        return self
1877
1878    def fade_out(self, acts=None, t=None, duration=None):
1879        """Gradually switch off the input list of meshes by increasing transparency."""
1880        if self.bookingMode:
1881            acts, t, duration, rng = self._parse(acts, t, duration)
1882            for tt in rng:
1883                alpha = lin_interpolate(tt, [t, t + duration], [1, 0])
1884                self.events.append((tt, self.fade_out, acts, alpha))
1885        else:
1886            for a in self._performers:
1887                if a.alpha() <= self._inputvalues:
1888                    continue
1889                a.alpha(self._inputvalues)
1890        return self
1891
1892    def change_alpha_between(self, alpha1, alpha2, acts=None, t=None, duration=None):
1893        """Gradually change transparency for the input list of meshes."""
1894        if self.bookingMode:
1895            acts, t, duration, rng = self._parse(acts, t, duration)
1896            for tt in rng:
1897                alpha = lin_interpolate(tt, [t, t + duration], [alpha1, alpha2])
1898                self.events.append((tt, self.fade_out, acts, alpha))
1899        else:
1900            for a in self._performers:
1901                a.alpha(self._inputvalues)
1902        return self
1903
1904    def change_color(self, c, acts=None, t=None, duration=None):
1905        """Gradually change color for the input list of meshes."""
1906        if self.bookingMode:
1907            acts, t, duration, rng = self._parse(acts, t, duration)
1908
1909            col2 = get_color(c)
1910            for tt in rng:
1911                inputvalues = []
1912                for a in acts:
1913                    col1 = a.color()
1914                    r = lin_interpolate(tt, [t, t + duration], [col1[0], col2[0]])
1915                    g = lin_interpolate(tt, [t, t + duration], [col1[1], col2[1]])
1916                    b = lin_interpolate(tt, [t, t + duration], [col1[2], col2[2]])
1917                    inputvalues.append((r, g, b))
1918                self.events.append((tt, self.change_color, acts, inputvalues))
1919        else:
1920            for i, a in enumerate(self._performers):
1921                a.color(self._inputvalues[i])
1922        return self
1923
1924    def change_backcolor(self, c, acts=None, t=None, duration=None):
1925        """Gradually change backface color for the input list of meshes.
1926        An initial backface color should be set in advance."""
1927        if self.bookingMode:
1928            acts, t, duration, rng = self._parse(acts, t, duration)
1929
1930            col2 = get_color(c)
1931            for tt in rng:
1932                inputvalues = []
1933                for a in acts:
1934                    if a.GetBackfaceProperty():
1935                        col1 = a.backColor()
1936                        r = lin_interpolate(tt, [t, t + duration], [col1[0], col2[0]])
1937                        g = lin_interpolate(tt, [t, t + duration], [col1[1], col2[1]])
1938                        b = lin_interpolate(tt, [t, t + duration], [col1[2], col2[2]])
1939                        inputvalues.append((r, g, b))
1940                    else:
1941                        inputvalues.append(None)
1942                self.events.append((tt, self.change_backcolor, acts, inputvalues))
1943        else:
1944            for i, a in enumerate(self._performers):
1945                a.backColor(self._inputvalues[i])
1946        return self
1947
1948    def change_to_wireframe(self, acts=None, t=None):
1949        """Switch representation to wireframe for the input list of meshes at time `t`."""
1950        if self.bookingMode:
1951            acts, t, _, _ = self._parse(acts, t, None)
1952            self.events.append((t, self.change_to_wireframe, acts, True))
1953        else:
1954            for a in self._performers:
1955                a.wireframe(self._inputvalues)
1956        return self
1957
1958    def change_to_surface(self, acts=None, t=None):
1959        """Switch representation to surface for the input list of meshes at time `t`."""
1960        if self.bookingMode:
1961            acts, t, _, _ = self._parse(acts, t, None)
1962            self.events.append((t, self.change_to_surface, acts, False))
1963        else:
1964            for a in self._performers:
1965                a.wireframe(self._inputvalues)
1966        return self
1967
1968    def change_line_width(self, lw, acts=None, t=None, duration=None):
1969        """Gradually change line width of the mesh edges for the input list of meshes."""
1970        if self.bookingMode:
1971            acts, t, duration, rng = self._parse(acts, t, duration)
1972            for tt in rng:
1973                inputvalues = []
1974                for a in acts:
1975                    newlw = lin_interpolate(tt, [t, t + duration], [a.lw(), lw])
1976                    inputvalues.append(newlw)
1977                self.events.append((tt, self.change_line_width, acts, inputvalues))
1978        else:
1979            for i, a in enumerate(self._performers):
1980                a.lw(self._inputvalues[i])
1981        return self
1982
1983    def change_line_color(self, c, acts=None, t=None, duration=None):
1984        """Gradually change line color of the mesh edges for the input list of meshes."""
1985        if self.bookingMode:
1986            acts, t, duration, rng = self._parse(acts, t, duration)
1987            col2 = get_color(c)
1988            for tt in rng:
1989                inputvalues = []
1990                for a in acts:
1991                    col1 = a.linecolor()
1992                    r = lin_interpolate(tt, [t, t + duration], [col1[0], col2[0]])
1993                    g = lin_interpolate(tt, [t, t + duration], [col1[1], col2[1]])
1994                    b = lin_interpolate(tt, [t, t + duration], [col1[2], col2[2]])
1995                    inputvalues.append((r, g, b))
1996                self.events.append((tt, self.change_line_color, acts, inputvalues))
1997        else:
1998            for i, a in enumerate(self._performers):
1999                a.linecolor(self._inputvalues[i])
2000        return self
2001
2002    def change_lighting(self, style, acts=None, t=None, duration=None):
2003        """Gradually change the lighting style for the input list of meshes.
2004
2005        Allowed styles are: [metallic, plastic, shiny, glossy, default].
2006        """
2007        if self.bookingMode:
2008            acts, t, duration, rng = self._parse(acts, t, duration)
2009
2010            c = (1,1,0.99)
2011            if   style=='metallic': pars = [0.1, 0.3, 1.0, 10, c]
2012            elif style=='plastic' : pars = [0.3, 0.4, 0.3,  5, c]
2013            elif style=='shiny'   : pars = [0.2, 0.6, 0.8, 50, c]
2014            elif style=='glossy'  : pars = [0.1, 0.7, 0.9, 90, c]
2015            elif style=='default' : pars = [0.1, 1.0, 0.05, 5, c]
2016            else:
2017                vedo.logger.error(f"Unknown lighting style {style}")
2018
2019            for tt in rng:
2020                inputvalues = []
2021                for a in acts:
2022                    pr = a.properties
2023                    aa = pr.GetAmbient()
2024                    ad = pr.GetDiffuse()
2025                    asp = pr.GetSpecular()
2026                    aspp = pr.GetSpecularPower()
2027                    naa = lin_interpolate(tt, [t, t + duration], [aa, pars[0]])
2028                    nad = lin_interpolate(tt, [t, t + duration], [ad, pars[1]])
2029                    nasp = lin_interpolate(tt, [t, t + duration], [asp, pars[2]])
2030                    naspp = lin_interpolate(tt, [t, t + duration], [aspp, pars[3]])
2031                    inputvalues.append((naa, nad, nasp, naspp))
2032                self.events.append((tt, self.change_lighting, acts, inputvalues))
2033        else:
2034            for i, a in enumerate(self._performers):
2035                pr = a.properties
2036                vals = self._inputvalues[i]
2037                pr.SetAmbient(vals[0])
2038                pr.SetDiffuse(vals[1])
2039                pr.SetSpecular(vals[2])
2040                pr.SetSpecularPower(vals[3])
2041        return self
2042
2043    def move(self, act=None, pt=(0, 0, 0), t=None, duration=None, style="linear"):
2044        """Smoothly change the position of a specific object to a new point in space."""
2045        if self.bookingMode:
2046            acts, t, duration, rng = self._parse(act, t, duration)
2047            if len(acts) != 1:
2048                vedo.logger.error("in move(), can move only one object.")
2049            cpos = acts[0].pos()
2050            pt = np.array(pt)
2051            dv = (pt - cpos) / len(rng)
2052            for j, tt in enumerate(rng):
2053                i = j + 1
2054                if "quad" in style:
2055                    x = i / len(rng)
2056                    y = x * x
2057                    self.events.append((tt, self.move, acts, cpos + dv * i * y))
2058                else:
2059                    self.events.append((tt, self.move, acts, cpos + dv * i))
2060        else:
2061            self._performers[0].pos(self._inputvalues)
2062        return self
2063
2064    def rotate(self, act=None, axis=(1, 0, 0), angle=0, t=None, duration=None):
2065        """Smoothly rotate a specific object by a specified angle and axis."""
2066        if self.bookingMode:
2067            acts, t, duration, rng = self._parse(act, t, duration)
2068            if len(acts) != 1:
2069                vedo.logger.error("in rotate(), can move only one object.")
2070            for tt in rng:
2071                ang = angle / len(rng)
2072                self.events.append((tt, self.rotate, acts, (axis, ang)))
2073        else:
2074            ax = self._inputvalues[0]
2075            if ax == "x":
2076                self._performers[0].rotate_x(self._inputvalues[1])
2077            elif ax == "y":
2078                self._performers[0].rotate_y(self._inputvalues[1])
2079            elif ax == "z":
2080                self._performers[0].rotate_z(self._inputvalues[1])
2081        return self
2082
2083    def scale(self, acts=None, factor=1, t=None, duration=None):
2084        """Smoothly scale a specific object to a specified scale factor."""
2085        if self.bookingMode:
2086            acts, t, duration, rng = self._parse(acts, t, duration)
2087            for tt in rng:
2088                fac = lin_interpolate(tt, [t, t + duration], [1, factor])
2089                self.events.append((tt, self.scale, acts, fac))
2090        else:
2091            for a in self._performers:
2092                a.scale(self._inputvalues)
2093        return self
2094
2095    def mesh_erode(self, act=None, corner=6, t=None, duration=None):
2096        """Erode a mesh by removing cells that are close to one of the 8 corners
2097        of the bounding box.
2098        """
2099        if self.bookingMode:
2100            acts, t, duration, rng = self._parse(act, t, duration)
2101            if len(acts) != 1:
2102                vedo.logger.error("in meshErode(), can erode only one object.")
2103            diag = acts[0].diagonal_size()
2104            x0, x1, y0, y1, z0, z1 = acts[0].GetBounds()
2105            corners = [
2106                (x0, y0, z0),
2107                (x1, y0, z0),
2108                (x1, y1, z0),
2109                (x0, y1, z0),
2110                (x0, y0, z1),
2111                (x1, y0, z1),
2112                (x1, y1, z1),
2113                (x0, y1, z1),
2114            ]
2115            pcl = acts[0].closest_point(corners[corner])
2116            dmin = np.linalg.norm(pcl - corners[corner])
2117            for tt in rng:
2118                d = lin_interpolate(tt, [t, t + duration], [dmin, diag * 1.01])
2119                if d > 0:
2120                    ids = acts[0].closest_point(corners[corner], radius=d, return_point_id=True)
2121                    if len(ids) <= acts[0].npoints:
2122                        self.events.append((tt, self.mesh_erode, acts, ids))
2123        return self
2124
2125    def play(self):
2126        """Play the internal list of events and save a video."""
2127
2128        self.events = sorted(self.events, key=lambda x: x[0])
2129        self.bookingMode = False
2130
2131        if self.show_progressbar:
2132            pb = vedo.ProgressBar(0, len(self.events), c="g")
2133
2134        if self.total_duration is None:
2135            self.total_duration = self.events[-1][0] - self.events[0][0]
2136
2137        if self.video_filename:
2138            vd = vedo.Video(self.video_filename, fps=self.video_fps, duration=self.total_duration)
2139
2140        ttlast = 0
2141        for e in self.events:
2142
2143            tt, action, self._performers, self._inputvalues = e
2144            action(0, 0)
2145
2146            dt = tt - ttlast
2147            if dt > self.eps:
2148                self.show(interactive=False, resetcam=self.resetcam)
2149                if self.video_filename:
2150                    vd.add_frame()
2151
2152                if dt > self.time_resolution + self.eps:
2153                    if self.video_filename:
2154                        vd.pause(dt)
2155
2156            ttlast = tt
2157
2158            if self.show_progressbar:
2159                pb.print("t=" + str(int(tt * 100) / 100) + "s,  " + action.__name__)
2160
2161        self.show(interactive=False, resetcam=self.resetcam)
2162        if self.video_filename:
2163            vd.add_frame()
2164            vd.close()
2165
2166        self.show(interactive=True, resetcam=self.resetcam)
2167        self.bookingMode = True
2168
2169
2170########################################################################
2171class AnimationPlayer(vedo.Plotter):
2172    """
2173    A Plotter with play/pause, step forward/backward and slider functionalties.
2174    Useful for inspecting time series.
2175
2176    The user has the responsibility to update all actors in the callback function.
2177
2178    Arguments:
2179        func :  (Callable)
2180            a function that passes an integer as input and updates the scene
2181        irange : (tuple)
2182            the range of the integer input representing the time series index
2183        dt : (float)
2184            the time interval between two calls to `func` in milliseconds
2185        loop : (bool)
2186            whether to loop the animation
2187        c : (list, str)
2188            the color of the play/pause button
2189        bc : (list)
2190            the background color of the play/pause button and the slider
2191        button_size : (int)
2192            the size of the play/pause buttons
2193        button_pos : (float, float)
2194            the position of the play/pause buttons as a fraction of the window size
2195        button_gap : (float)
2196            the gap between the buttons
2197        slider_length : (float)
2198            the length of the slider as a fraction of the window size
2199        slider_pos : (float, float)
2200            the position of the slider as a fraction of the window size
2201        kwargs: (dict)
2202            keyword arguments to be passed to `Plotter`
2203
2204    Examples:
2205        - [aspring2_player.py](https://vedo.embl.es/images/simulations/spring_player.gif)
2206    """
2207
2208    # Original class contributed by @mikaeltulldahl (Mikael Tulldahl)
2209
2210    PLAY_SYMBOL        = "    \u23F5   "
2211    PAUSE_SYMBOL       = "   \u23F8   "
2212    ONE_BACK_SYMBOL    = " \u29CF"
2213    ONE_FORWARD_SYMBOL = "\u29D0 "
2214
2215    def __init__(
2216        self,
2217        func,
2218        irange: tuple,
2219        dt: float = 1.0,
2220        loop: bool = True,
2221        c=("white", "white"),
2222        bc=("green3", "red4"),
2223        button_size=25,
2224        button_pos=(0.5, 0.04),
2225        button_gap=0.055,
2226        slider_length=0.5,
2227        slider_pos=(0.5, 0.055),
2228        **kwargs,
2229    ):
2230        super().__init__(**kwargs)
2231
2232        min_value, max_value = np.array(irange).astype(int)
2233        button_pos = np.array(button_pos)
2234        slider_pos = np.array(slider_pos)
2235
2236        self._func = func
2237
2238        self.value = min_value - 1
2239        self.min_value = min_value
2240        self.max_value = max_value
2241        self.dt = max(dt, 1)
2242        self.is_playing = False
2243        self._loop = loop
2244
2245        self.timer_callback_id = self.add_callback(
2246            "timer", self._handle_timer, enable_picking=False
2247        )
2248        self.timer_id = None
2249
2250        self.play_pause_button = self.add_button(
2251            self.toggle,
2252            pos=button_pos,  # x,y fraction from bottom left corner
2253            states=[self.PLAY_SYMBOL, self.PAUSE_SYMBOL],
2254            font="Kanopus",
2255            size=button_size,
2256            bc=bc,
2257        )
2258        self.button_oneback = self.add_button(
2259            self.onebackward,
2260            pos=(-button_gap, 0) + button_pos,
2261            states=[self.ONE_BACK_SYMBOL],
2262            font="Kanopus",
2263            size=button_size,
2264            c=c,
2265            bc=bc,
2266        )
2267        self.button_oneforward = self.add_button(
2268            self.oneforward,
2269            pos=(button_gap, 0) + button_pos,
2270            states=[self.ONE_FORWARD_SYMBOL],
2271            font="Kanopus",
2272            size=button_size,
2273            bc=bc,
2274        )
2275        d = (1 - slider_length) / 2
2276        self.slider: SliderWidget = self.add_slider(
2277            self._slider_callback,
2278            self.min_value,
2279            self.max_value - 1,
2280            value=self.min_value,
2281            pos=[(d - 0.5, 0) + slider_pos, (0.5 - d, 0) + slider_pos],
2282            show_value=False,
2283            c=bc[0],
2284            alpha=1,
2285        )
2286
2287    def pause(self) -> None:
2288        """Pause the animation."""
2289        self.is_playing = False
2290        if self.timer_id is not None:
2291            self.timer_callback("destroy", self.timer_id)
2292            self.timer_id = None
2293        self.play_pause_button.status(self.PLAY_SYMBOL)
2294
2295    def resume(self) -> None:
2296        """Resume the animation."""
2297        if self.timer_id is not None:
2298            self.timer_callback("destroy", self.timer_id)
2299        self.timer_id = self.timer_callback("create", dt=int(self.dt))
2300        self.is_playing = True
2301        self.play_pause_button.status(self.PAUSE_SYMBOL)
2302
2303    def toggle(self, _obj, _evt) -> None:
2304        """Toggle between play and pause."""
2305        if not self.is_playing:
2306            self.resume()
2307        else:
2308            self.pause()
2309
2310    def oneforward(self, _obj, _evt) -> None:
2311        """Advance the animation by one frame."""
2312        self.pause()
2313        self.set_frame(self.value + 1)
2314
2315    def onebackward(self, _obj, _evt) -> None:
2316        """Go back one frame in the animation."""
2317        self.pause()
2318        self.set_frame(self.value - 1)
2319
2320    def set_frame(self, value: int) -> None:
2321        """Set the current value of the animation."""
2322        if self._loop:
2323            if value < self.min_value:
2324                value = self.max_value - 1
2325            elif value >= self.max_value:
2326                value = self.min_value
2327        else:
2328            if value < self.min_value:
2329                self.pause()
2330                value = self.min_value
2331            elif value >= self.max_value - 1:
2332                value = self.max_value - 1
2333                self.pause()
2334
2335        if self.value != value:
2336            self.value = value
2337            self.slider.value = value
2338            self._func(value)
2339
2340    def _slider_callback(self, widget: SliderWidget, _: str) -> None:
2341        self.pause()
2342        self.set_frame(int(round(widget.value)))
2343
2344    def _handle_timer(self, evt=None) -> None:
2345        self.set_frame(self.value + 1)
2346
2347    def stop(self) -> "AnimationPlayer":
2348        """
2349        Stop the animation timers, remove buttons and slider.
2350        Behave like a normal `Plotter` after this.
2351        """
2352        # stop timer
2353        if self.timer_id is not None:
2354            self.timer_callback("destroy", self.timer_id)
2355            self.timer_id = None
2356
2357        # remove callbacks
2358        self.remove_callback(self.timer_callback_id)
2359
2360        # remove buttons
2361        self.slider.off()
2362        self.renderer.RemoveActor(self.play_pause_button.actor)
2363        self.renderer.RemoveActor(self.button_oneback.actor)
2364        self.renderer.RemoveActor(self.button_oneforward.actor)
2365        return self
2366
2367
2368########################################################################
2369class Clock(vedo.Assembly):
2370    def __init__(self, h=None, m=None, s=None, font="Quikhand", title="", c="k"):
2371        """
2372        Create a clock with current time or user provided time.
2373
2374        Arguments:
2375            h : (int)
2376                hours in range [0,23]
2377            m : (int)
2378                minutes in range [0,59]
2379            s : (int)
2380                seconds in range [0,59]
2381            font : (str)
2382                font type
2383            title : (str)
2384                some extra text to show on the clock
2385            c : (str)
2386                color of the numbers
2387
2388        Example:
2389            ```python
2390            import time
2391            from vedo import show
2392            from vedo.applications import Clock
2393            clock = Clock()
2394            plt = show(clock, interactive=False)
2395            for i in range(10):
2396                time.sleep(1)
2397                clock.update()
2398                plt.render()
2399            plt.close()
2400            ```
2401            ![](https://vedo.embl.es/images/feats/clock.png)
2402        """
2403        self.elapsed = 0
2404        self._start = time.time()
2405
2406        wd = ""
2407        if h is None and m is None:
2408            t = time.localtime()
2409            h = t.tm_hour
2410            m = t.tm_min
2411            s = t.tm_sec
2412            if not title:
2413                d = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
2414                wd = f"{d[t.tm_wday]} {t.tm_mday}/{t.tm_mon}/{t.tm_year} "
2415
2416        h = int(h) % 24
2417        m = int(m) % 60
2418        t = (h * 60 + m) / 12 / 60
2419
2420        alpha = 2 * np.pi * t + np.pi / 2
2421        beta = 12 * 2 * np.pi * t + np.pi / 2
2422
2423        x1, y1 = np.cos(alpha), np.sin(alpha)
2424        x2, y2 = np.cos(beta), np.sin(beta)
2425        if s is not None:
2426            s = int(s) % 60
2427            gamma = s * 2 * np.pi / 60 + np.pi / 2
2428            x3, y3 = np.cos(gamma), np.sin(gamma)
2429
2430        ore = Line([0, 0], [x1, y1], lw=14, c="red4").scale(0.5).mirror()
2431        minu = Line([0, 0], [x2, y2], lw=7, c="blue3").scale(0.75).mirror()
2432        secs = None
2433        if s is not None:
2434            secs = Line([0, 0], [x3, y3], lw=1, c="k").scale(0.95).mirror()
2435            secs.z(0.003)
2436        back1 = vedo.shapes.Circle(res=180, c="k5")
2437        back2 = vedo.shapes.Circle(res=12).mirror().scale(0.84).rotate_z(-360 / 12)
2438        labels = back2.labels(range(1, 13), justify="center", font=font, c=c, scale=0.14)
2439        txt = vedo.shapes.Text3D(wd + title, font="VictorMono", justify="top-center", s=0.07, c=c)
2440        txt.pos(0, -0.25, 0.001)
2441        labels.z(0.001)
2442        minu.z(0.002)
2443        super().__init__([back1, labels, ore, minu, secs, txt])
2444        self.name = "Clock"
2445
2446    def update(self, h=None, m=None, s=None) -> "Clock":
2447        """Update clock with current or user time."""
2448        parts = self.unpack()
2449        self.elapsed = time.time() - self._start
2450
2451        if h is None and m is None:
2452            t = time.localtime()
2453            h = t.tm_hour
2454            m = t.tm_min
2455            s = t.tm_sec
2456
2457        h = int(h) % 24
2458        m = int(m) % 60
2459        t = (h * 60 + m) / 12 / 60
2460
2461        alpha = 2 * np.pi * t + np.pi / 2
2462        beta = 12 * 2 * np.pi * t + np.pi / 2
2463
2464        x1, y1 = np.cos(alpha), np.sin(alpha)
2465        x2, y2 = np.cos(beta), np.sin(beta)
2466        if s is not None:
2467            s = int(s) % 60
2468            gamma = s * 2 * np.pi / 60 + np.pi / 2
2469            x3, y3 = np.cos(gamma), np.sin(gamma)
2470
2471        pts2 = parts[2].vertices
2472        pts2[1] = [-x1 * 0.5, y1 * 0.5, 0.001]
2473        parts[2].vertices = pts2
2474
2475        pts3 = parts[3].vertices
2476        pts3[1] = [-x2 * 0.75, y2 * 0.75, 0.002]
2477        parts[3].vertices = pts3
2478
2479        if s is not None:
2480            pts4 = parts[4].vertices
2481            pts4[1] = [-x3 * 0.95, y3 * 0.95, 0.003]
2482            parts[4].vertices = pts4
2483
2484        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 = []
1481        self.points = None
1482        self.spline = None
1483        self.jline = None
1484        self.topline = None
1485        self.top_pts = []
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 = []
1481        self.points = None
1482        self.spline = None
1483        self.jline = None
1484        self.topline = None
1485        self.top_pts = []

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

Can also be invoked from command line with:

vedo --edit https://vedo.embl.es/examples/data/porsche.ply
Usage:
  • Left-click and hold to rotate
  • Right-click and move to draw line
  • Second right-click to stop drawing
  • Press "c" to clear points
  • "z/Z" to cut mesh (Z inverts inside-out the selection area)
  • "L" to keep only the largest connected surface
  • "s" to save mesh to file (tag _edited is appended to filename)
  • "u" to undo last action
  • "h" for help, "i" for info
Arguments:
  • mesh : (Mesh, Points) The input Mesh or pointcloud.
  • splined : (bool) join points with a spline or a simple line.
  • font : (str) Font name for the instructions.
  • alpha : (float) transparency of the instruction message panel.
  • lw : (str) selection line width.
  • lc : (str) selection line color.
  • pc : (str) selection points color.
  • c : (str) background color of instructions.
  • tc : (str) text color of instructions.
  • tol : (int) tolerance of the point proximity.
  • **kwargs : (dict) keyword arguments to pass to Plotter.
Examples:
def init(self, init_points):
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        def setOTF():
 969            otf = self.properties.GetScalarOpacity()
 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            slider_cmap()
 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        def setOTF():
 969            otf = self.properties.GetScalarOpacity()
 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            slider_cmap()
 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
3694    @property
3695    def camera(self):
3696        """Return the current active camera."""
3697        if self.renderer:
3698            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, mode="auto", **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            mode : (str)
1653                Set the mode of interaction.
1654            **kwargs : (dict)
1655                keyword arguments to pass to Plotter.
1656        """
1657        super().__init__(**kwargs)
1658
1659        self.verbose = True
1660        self.splined = splined
1661        self.resolution = None  # spline resolution (None = automatic)
1662        self.closed = closed
1663        self.lcolor = "yellow4"
1664        self.lwidth = 3
1665        self.pcolor = "purple5"
1666        self.psize = 10
1667
1668        self.cpoints = list(init_points)
1669        self.vpoints = None
1670        self.line = None
1671
1672        if isinstance(obj, str):
1673            self.object = vedo.file_io.load(obj)
1674        else:
1675            self.object = obj
1676
1677        self.mode = mode
1678        if self.mode == "auto":
1679            if isinstance(self.object, vedo.Image):
1680                self.mode = "image"
1681                self.parallel_projection(True)
1682            else:
1683                self.mode = "TrackballCamera"
1684
1685        t = (
1686            "Click to add a point\n"
1687            "Right-click to remove it\n"
1688            "Drag mouse to change contrast\n"
1689            "Press c to clear points\n"
1690            "Press q to continue"
1691        )
1692        self.instructions = Text2D(t, pos="bottom-left", c="white", bg="green", font="Calco")
1693
1694        self += [self.object, self.instructions]
1695
1696        self.callid1 = self.add_callback("KeyPress", self._key_press)
1697        self.callid2 = self.add_callback("LeftButtonPress", self._on_left_click)
1698        self.callid3 = self.add_callback("RightButtonPress", self._on_right_click)
1699
1700
1701    def points(self, newpts=None) -> Union["SplinePlotter", np.ndarray]:
1702        """Retrieve the 3D coordinates of the clicked points"""
1703        if newpts is not None:
1704            self.cpoints = newpts
1705            self.update()
1706            return self
1707        return np.array(self.cpoints)
1708
1709    def _on_left_click(self, evt):
1710        if not evt.actor:
1711            return
1712        if evt.actor.name == "points":
1713            # remove clicked point if clicked twice
1714            pid = self.vpoints.closest_point(evt.picked3d, return_point_id=True)
1715            self.cpoints.pop(pid)
1716            self.update()
1717            return
1718        p = evt.picked3d
1719        self.cpoints.append(p)
1720        self.update()
1721        if self.verbose:
1722            vedo.colors.printc("Added point:", precision(p, 4), c="g")
1723
1724    def _on_right_click(self, evt):
1725        if evt.actor and len(self.cpoints) > 0:
1726            self.cpoints.pop()  # pop removes from the list the last pt
1727            self.update()
1728            if self.verbose:
1729                vedo.colors.printc("Deleted last point", c="r")
1730
1731    def update(self):
1732        self.remove(self.line, self.vpoints)  # remove old points and spline
1733        self.vpoints = Points(self.cpoints).ps(self.psize).c(self.pcolor)
1734        self.vpoints.name = "points"
1735        self.vpoints.pickable(True)  # to allow toggle
1736        minnr = 1
1737        if self.splined:
1738            minnr = 2
1739        if self.lwidth and len(self.cpoints) > minnr:
1740            if self.splined:
1741                try:
1742                    self.line = Spline(self.cpoints, closed=self.closed, res=self.resolution)
1743                except ValueError:
1744                    # if clicking too close splining might fail
1745                    self.cpoints.pop()
1746                    return
1747            else:
1748                self.line = Line(self.cpoints, closed=self.closed)
1749            self.line.c(self.lcolor).lw(self.lwidth).pickable(False)
1750            self.add(self.vpoints, self.line)
1751        else:
1752            self.add(self.vpoints)
1753
1754    def _key_press(self, evt):
1755        if evt.keypress == "c":
1756            self.cpoints = []
1757            self.remove(self.line, self.vpoints).render()
1758            if self.verbose:
1759                vedo.colors.printc("==== Cleared all points ====", c="r", invert=True)
1760
1761    def start(self) -> "SplinePlotter":
1762        """Start the interaction"""
1763        self.update()
1764        self.show(self.object, self.instructions, mode=self.mode)
1765        return self

Interactive drawing of splined curves on meshes.

SplinePlotter( obj, init_points=(), closed=False, splined=True, mode='auto', **kwargs)
1636    def __init__(self, obj, init_points=(), closed=False, splined=True, mode="auto", **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            mode : (str)
1653                Set the mode of interaction.
1654            **kwargs : (dict)
1655                keyword arguments to pass to Plotter.
1656        """
1657        super().__init__(**kwargs)
1658
1659        self.verbose = True
1660        self.splined = splined
1661        self.resolution = None  # spline resolution (None = automatic)
1662        self.closed = closed
1663        self.lcolor = "yellow4"
1664        self.lwidth = 3
1665        self.pcolor = "purple5"
1666        self.psize = 10
1667
1668        self.cpoints = list(init_points)
1669        self.vpoints = None
1670        self.line = None
1671
1672        if isinstance(obj, str):
1673            self.object = vedo.file_io.load(obj)
1674        else:
1675            self.object = obj
1676
1677        self.mode = mode
1678        if self.mode == "auto":
1679            if isinstance(self.object, vedo.Image):
1680                self.mode = "image"
1681                self.parallel_projection(True)
1682            else:
1683                self.mode = "TrackballCamera"
1684
1685        t = (
1686            "Click to add a point\n"
1687            "Right-click to remove it\n"
1688            "Drag mouse to change contrast\n"
1689            "Press c to clear points\n"
1690            "Press q to continue"
1691        )
1692        self.instructions = Text2D(t, pos="bottom-left", c="white", bg="green", font="Calco")
1693
1694        self += [self.object, self.instructions]
1695
1696        self.callid1 = self.add_callback("KeyPress", self._key_press)
1697        self.callid2 = self.add_callback("LeftButtonPress", self._on_left_click)
1698        self.callid3 = self.add_callback("RightButtonPress", self._on_right_click)

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

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

Retrieve the 3D coordinates of the clicked points

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

Start the interaction

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

Pause the animation.

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

Resume the animation.

def toggle(self, _obj, _evt) -> None:
2304    def toggle(self, _obj, _evt) -> None:
2305        """Toggle between play and pause."""
2306        if not self.is_playing:
2307            self.resume()
2308        else:
2309            self.pause()

Toggle between play and pause.

def oneforward(self, _obj, _evt) -> None:
2311    def oneforward(self, _obj, _evt) -> None:
2312        """Advance the animation by one frame."""
2313        self.pause()
2314        self.set_frame(self.value + 1)

Advance the animation by one frame.

def onebackward(self, _obj, _evt) -> None:
2316    def onebackward(self, _obj, _evt) -> None:
2317        """Go back one frame in the animation."""
2318        self.pause()
2319        self.set_frame(self.value - 1)

Go back one frame in the animation.

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

Set the current value of the animation.

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

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