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