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