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