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