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