vedo.volume
Work with volumetric datasets (voxel data).
1import glob 2import os 3import time 4from weakref import ref as weak_ref_to 5from typing import Union, List, Iterable 6from typing_extensions import Self 7 8import numpy as np 9 10import vedo.vtkclasses as vtki 11 12import vedo 13from vedo import transformations 14from vedo import utils 15from vedo.mesh import Mesh 16from vedo.core import VolumeAlgorithms 17from vedo.visual import VolumeVisual 18 19 20__docformat__ = "google" 21 22__doc__ = """ 23Work with volumetric datasets (voxel data). 24 25![](https://vedo.embl.es/images/volumetric/slicePlane2.png) 26""" 27 28__all__ = ["Volume"] 29 30 31########################################################################## 32class Volume(VolumeAlgorithms, VolumeVisual): 33 """ 34 Class to describe dataset that are defined on "voxels", 35 the 3D equivalent of 2D pixels. 36 """ 37 def __init__( 38 self, 39 inputobj=None, 40 dims=None, 41 origin=None, 42 spacing=None, 43 ) -> None: 44 """ 45 This class can be initialized with a numpy object, 46 a `vtkImageData` or a list of 2D bmp files. 47 48 Arguments: 49 origin : (list) 50 set volume origin coordinates 51 spacing : (list) 52 voxel dimensions in x, y and z. 53 dims : (list) 54 specify the dimensions of the volume. 55 56 Example: 57 ```python 58 from vedo import Volume 59 vol = Volume("path/to/mydata/rec*.bmp") 60 vol.show() 61 ``` 62 63 Examples: 64 - [numpy2volume1.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/numpy2volume1.py) 65 66 ![](https://vedo.embl.es/images/volumetric/numpy2volume1.png) 67 68 - [read_volume2.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/read_volume2.py) 69 70 ![](https://vedo.embl.es/images/volumetric/read_volume2.png) 71 72 .. note:: 73 if a `list` of values is used for `alphas` this is interpreted 74 as a transfer function along the range of the scalar. 75 """ 76 super().__init__() 77 78 self.name = "Volume" 79 self.filename = "" 80 self.file_size = "" 81 82 self.info = {} 83 self.time = time.time() 84 85 self.actor = vtki.vtkVolume() 86 self.actor.retrieve_object = weak_ref_to(self) 87 self.properties = self.actor.GetProperty() 88 89 self.transform = None 90 self.point_locator = None 91 self.cell_locator = None 92 self.line_locator = None 93 94 ################### 95 if isinstance(inputobj, str): 96 if "https://" in inputobj: 97 inputobj = vedo.file_io.download(inputobj, verbose=False) # fpath 98 elif os.path.isfile(inputobj): 99 self.filename = inputobj 100 else: 101 inputobj = sorted(glob.glob(inputobj)) 102 103 ################### 104 inputtype = str(type(inputobj)) 105 106 # print('Volume inputtype', inputtype, c='b') 107 108 if inputobj is None: 109 img = vtki.vtkImageData() 110 111 elif utils.is_sequence(inputobj): 112 113 if isinstance(inputobj[0], str) and ".bmp" in inputobj[0].lower(): 114 # scan sequence of BMP files 115 ima = vtki.new("ImageAppend") 116 ima.SetAppendAxis(2) 117 pb = utils.ProgressBar(0, len(inputobj)) 118 for i in pb.range(): 119 f = inputobj[i] 120 if "_rec_spr" in f: # OPT specific 121 continue 122 picr = vtki.new("BMPReader") 123 picr.SetFileName(f) 124 picr.Update() 125 mgf = vtki.new("ImageMagnitude") 126 mgf.SetInputData(picr.GetOutput()) 127 mgf.Update() 128 ima.AddInputData(mgf.GetOutput()) 129 pb.print("loading...") 130 ima.Update() 131 img = ima.GetOutput() 132 133 else: 134 135 if len(inputobj.shape) == 1: 136 varr = utils.numpy2vtk(inputobj) 137 else: 138 varr = utils.numpy2vtk(inputobj.ravel(order="F")) 139 varr.SetName("input_scalars") 140 141 img = vtki.vtkImageData() 142 if dims is not None: 143 img.SetDimensions(dims[2], dims[1], dims[0]) 144 else: 145 if len(inputobj.shape) == 1: 146 vedo.logger.error("must set dimensions (dims keyword) in Volume") 147 raise RuntimeError() 148 img.SetDimensions(inputobj.shape) 149 img.GetPointData().AddArray(varr) 150 img.GetPointData().SetActiveScalars(varr.GetName()) 151 152 elif isinstance(inputobj, vtki.vtkImageData): 153 img = inputobj 154 155 elif isinstance(inputobj, str): 156 if "https://" in inputobj: 157 inputobj = vedo.file_io.download(inputobj, verbose=False) 158 img = vedo.file_io.loadImageData(inputobj) 159 self.filename = inputobj 160 161 else: 162 vedo.logger.error(f"cannot understand input type {inputtype}") 163 return 164 165 if dims is not None: 166 img.SetDimensions(dims) 167 168 if origin is not None: 169 img.SetOrigin(origin) 170 171 if spacing is not None: 172 img.SetSpacing(spacing) 173 174 self.dataset = img 175 self.transform = None 176 177 ##################################### 178 mapper = vtki.new("SmartVolumeMapper") 179 mapper.SetInputData(img) 180 self.actor.SetMapper(mapper) 181 182 if img.GetPointData().GetScalars(): 183 if img.GetPointData().GetScalars().GetNumberOfComponents() == 1: 184 self.properties.SetShade(True) 185 self.properties.SetInterpolationType(1) 186 self.cmap("RdBu_r") 187 self.alpha([0.0, 0.0, 0.2, 0.4, 0.8, 1.0]) 188 self.alpha_gradient(None) 189 self.properties.SetScalarOpacityUnitDistance(1.0) 190 191 self.pipeline = utils.OperationNode( 192 "Volume", comment=f"dims={tuple(self.dimensions())}", c="#4cc9f0" 193 ) 194 ####################################################################### 195 196 @property 197 def mapper(self): 198 """Return the underlying `vtkMapper` object.""" 199 return self.actor.GetMapper() 200 201 @mapper.setter 202 def mapper(self, mapper): 203 """ 204 Set the underlying `vtkMapper` object. 205 206 Arguments: 207 mapper : (str, vtkMapper) 208 either 'gpu', 'opengl_gpu', 'fixed' or 'smart' 209 """ 210 if isinstance(mapper, 211 (vtki.get_class("Mapper"), vtki.get_class("ImageResliceMapper")) 212 ): 213 pass 214 elif mapper is None: 215 mapper = vtki.new("SmartVolumeMapper") 216 elif "gpu" in mapper: 217 mapper = vtki.new("GPUVolumeRayCastMapper") 218 elif "opengl_gpu" in mapper: 219 mapper = vtki.new("OpenGLGPUVolumeRayCastMapper") 220 elif "smart" in mapper: 221 mapper = vtki.new("SmartVolumeMapper") 222 elif "fixed" in mapper: 223 mapper = vtki.new("FixedPointVolumeRayCastMapper") 224 else: 225 print("Error unknown mapper type", [mapper]) 226 raise RuntimeError() 227 self.actor.SetMapper(mapper) 228 229 def c(self, *args, **kwargs) -> Self: 230 """Deprecated. Use `Volume.cmap()` instead.""" 231 vedo.logger.warning("Volume.c() is deprecated, use Volume.cmap() instead") 232 return self.cmap(*args, **kwargs) 233 234 def _update(self, data, reset_locators=False): 235 # reset_locators here is dummy 236 self.dataset = data 237 self.mapper.SetInputData(data) 238 self.dataset.GetPointData().Modified() 239 self.mapper.Modified() 240 self.mapper.Update() 241 return self 242 243 def __str__(self): 244 """Print a summary for the `Volume` object.""" 245 module = self.__class__.__module__ 246 name = self.__class__.__name__ 247 out = vedo.printc( 248 f"{module}.{name} at ({hex(self.memory_address())})".ljust(75), 249 c="c", bold=True, invert=True, return_string=True, 250 ) 251 out += "\x1b[0m\x1b[36;1m" 252 253 out+= "name".ljust(14) + ": " + str(self.name) + "\n" 254 if self.filename: 255 out+= "filename".ljust(14) + ": " + str(self.filename) + "\n" 256 257 out+= "dimensions".ljust(14) + ": " + str(self.shape) + "\n" 258 259 out+= "origin".ljust(14) + ": " 260 out+= utils.precision(self.origin(), 6) + "\n" 261 262 out+= "center".ljust(14) + ": " 263 out+= utils.precision(self.center(), 6) + "\n" 264 265 out+= "spacing".ljust(14) + ": " 266 out+= utils.precision(self.spacing(), 6) + "\n" 267 268 bnds = self.bounds() 269 bx1, bx2 = utils.precision(bnds[0], 3), utils.precision(bnds[1], 3) 270 by1, by2 = utils.precision(bnds[2], 3), utils.precision(bnds[3], 3) 271 bz1, bz2 = utils.precision(bnds[4], 3), utils.precision(bnds[5], 3) 272 out+= "bounds".ljust(14) + ":" 273 out+= " x=(" + bx1 + ", " + bx2 + ")," 274 out+= " y=(" + by1 + ", " + by2 + ")," 275 out+= " z=(" + bz1 + ", " + bz2 + ")\n" 276 277 out+= "memory size".ljust(14) + ": " 278 out+= str(int(self.dataset.GetActualMemorySize()/1024+0.5))+" MB\n" 279 280 st = self.dataset.GetScalarTypeAsString() 281 out+= "scalar size".ljust(14) + ": " 282 out+= str(self.dataset.GetScalarSize()) + f" bytes ({st})\n" 283 out+= "scalar range".ljust(14) + ": " 284 out+= str(self.dataset.GetScalarRange()) + "\n" 285 286 #utils.print_histogram(self, logscale=True, bins=8, height=15, c="b", bold=True) 287 return out.rstrip() + "\x1b[0m" 288 289 def _repr_html_(self): 290 """ 291 HTML representation of the Volume object for Jupyter Notebooks. 292 293 Returns: 294 HTML text with the image and some properties. 295 """ 296 import io 297 import base64 298 from PIL import Image 299 300 library_name = "vedo.volume.Volume" 301 help_url = "https://vedo.embl.es/docs/vedo/volume.html" 302 303 arr = self.thumbnail(azimuth=0, elevation=-60, zoom=1.4, axes=True) 304 305 im = Image.fromarray(arr) 306 buffered = io.BytesIO() 307 im.save(buffered, format="PNG", quality=100) 308 encoded = base64.b64encode(buffered.getvalue()).decode("utf-8") 309 url = "data:image/png;base64," + encoded 310 image = f"<img src='{url}'></img>" 311 312 # statisitics 313 bounds = "<br/>".join( 314 [ 315 utils.precision(min_x, 4) + " ... " + utils.precision(max_x, 4) 316 for min_x, max_x in zip(self.bounds()[::2], self.bounds()[1::2]) 317 ] 318 ) 319 320 help_text = "" 321 if self.name: 322 help_text += f"<b> {self.name}:   </b>" 323 help_text += '<b><a href="' + help_url + '" target="_blank">' + library_name + "</a></b>" 324 if self.filename: 325 dots = "" 326 if len(self.filename) > 30: 327 dots = "..." 328 help_text += f"<br/><code><i>({dots}{self.filename[-30:]})</i></code>" 329 330 pdata = "" 331 if self.dataset.GetPointData().GetScalars(): 332 if self.dataset.GetPointData().GetScalars().GetName(): 333 name = self.dataset.GetPointData().GetScalars().GetName() 334 pdata = "<tr><td><b> point data array </b></td><td>" + name + "</td></tr>" 335 336 cdata = "" 337 if self.dataset.GetCellData().GetScalars(): 338 if self.dataset.GetCellData().GetScalars().GetName(): 339 name = self.dataset.GetCellData().GetScalars().GetName() 340 cdata = "<tr><td><b> voxel data array </b></td><td>" + name + "</td></tr>" 341 342 img = self.dataset 343 344 allt = [ 345 "<table>", 346 "<tr>", 347 "<td>", 348 image, 349 "</td>", 350 "<td style='text-align: center; vertical-align: center;'><br/>", 351 help_text, 352 "<table>", 353 "<tr><td><b> bounds </b> <br/> (x/y/z) </td><td>" + str(bounds) + "</td></tr>", 354 "<tr><td><b> dimensions </b></td><td>" + str(img.GetDimensions()) + "</td></tr>", 355 "<tr><td><b> voxel spacing </b></td><td>" 356 + utils.precision(img.GetSpacing(), 3) 357 + "</td></tr>", 358 "<tr><td><b> in memory size </b></td><td>" 359 + str(int(img.GetActualMemorySize() / 1024)) 360 + "MB</td></tr>", 361 pdata, 362 cdata, 363 "<tr><td><b> scalar range </b></td><td>" 364 + utils.precision(img.GetScalarRange(), 4) 365 + "</td></tr>", 366 "</table>", 367 "</table>", 368 ] 369 return "\n".join(allt) 370 371 def copy(self, deep=True) -> "Volume": 372 """Return a copy of the Volume. Alias of `clone()`.""" 373 return self.clone(deep=deep) 374 375 def clone(self, deep=True) -> "Volume": 376 """Return a clone copy of the Volume. Alias of `copy()`.""" 377 if deep: 378 newimg = vtki.vtkImageData() 379 newimg.CopyStructure(self.dataset) 380 newimg.CopyAttributes(self.dataset) 381 newvol = Volume(newimg) 382 else: 383 newvol = Volume(self.dataset) 384 385 prop = vtki.vtkVolumeProperty() 386 prop.DeepCopy(self.properties) 387 newvol.actor.SetProperty(prop) 388 newvol.properties = prop 389 390 newvol.pipeline = utils.OperationNode("clone", parents=[self], c="#bbd0ff", shape="diamond") 391 return newvol 392 393 def astype(self, dtype: Union[str, int]) -> Self: 394 """ 395 Reset the type of the scalars array. 396 397 Arguments: 398 dtype : (str) 399 the type of the scalars array in 400 ["int8", "uint8", "int16", "uint16", "int32", "uint32", "float32", "float64"] 401 """ 402 if dtype in ["int8", "uint8", "int16", "uint16", "int32", "uint32", "float32", "float64"]: 403 caster = vtki.new("ImageCast") 404 caster.SetInputData(self.dataset) 405 caster.SetOutputScalarType(int(vtki.array_types[dtype])) 406 caster.ClampOverflowOn() 407 caster.Update() 408 self._update(caster.GetOutput()) 409 self.pipeline = utils.OperationNode(f"astype({dtype})", parents=[self], c="#4cc9f0") 410 else: 411 vedo.logger.error(f"astype(): unknown type {dtype}") 412 raise ValueError() 413 return self 414 415 def component_weight(self, i: int, weight: float) -> Self: 416 """Set the scalar component weight in range [0,1].""" 417 self.properties.SetComponentWeight(i, weight) 418 return self 419 420 def xslice(self, i: int) -> Mesh: 421 """Extract the slice at index `i` of volume along x-axis.""" 422 vslice = vtki.new("ImageDataGeometryFilter") 423 vslice.SetInputData(self.dataset) 424 nx, ny, nz = self.dataset.GetDimensions() 425 if i > nx - 1: 426 i = nx - 1 427 vslice.SetExtent(i, i, 0, ny, 0, nz) 428 vslice.Update() 429 m = Mesh(vslice.GetOutput()) 430 m.pipeline = utils.OperationNode(f"xslice {i}", parents=[self], c="#4cc9f0:#e9c46a") 431 return m 432 433 def yslice(self, j: int) -> Mesh: 434 """Extract the slice at index `j` of volume along y-axis.""" 435 vslice = vtki.new("ImageDataGeometryFilter") 436 vslice.SetInputData(self.dataset) 437 nx, ny, nz = self.dataset.GetDimensions() 438 if j > ny - 1: 439 j = ny - 1 440 vslice.SetExtent(0, nx, j, j, 0, nz) 441 vslice.Update() 442 m = Mesh(vslice.GetOutput()) 443 m.pipeline = utils.OperationNode(f"yslice {j}", parents=[self], c="#4cc9f0:#e9c46a") 444 return m 445 446 def zslice(self, k: int) -> Mesh: 447 """Extract the slice at index `i` of volume along z-axis.""" 448 vslice = vtki.new("ImageDataGeometryFilter") 449 vslice.SetInputData(self.dataset) 450 nx, ny, nz = self.dataset.GetDimensions() 451 if k > nz - 1: 452 k = nz - 1 453 vslice.SetExtent(0, nx, 0, ny, k, k) 454 vslice.Update() 455 m = Mesh(vslice.GetOutput()) 456 m.pipeline = utils.OperationNode(f"zslice {k}", parents=[self], c="#4cc9f0:#e9c46a") 457 return m 458 459 def slice_plane(self, origin: List[float], normal: List[float], autocrop=False, border=0.5, mode="linear") -> Mesh: 460 """ 461 Extract the slice along a given plane position and normal. 462 463 Two metadata arrays are added to the output Mesh: 464 - "shape" : contains the shape of the slice 465 - "original_bounds" : contains the original bounds of the slice 466 One can access them with e.g. `myslice.metadata["shape"]`. 467 468 Arguments: 469 origin : (list) 470 position of the plane 471 normal : (list) 472 normal to the plane 473 autocrop : (bool) 474 crop the output to the minimal possible size 475 border : (float) 476 add a border to the output slice 477 mode : (str) 478 interpolation mode, one of the following: "linear", "nearest", "cubic" 479 480 Example: 481 - [slice_plane1.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/slice_plane1.py) 482 483 ![](https://vedo.embl.es/images/volumetric/slicePlane1.gif) 484 485 - [slice_plane2.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/slice_plane2.py) 486 487 ![](https://vedo.embl.es/images/volumetric/slicePlane2.png) 488 489 - [slice_plane3.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/slice_plane3.py) 490 491 ![](https://vedo.embl.es/images/volumetric/slicePlane3.jpg) 492 """ 493 newaxis = utils.versor(normal) 494 pos = np.array(origin) 495 initaxis = (0, 0, 1) 496 crossvec = np.cross(initaxis, newaxis) 497 angle = np.arccos(np.dot(initaxis, newaxis)) 498 T = vtki.vtkTransform() 499 T.PostMultiply() 500 T.RotateWXYZ(np.rad2deg(angle), crossvec.tolist()) 501 T.Translate(pos.tolist()) 502 503 reslice = vtki.new("ImageReslice") 504 reslice.SetResliceAxes(T.GetMatrix()) 505 reslice.SetInputData(self.dataset) 506 reslice.SetOutputDimensionality(2) 507 reslice.SetTransformInputSampling(True) 508 reslice.SetGenerateStencilOutput(False) 509 if border: 510 reslice.SetBorder(True) 511 reslice.SetBorderThickness(border) 512 else: 513 reslice.SetBorder(False) 514 if mode == "linear": 515 reslice.SetInterpolationModeToLinear() 516 elif mode == "nearest": 517 reslice.SetInterpolationModeToNearestNeighbor() 518 elif mode == "cubic": 519 reslice.SetInterpolationModeToCubic() 520 else: 521 vedo.logger.error(f"in slice_plane(): unknown interpolation mode {mode}") 522 raise ValueError() 523 reslice.SetAutoCropOutput(not autocrop) 524 reslice.Update() 525 img = reslice.GetOutput() 526 527 vslice = vtki.new("ImageDataGeometryFilter") 528 vslice.SetInputData(img) 529 vslice.Update() 530 531 msh = Mesh(vslice.GetOutput()).apply_transform(T) 532 msh.properties.LightingOff() 533 534 d0, d1, _ = img.GetDimensions() 535 varr1 = utils.numpy2vtk([d1, d0], name="shape") 536 varr2 = utils.numpy2vtk(img.GetBounds(), name="original_bounds") 537 msh.dataset.GetFieldData().AddArray(varr1) 538 msh.dataset.GetFieldData().AddArray(varr2) 539 msh.pipeline = utils.OperationNode("slice_plane", parents=[self], c="#4cc9f0:#e9c46a") 540 return msh 541 542 def slab(self, slice_range=(), axis='z', operation="mean") -> Mesh: 543 """ 544 Extract a slab from a `Volume` by combining 545 all of the slices of an image to create a single slice. 546 547 Returns a `Mesh` containing metadata which 548 can be accessed with e.g. `mesh.metadata["slab_range"]`. 549 550 Metadata: 551 slab_range : (list) 552 contains the range of slices extracted 553 slab_axis : (str) 554 contains the axis along which the slab was extracted 555 slab_operation : (str) 556 contains the operation performed on the slab 557 slab_bounding_box : (list) 558 contains the bounding box of the slab 559 560 Arguments: 561 slice_range : (list) 562 range of slices to extract 563 axis : (str) 564 axis along which to extract the slab 565 operation : (str) 566 operation to perform on the slab, 567 allowed values are: "sum", "min", "max", "mean". 568 569 Example: 570 - [slab.py](https://github.com/marcomusy/vedo/blob/master/examples/volumetric/slab_vol.py) 571 572 ![](https://vedo.embl.es/images/volumetric/slab_vol.jpg) 573 """ 574 if len(slice_range) != 2: 575 vedo.logger.error("in slab(): slice_range is empty or invalid") 576 raise ValueError() 577 578 islab = vtki.new("ImageSlab") 579 islab.SetInputData(self.dataset) 580 581 if operation in ["+", "add", "sum"]: 582 islab.SetOperationToSum() 583 elif "min" in operation: 584 islab.SetOperationToMin() 585 elif "max" in operation: 586 islab.SetOperationToMax() 587 elif "mean" in operation: 588 islab.SetOperationToMean() 589 else: 590 vedo.logger.error(f"in slab(): unknown operation {operation}") 591 raise ValueError() 592 593 dims = self.dimensions() 594 if axis == 'x': 595 islab.SetOrientationToX() 596 if slice_range[0] > dims[0]-1: 597 slice_range[0] = dims[0]-1 598 if slice_range[1] > dims[0]-1: 599 slice_range[1] = dims[0]-1 600 elif axis == 'y': 601 islab.SetOrientationToY() 602 if slice_range[0] > dims[1]-1: 603 slice_range[0] = dims[1]-1 604 if slice_range[1] > dims[1]-1: 605 slice_range[1] = dims[1]-1 606 elif axis == 'z': 607 islab.SetOrientationToZ() 608 if slice_range[0] > dims[2]-1: 609 slice_range[0] = dims[2]-1 610 if slice_range[1] > dims[2]-1: 611 slice_range[1] = dims[2]-1 612 else: 613 vedo.logger.error(f"Error in slab(): unknown axis {axis}") 614 raise RuntimeError() 615 616 islab.SetSliceRange(slice_range) 617 islab.Update() 618 619 msh = Mesh(islab.GetOutput()).lighting('off') 620 msh.mapper.SetLookupTable(utils.ctf2lut(self, msh)) 621 msh.mapper.SetScalarRange(self.scalar_range()) 622 623 msh.metadata["slab_range"] = slice_range 624 msh.metadata["slab_axis"] = axis 625 msh.metadata["slab_operation"] = operation 626 627 # compute bounds of slab 628 origin = list(self.origin()) 629 spacing = list(self.spacing()) 630 if axis == 'x': 631 msh.metadata["slab_bounding_box"] = [ 632 origin[0] + slice_range[0]*spacing[0], 633 origin[0] + slice_range[1]*spacing[0], 634 origin[1], 635 origin[1] + dims[1]*spacing[1], 636 origin[2], 637 origin[2] + dims[2]*spacing[2], 638 ] 639 elif axis == 'y': 640 msh.metadata["slab_bounding_box"] = [ 641 origin[0], 642 origin[0] + dims[0]*spacing[0], 643 origin[1] + slice_range[0]*spacing[1], 644 origin[1] + slice_range[1]*spacing[1], 645 origin[2], 646 origin[2] + dims[2]*spacing[2], 647 ] 648 elif axis == 'z': 649 msh.metadata["slab_bounding_box"] = [ 650 origin[0], 651 origin[0] + dims[0]*spacing[0], 652 origin[1], 653 origin[1] + dims[1]*spacing[1], 654 origin[2] + slice_range[0]*spacing[2], 655 origin[2] + slice_range[1]*spacing[2], 656 ] 657 658 msh.pipeline = utils.OperationNode( 659 f"slab{slice_range}", 660 comment=f"axis={axis}, operation={operation}", 661 parents=[self], 662 c="#4cc9f0:#e9c46a", 663 ) 664 msh.name = "SlabMesh" 665 return msh 666 667 668 def warp( 669 self, 670 source: Union["vedo.Points", List], 671 target: Union["vedo.Points", List], 672 sigma=1, mode="3d", fit=True, 673 ) -> Self: 674 """ 675 Warp volume scalars within a Volume by specifying 676 source and target sets of points. 677 678 Arguments: 679 source : (Points, list) 680 the list of source points 681 target : (Points, list) 682 the list of target points 683 fit : (bool) 684 fit/adapt the old bounding box to the warped geometry 685 """ 686 if isinstance(source, vedo.Points): 687 source = source.vertices 688 if isinstance(target, vedo.Points): 689 target = target.vertices 690 691 NLT = transformations.NonLinearTransform() 692 NLT.source_points = source 693 NLT.target_points = target 694 NLT.sigma = sigma 695 NLT.mode = mode 696 697 self.apply_transform(NLT, fit=fit) 698 self.pipeline = utils.OperationNode("warp", parents=[self], c="#4cc9f0") 699 return self 700 701 def apply_transform( 702 self, 703 T: Union[transformations.LinearTransform, transformations.NonLinearTransform], 704 fit=True, interpolation="cubic", 705 ) -> Self: 706 """ 707 Apply a transform to the scalars in the volume. 708 709 Arguments: 710 T : (LinearTransform, NonLinearTransform) 711 The transformation to be applied 712 fit : (bool) 713 fit/adapt the old bounding box to the modified geometry 714 interpolation : (str) 715 one of the following: "nearest", "linear", "cubic" 716 """ 717 if utils.is_sequence(T): 718 T = transformations.LinearTransform(T) 719 720 TI = T.compute_inverse() 721 722 reslice = vtki.new("ImageReslice") 723 reslice.SetInputData(self.dataset) 724 reslice.SetResliceTransform(TI.T) 725 reslice.SetOutputDimensionality(3) 726 if "lin" in interpolation.lower(): 727 reslice.SetInterpolationModeToLinear() 728 elif "near" in interpolation.lower(): 729 reslice.SetInterpolationModeToNearestNeighbor() 730 elif "cubic" in interpolation.lower(): 731 reslice.SetInterpolationModeToCubic() 732 else: 733 vedo.logger.error( 734 f"in apply_transform: unknown interpolation mode {interpolation}") 735 raise ValueError() 736 reslice.SetAutoCropOutput(fit) 737 reslice.Update() 738 self._update(reslice.GetOutput()) 739 self.transform = T 740 self.pipeline = utils.OperationNode( 741 "apply_transform", parents=[self], c="#4cc9f0") 742 return self 743 744 def imagedata(self) -> vtki.vtkImageData: 745 """ 746 DEPRECATED: 747 Use `Volume.dataset` instead. 748 749 Return the underlying `vtkImagaData` object. 750 """ 751 print("Volume.imagedata() is deprecated, use Volume.dataset instead") 752 return self.dataset 753 754 def modified(self) -> Self: 755 """ 756 Mark the object as modified. 757 758 Example: 759 760 - [numpy2volume0.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/numpy2volume0.py) 761 """ 762 scals = self.dataset.GetPointData().GetScalars() 763 if scals: 764 scals.Modified() 765 return self 766 767 def tonumpy(self) -> np.ndarray: 768 """ 769 Get read-write access to voxels of a Volume object as a numpy array. 770 771 When you set values in the output image, you don't want numpy to reallocate the array 772 but instead set values in the existing array, so use the [:] operator. 773 774 Example: 775 `arr[:] = arr*2 + 15` 776 777 If the array is modified add a call to: 778 `volume.modified()` 779 when all your modifications are completed. 780 """ 781 narray_shape = tuple(reversed(self.dataset.GetDimensions())) 782 783 scals = self.dataset.GetPointData().GetScalars() 784 comps = scals.GetNumberOfComponents() 785 if comps == 1: 786 narray = utils.vtk2numpy(scals).reshape(narray_shape) 787 narray = np.transpose(narray, axes=[2, 1, 0]) 788 else: 789 narray = utils.vtk2numpy(scals).reshape(*narray_shape, comps) 790 narray = np.transpose(narray, axes=[2, 1, 0, 3]) 791 792 # narray = utils.vtk2numpy(self.dataset.GetPointData().GetScalars()).reshape(narray_shape) 793 # narray = np.transpose(narray, axes=[2, 1, 0]) 794 return narray 795 796 @property 797 def shape(self) -> np.ndarray: 798 """Return the nr. of voxels in the 3 dimensions.""" 799 return np.array(self.dataset.GetDimensions()) 800 801 def dimensions(self) -> np.ndarray: 802 """Return the nr. of voxels in the 3 dimensions.""" 803 return np.array(self.dataset.GetDimensions()) 804 805 def scalar_range(self) -> np.ndarray: 806 """Return the range of the scalar values.""" 807 return np.array(self.dataset.GetScalarRange()) 808 809 def spacing(self, s=None) -> Union[Self, Iterable[float]]: 810 """Set/get the voxels size in the 3 dimensions.""" 811 if s is not None: 812 self.dataset.SetSpacing(s) 813 return self 814 return np.array(self.dataset.GetSpacing()) 815 816 def origin(self, s=None) -> Union[Self, Iterable[float]]: 817 """ 818 Set/get the origin of the volumetric dataset. 819 820 The origin is the position in world coordinates of the point index (0,0,0). 821 This point does not have to be part of the dataset, in other words, 822 the dataset extent does not have to start at (0,0,0) and the origin 823 can be outside of the dataset bounding box. 824 The origin plus spacing determine the position in space of the points. 825 """ 826 if s is not None: 827 self.dataset.SetOrigin(s) 828 return self 829 return np.array(self.dataset.GetOrigin()) 830 831 def pos(self, p=None) -> Union[Self, Iterable[float]]: 832 """Set/get the position of the volumetric dataset.""" 833 if p is not None: 834 self.origin(p) 835 return self 836 return self.origin() 837 838 def center(self) -> np.ndarray: 839 """Get the center of the volumetric dataset.""" 840 # note that this does not have the set method like origin and spacing 841 return np.array(self.dataset.GetCenter()) 842 843 def shift(self, s: list) -> Self: 844 """Shift the volumetric dataset by a vector.""" 845 self.origin(self.origin() + np.array(s)) 846 return self 847 848 def rotate_x(self, angle: float, rad=False, around=None) -> Self: 849 """ 850 Rotate around x-axis. If angle is in radians set `rad=True`. 851 852 Use `around` to define a pivoting point. 853 """ 854 if angle == 0: 855 return self 856 LT = transformations.LinearTransform().rotate_x(angle, rad, around) 857 return self.apply_transform(LT, fit=True, interpolation="linear") 858 859 def rotate_y(self, angle: float, rad=False, around=None) -> Self: 860 """ 861 Rotate around y-axis. If angle is in radians set `rad=True`. 862 863 Use `around` to define a pivoting point. 864 """ 865 if angle == 0: 866 return self 867 LT = transformations.LinearTransform().rotate_y(angle, rad, around) 868 return self.apply_transform(LT, fit=True, interpolation="linear") 869 870 def rotate_z(self, angle: float, rad=False, around=None) -> Self: 871 """ 872 Rotate around z-axis. If angle is in radians set `rad=True`. 873 874 Use `around` to define a pivoting point. 875 """ 876 if angle == 0: 877 return self 878 LT = transformations.LinearTransform().rotate_z(angle, rad, around) 879 return self.apply_transform(LT, fit=True, interpolation="linear") 880 881 def get_cell_from_ijk(self, ijk: list) -> int: 882 """ 883 Get the voxel id number at the given ijk coordinates. 884 885 Arguments: 886 ijk : (list) 887 the ijk coordinates of the voxel 888 """ 889 return self.dataset.ComputeCellId(ijk) 890 891 def get_point_from_ijk(self, ijk: list) -> int: 892 """ 893 Get the point id number at the given ijk coordinates. 894 895 Arguments: 896 ijk : (list) 897 the ijk coordinates of the voxel 898 """ 899 return self.dataset.ComputePointId(ijk) 900 901 def permute_axes(self, x: int, y: int, z: int) -> Self: 902 """ 903 Reorder the axes of the Volume by specifying 904 the input axes which are supposed to become the new X, Y, and Z. 905 """ 906 imp = vtki.new("ImagePermute") 907 imp.SetFilteredAxes(x, y, z) 908 imp.SetInputData(self.dataset) 909 imp.Update() 910 self._update(imp.GetOutput()) 911 self.pipeline = utils.OperationNode( 912 f"permute_axes({(x,y,z)})", parents=[self], c="#4cc9f0" 913 ) 914 return self 915 916 def resample(self, new_spacing: List[float], interpolation=1) -> Self: 917 """ 918 Resamples a `Volume` to be larger or smaller. 919 920 This method modifies the spacing of the input. 921 Linear interpolation is used to resample the data. 922 923 Arguments: 924 new_spacing : (list) 925 a list of 3 new spacings for the 3 axes 926 interpolation : (int) 927 0=nearest_neighbor, 1=linear, 2=cubic 928 """ 929 rsp = vtki.new("ImageResample") 930 oldsp = self.spacing() 931 for i in range(3): 932 if oldsp[i] != new_spacing[i]: 933 rsp.SetAxisOutputSpacing(i, new_spacing[i]) 934 rsp.InterpolateOn() 935 rsp.SetInterpolationMode(interpolation) 936 rsp.OptimizationOn() 937 rsp.Update() 938 self._update(rsp.GetOutput()) 939 self.pipeline = utils.OperationNode( 940 "resample", comment=f"spacing: {tuple(new_spacing)}", parents=[self], c="#4cc9f0" 941 ) 942 return self 943 944 945 def threshold(self, above=None, below=None, replace=None, replace_value=None) -> Self: 946 """ 947 Binary or continuous volume thresholding. 948 Find the voxels that contain a value above/below the input values 949 and replace them with a new value (default is 0). 950 """ 951 th = vtki.new("ImageThreshold") 952 th.SetInputData(self.dataset) 953 954 # sanity checks 955 if above is not None and below is not None: 956 if above == below: 957 return self 958 if above > below: 959 vedo.logger.warning("in volume.threshold(), above > below, skip.") 960 return self 961 962 ## cases 963 if below is not None and above is not None: 964 th.ThresholdBetween(above, below) 965 966 elif above is not None: 967 th.ThresholdByUpper(above) 968 969 elif below is not None: 970 th.ThresholdByLower(below) 971 972 ## 973 if replace is not None: 974 th.SetReplaceIn(True) 975 th.SetInValue(replace) 976 else: 977 th.SetReplaceIn(False) 978 979 if replace_value is not None: 980 th.SetReplaceOut(True) 981 th.SetOutValue(replace_value) 982 else: 983 th.SetReplaceOut(False) 984 985 th.Update() 986 self._update(th.GetOutput()) 987 self.pipeline = utils.OperationNode("threshold", parents=[self], c="#4cc9f0") 988 return self 989 990 def crop(self, left=None, right=None, back=None, front=None, bottom=None, top=None, VOI=()) -> Self: 991 """ 992 Crop a `Volume` object. 993 994 Arguments: 995 left : (float) 996 fraction to crop from the left plane (negative x) 997 right : (float) 998 fraction to crop from the right plane (positive x) 999 back : (float) 1000 fraction to crop from the back plane (negative y) 1001 front : (float) 1002 fraction to crop from the front plane (positive y) 1003 bottom : (float) 1004 fraction to crop from the bottom plane (negative z) 1005 top : (float) 1006 fraction to crop from the top plane (positive z) 1007 VOI : (list) 1008 extract Volume Of Interest expressed in voxel numbers 1009 1010 Example: 1011 `vol.crop(VOI=(xmin, xmax, ymin, ymax, zmin, zmax)) # all integers nrs` 1012 """ 1013 extractVOI = vtki.new("ExtractVOI") 1014 extractVOI.SetInputData(self.dataset) 1015 1016 if VOI: 1017 extractVOI.SetVOI(VOI) 1018 else: 1019 d = self.dataset.GetDimensions() 1020 bx0, bx1, by0, by1, bz0, bz1 = 0, d[0]-1, 0, d[1]-1, 0, d[2]-1 1021 if left is not None: bx0 = int((d[0]-1)*left) 1022 if right is not None: bx1 = int((d[0]-1)*(1-right)) 1023 if back is not None: by0 = int((d[1]-1)*back) 1024 if front is not None: by1 = int((d[1]-1)*(1-front)) 1025 if bottom is not None: bz0 = int((d[2]-1)*bottom) 1026 if top is not None: bz1 = int((d[2]-1)*(1-top)) 1027 extractVOI.SetVOI(bx0, bx1, by0, by1, bz0, bz1) 1028 extractVOI.Update() 1029 self._update(extractVOI.GetOutput()) 1030 1031 self.pipeline = utils.OperationNode( 1032 "crop", parents=[self], c="#4cc9f0", comment=f"dims={tuple(self.dimensions())}" 1033 ) 1034 return self 1035 1036 def append(self, *volumes, axis="z", preserve_extents=False) -> Self: 1037 """ 1038 Take the components from multiple inputs and merges them into one output. 1039 Except for the append axis, all inputs must have the same extent. 1040 All inputs must have the same number of scalar components. 1041 The output has the same origin and spacing as the first input. 1042 The origin and spacing of all other inputs are ignored. 1043 All inputs must have the same scalar type. 1044 1045 Arguments: 1046 axis : (int, str) 1047 axis expanded to hold the multiple images 1048 preserve_extents : (bool) 1049 if True, the extent of the inputs is used to place 1050 the image in the output. The whole extent of the output is the union of the input 1051 whole extents. Any portion of the output not covered by the inputs is set to zero. 1052 The origin and spacing is taken from the first input. 1053 1054 Example: 1055 ```python 1056 from vedo import Volume, dataurl 1057 vol = Volume(dataurl+'embryo.tif') 1058 vol.append(vol, axis='x').show().close() 1059 ``` 1060 ![](https://vedo.embl.es/images/feats/volume_append.png) 1061 """ 1062 ima = vtki.new("ImageAppend") 1063 ima.SetInputData(self.dataset) 1064 # if not utils.is_sequence(volumes): 1065 # volumes = [volumes] 1066 for volume in volumes: 1067 if isinstance(volume, vtki.vtkImageData): 1068 ima.AddInputData(volume) 1069 else: 1070 ima.AddInputData(volume.dataset) 1071 ima.SetPreserveExtents(preserve_extents) 1072 if axis == "x": 1073 axis = 0 1074 elif axis == "y": 1075 axis = 1 1076 elif axis == "z": 1077 axis = 2 1078 ima.SetAppendAxis(axis) 1079 ima.Update() 1080 self._update(ima.GetOutput()) 1081 1082 self.pipeline = utils.OperationNode( 1083 "append", 1084 parents=[self, *volumes], 1085 c="#4cc9f0", 1086 comment=f"dims={tuple(self.dimensions())}", 1087 ) 1088 return self 1089 1090 def pad(self, voxels=10, value=0) -> Self: 1091 """ 1092 Add the specified number of voxels at the `Volume` borders. 1093 Voxels can be a list formatted as `[nx0, nx1, ny0, ny1, nz0, nz1]`. 1094 1095 Arguments: 1096 voxels : (int, list) 1097 number of voxels to be added (or a list of length 4) 1098 value : (int) 1099 intensity value (gray-scale color) of the padding 1100 1101 Example: 1102 ```python 1103 from vedo import Volume, dataurl, show 1104 iso = Volume(dataurl+'embryo.tif').isosurface() 1105 vol = iso.binarize(spacing=(100, 100, 100)).pad(10) 1106 vol.dilate([15,15,15]) 1107 show(iso, vol.isosurface(), N=2, axes=1) 1108 ``` 1109 ![](https://vedo.embl.es/images/volumetric/volume_pad.png) 1110 """ 1111 x0, x1, y0, y1, z0, z1 = self.dataset.GetExtent() 1112 pf = vtki.new("ImageConstantPad") 1113 pf.SetInputData(self.dataset) 1114 pf.SetConstant(value) 1115 if utils.is_sequence(voxels): 1116 pf.SetOutputWholeExtent( 1117 x0 - voxels[0], x1 + voxels[1], 1118 y0 - voxels[2], y1 + voxels[3], 1119 z0 - voxels[4], z1 + voxels[5], 1120 ) 1121 else: 1122 pf.SetOutputWholeExtent( 1123 x0 - voxels, x1 + voxels, 1124 y0 - voxels, y1 + voxels, 1125 z0 - voxels, z1 + voxels, 1126 ) 1127 pf.Update() 1128 self._update(pf.GetOutput()) 1129 self.pipeline = utils.OperationNode( 1130 "pad", comment=f"{voxels} voxels", parents=[self], c="#f28482" 1131 ) 1132 return self 1133 1134 def resize(self, newdims: List[int]) -> Self: 1135 """Increase or reduce the number of voxels of a Volume with interpolation.""" 1136 rsz = vtki.new("ImageResize") 1137 rsz.SetResizeMethodToOutputDimensions() 1138 rsz.SetInputData(self.dataset) 1139 rsz.SetOutputDimensions(newdims) 1140 rsz.Update() 1141 self.dataset = rsz.GetOutput() 1142 self._update(self.dataset) 1143 self.pipeline = utils.OperationNode( 1144 "resize", parents=[self], c="#4cc9f0", comment=f"dims={tuple(self.dimensions())}" 1145 ) 1146 return self 1147 1148 def normalize(self) -> Self: 1149 """Normalize that scalar components for each point.""" 1150 norm = vtki.new("ImageNormalize") 1151 norm.SetInputData(self.dataset) 1152 norm.Update() 1153 self._update(norm.GetOutput()) 1154 self.pipeline = utils.OperationNode("normalize", parents=[self], c="#4cc9f0") 1155 return self 1156 1157 def mirror(self, axis="x") -> Self: 1158 """ 1159 Mirror flip along one of the cartesian axes. 1160 """ 1161 img = self.dataset 1162 1163 ff = vtki.new("ImageFlip") 1164 ff.SetInputData(img) 1165 if axis.lower() == "x": 1166 ff.SetFilteredAxis(0) 1167 elif axis.lower() == "y": 1168 ff.SetFilteredAxis(1) 1169 elif axis.lower() == "z": 1170 ff.SetFilteredAxis(2) 1171 else: 1172 vedo.logger.error("mirror must be set to either x, y, z or n") 1173 raise RuntimeError() 1174 ff.Update() 1175 self._update(ff.GetOutput()) 1176 self.pipeline = utils.OperationNode(f"mirror {axis}", parents=[self], c="#4cc9f0") 1177 return self 1178 1179 def operation(self, operation: str, volume2=None) -> "Volume": 1180 """ 1181 Perform operations with `Volume` objects. 1182 Keyword `volume2` can be a constant `float`. 1183 1184 Possible operations are: 1185 ``` 1186 and, or, xor, nand, nor, not, 1187 +, -, /, 1/x, sin, cos, exp, log, 1188 abs, **2, sqrt, min, max, atan, atan2, median, 1189 mag, dot, gradient, divergence, laplacian. 1190 ``` 1191 1192 Example: 1193 ```py 1194 from vedo import Box, show 1195 vol1 = Box(size=(35,10, 5)).binarize() 1196 vol2 = Box(size=( 5,10,35)).binarize() 1197 vol = vol1.operation("xor", vol2) 1198 show([[vol1, vol2], 1199 ["vol1 xor vol2", vol]], 1200 N=2, axes=1, viewup="z", 1201 ).close() 1202 ``` 1203 1204 Note: 1205 For logic operations, the two volumes must have the same bounds. 1206 If they do not, a larger image is created to contain both and the 1207 volumes are resampled onto the larger image before the operation is 1208 performed. This can be slow and memory intensive. 1209 1210 See also: 1211 - [volume_operations.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/volume_operations.py) 1212 """ 1213 op = operation.lower() 1214 image1 = self.dataset 1215 1216 if op in ["and", "or", "xor", "nand", "nor"]: 1217 1218 if not np.allclose(image1.GetBounds(), volume2.dataset.GetBounds()): 1219 # create a larger image to contain both 1220 b1 = image1.GetBounds() 1221 b2 = volume2.dataset.GetBounds() 1222 b = [ 1223 min(b1[0], b2[0]), 1224 max(b1[1], b2[1]), 1225 min(b1[2], b2[2]), 1226 max(b1[3], b2[3]), 1227 min(b1[4], b2[4]), 1228 max(b1[5], b2[5]), 1229 ] 1230 dims1 = image1.GetDimensions() 1231 dims2 = volume2.dataset.GetDimensions() 1232 dims = [max(dims1[0], dims2[0]), max(dims1[1], dims2[1]), max(dims1[2], dims2[2])] 1233 1234 image = vtki.vtkImageData() 1235 image.SetDimensions(dims) 1236 spacing = ( 1237 (b[1] - b[0]) / dims[0], 1238 (b[3] - b[2]) / dims[1], 1239 (b[5] - b[4]) / dims[2], 1240 ) 1241 image.SetSpacing(spacing) 1242 image.SetOrigin((b[0], b[2], b[4])) 1243 image.AllocateScalars(vtki.VTK_UNSIGNED_CHAR, 1) 1244 image.GetPointData().GetScalars().FillComponent(0, 0) 1245 1246 interp1 = vtki.new("ImageReslice") 1247 interp1.SetInputData(image1) 1248 interp1.SetOutputExtent(image.GetExtent()) 1249 interp1.SetOutputOrigin(image.GetOrigin()) 1250 interp1.SetOutputSpacing(image.GetSpacing()) 1251 interp1.SetInterpolationModeToNearestNeighbor() 1252 interp1.Update() 1253 imageA = interp1.GetOutput() 1254 1255 interp2 = vtki.new("ImageReslice") 1256 interp2.SetInputData(volume2.dataset) 1257 interp2.SetOutputExtent(image.GetExtent()) 1258 interp2.SetOutputOrigin(image.GetOrigin()) 1259 interp2.SetOutputSpacing(image.GetSpacing()) 1260 interp2.SetInterpolationModeToNearestNeighbor() 1261 interp2.Update() 1262 imageB = interp2.GetOutput() 1263 1264 else: 1265 imageA = image1 1266 imageB = volume2.dataset 1267 1268 img_logic = vtki.new("ImageLogic") 1269 img_logic.SetInput1Data(imageA) 1270 img_logic.SetInput2Data(imageB) 1271 img_logic.SetOperation(["and", "or", "xor", "nand", "nor"].index(op)) 1272 img_logic.Update() 1273 1274 out_vol = Volume(img_logic.GetOutput()) 1275 out_vol.pipeline = utils.OperationNode( 1276 "operation", comment=f"{op}", parents=[self, volume2], c="#4cc9f0", shape="cylinder" 1277 ) 1278 return out_vol ###################################################### 1279 1280 if volume2 and isinstance(volume2, Volume): 1281 # assert image1.GetScalarType() == volume2.dataset.GetScalarType(), "volumes have different scalar types" 1282 # make sure they have the same bounds: 1283 assert np.allclose(image1.GetBounds(), volume2.dataset.GetBounds()), "volumes have different bounds" 1284 # make sure they have the same spacing: 1285 assert np.allclose(image1.GetSpacing(), volume2.dataset.GetSpacing()), "volumes have different spacing" 1286 # make sure they have the same origin: 1287 assert np.allclose(image1.GetOrigin(), volume2.dataset.GetOrigin()), "volumes have different origin" 1288 1289 mf = None 1290 if op in ["median"]: 1291 mf = vtki.new("ImageMedian3D") 1292 mf.SetInputData(image1) 1293 elif op in ["mag"]: 1294 mf = vtki.new("ImageMagnitude") 1295 mf.SetInputData(image1) 1296 elif op in ["dot"]: 1297 mf = vtki.new("ImageDotProduct") 1298 mf.SetInput1Data(image1) 1299 mf.SetInput2Data(volume2.dataset) 1300 elif op in ["grad", "gradient"]: 1301 mf = vtki.new("ImageGradient") 1302 mf.SetDimensionality(3) 1303 mf.SetInputData(image1) 1304 elif op in ["div", "divergence"]: 1305 mf = vtki.new("ImageDivergence") 1306 mf.SetInputData(image1) 1307 elif op in ["laplacian"]: 1308 mf = vtki.new("ImageLaplacian") 1309 mf.SetDimensionality(3) 1310 mf.SetInputData(image1) 1311 elif op in ["not"]: 1312 mf = vtki.new("ImageLogic") 1313 mf.SetInput1Data(image1) 1314 mf.SetOperation(4) 1315 1316 if mf is not None: 1317 mf.Update() 1318 vol = Volume(mf.GetOutput()) 1319 vol.pipeline = utils.OperationNode( 1320 "operation", comment=f"{op}", parents=[self], c="#4cc9f0", shape="cylinder" 1321 ) 1322 return vol ###################################################### 1323 1324 mat = vtki.new("ImageMathematics") 1325 mat.SetInput1Data(image1) 1326 1327 K = None 1328 1329 if utils.is_number(volume2): 1330 K = volume2 1331 mat.SetConstantK(K) 1332 mat.SetConstantC(K) 1333 1334 elif volume2 is not None: # assume image2 is a constant value 1335 mat.SetInput2Data(volume2.dataset) 1336 1337 # ########################### 1338 if op in ["+", "add", "plus"]: 1339 if K: 1340 mat.SetOperationToAddConstant() 1341 else: 1342 mat.SetOperationToAdd() 1343 1344 elif op in ["-", "subtract", "minus"]: 1345 if K: 1346 mat.SetConstantC(-float(K)) 1347 mat.SetOperationToAddConstant() 1348 else: 1349 mat.SetOperationToSubtract() 1350 1351 elif op in ["*", "multiply", "times"]: 1352 if K: 1353 mat.SetOperationToMultiplyByK() 1354 else: 1355 mat.SetOperationToMultiply() 1356 1357 elif op in ["/", "divide"]: 1358 if K: 1359 mat.SetConstantK(1.0 / K) 1360 mat.SetOperationToMultiplyByK() 1361 else: 1362 mat.SetOperationToDivide() 1363 1364 elif op in ["1/x", "invert"]: 1365 mat.SetOperationToInvert() 1366 elif op in ["sin"]: 1367 mat.SetOperationToSin() 1368 elif op in ["cos"]: 1369 mat.SetOperationToCos() 1370 elif op in ["exp"]: 1371 mat.SetOperationToExp() 1372 elif op in ["log"]: 1373 mat.SetOperationToLog() 1374 elif op in ["abs"]: 1375 mat.SetOperationToAbsoluteValue() 1376 elif op in ["**2", "square"]: 1377 mat.SetOperationToSquare() 1378 elif op in ["sqrt", "sqr"]: 1379 mat.SetOperationToSquareRoot() 1380 elif op in ["min"]: 1381 mat.SetOperationToMin() 1382 elif op in ["max"]: 1383 mat.SetOperationToMax() 1384 elif op in ["atan"]: 1385 mat.SetOperationToATAN() 1386 elif op in ["atan2"]: 1387 mat.SetOperationToATAN2() 1388 else: 1389 vedo.logger.error(f"unknown operation {operation}") 1390 raise RuntimeError() 1391 mat.Update() 1392 1393 self._update(mat.GetOutput()) 1394 1395 self.pipeline = utils.OperationNode( 1396 "operation", comment=f"{op}", parents=[self, volume2], shape="cylinder", c="#4cc9f0" 1397 ) 1398 return self 1399 1400 def frequency_pass_filter(self, low_cutoff=None, high_cutoff=None, order=1) -> Self: 1401 """ 1402 Low-pass and high-pass filtering become trivial in the frequency domain. 1403 A portion of the pixels/voxels are simply masked or attenuated. 1404 This function applies a high pass Butterworth filter that attenuates the 1405 frequency domain image. 1406 1407 The gradual attenuation of the filter is important. 1408 A simple high-pass filter would simply mask a set of pixels in the frequency domain, 1409 but the abrupt transition would cause a ringing effect in the spatial domain. 1410 1411 Arguments: 1412 low_cutoff : (list) 1413 the cutoff frequencies for x, y and z 1414 high_cutoff : (list) 1415 the cutoff frequencies for x, y and z 1416 order : (int) 1417 order determines sharpness of the cutoff curve 1418 """ 1419 # https://lorensen.github.io/VTKExamples/site/Cxx/ImageProcessing/IdealHighPass 1420 fft = vtki.new("ImageFFT") 1421 fft.SetInputData(self.dataset) 1422 fft.Update() 1423 out = fft.GetOutput() 1424 1425 if high_cutoff: 1426 blp = vtki.new("ImageButterworthLowPass") 1427 blp.SetInputData(out) 1428 blp.SetCutOff(high_cutoff) 1429 blp.SetOrder(order) 1430 blp.Update() 1431 out = blp.GetOutput() 1432 1433 if low_cutoff: 1434 bhp = vtki.new("ImageButterworthHighPass") 1435 bhp.SetInputData(out) 1436 bhp.SetCutOff(low_cutoff) 1437 bhp.SetOrder(order) 1438 bhp.Update() 1439 out = bhp.GetOutput() 1440 1441 rfft = vtki.new("ImageRFFT") 1442 rfft.SetInputData(out) 1443 rfft.Update() 1444 1445 ecomp = vtki.new("ImageExtractComponents") 1446 ecomp.SetInputData(rfft.GetOutput()) 1447 ecomp.SetComponents(0) 1448 ecomp.Update() 1449 self._update(ecomp.GetOutput()) 1450 self.pipeline = utils.OperationNode("frequency_pass_filter", parents=[self], c="#4cc9f0") 1451 return self 1452 1453 def smooth_gaussian(self, sigma=(2, 2, 2), radius=None) -> Self: 1454 """ 1455 Performs a convolution of the input Volume with a gaussian. 1456 1457 Arguments: 1458 sigma : (float, list) 1459 standard deviation(s) in voxel units. 1460 A list can be given to smooth in the three direction differently. 1461 radius : (float, list) 1462 radius factor(s) determine how far out the gaussian 1463 kernel will go before being clamped to zero. A list can be given too. 1464 """ 1465 gsf = vtki.new("ImageGaussianSmooth") 1466 gsf.SetDimensionality(3) 1467 gsf.SetInputData(self.dataset) 1468 if utils.is_sequence(sigma): 1469 gsf.SetStandardDeviations(sigma) 1470 else: 1471 gsf.SetStandardDeviation(sigma) 1472 if radius is not None: 1473 if utils.is_sequence(radius): 1474 gsf.SetRadiusFactors(radius) 1475 else: 1476 gsf.SetRadiusFactor(radius) 1477 gsf.Update() 1478 self._update(gsf.GetOutput()) 1479 self.pipeline = utils.OperationNode("smooth_gaussian", parents=[self], c="#4cc9f0") 1480 return self 1481 1482 def smooth_median(self, neighbours=(2, 2, 2)) -> Self: 1483 """ 1484 Median filter that replaces each pixel with the median value 1485 from a rectangular neighborhood around that pixel. 1486 """ 1487 imgm = vtki.new("ImageMedian3D") 1488 imgm.SetInputData(self.dataset) 1489 if utils.is_sequence(neighbours): 1490 imgm.SetKernelSize(neighbours[0], neighbours[1], neighbours[2]) 1491 else: 1492 imgm.SetKernelSize(neighbours, neighbours, neighbours) 1493 imgm.Update() 1494 self._update(imgm.GetOutput()) 1495 self.pipeline = utils.OperationNode("smooth_median", parents=[self], c="#4cc9f0") 1496 return self 1497 1498 def erode(self, neighbours=(2, 2, 2)) -> Self: 1499 """ 1500 Replace a voxel with the minimum over an ellipsoidal neighborhood of voxels. 1501 If `neighbours` of an axis is 1, no processing is done on that axis. 1502 1503 Examples: 1504 - [erode_dilate.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/erode_dilate.py) 1505 1506 ![](https://vedo.embl.es/images/volumetric/erode_dilate.png) 1507 """ 1508 ver = vtki.new("ImageContinuousErode3D") 1509 ver.SetInputData(self.dataset) 1510 ver.SetKernelSize(neighbours[0], neighbours[1], neighbours[2]) 1511 ver.Update() 1512 self._update(ver.GetOutput()) 1513 self.pipeline = utils.OperationNode("erode", parents=[self], c="#4cc9f0") 1514 return self 1515 1516 def dilate(self, neighbours=(2, 2, 2)) -> Self: 1517 """ 1518 Replace a voxel with the maximum over an ellipsoidal neighborhood of voxels. 1519 If `neighbours` of an axis is 1, no processing is done on that axis. 1520 1521 Check also `erode()` and `pad()`. 1522 1523 Examples: 1524 - [erode_dilate.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/erode_dilate.py) 1525 """ 1526 ver = vtki.new("ImageContinuousDilate3D") 1527 ver.SetInputData(self.dataset) 1528 ver.SetKernelSize(neighbours[0], neighbours[1], neighbours[2]) 1529 ver.Update() 1530 self._update(ver.GetOutput()) 1531 self.pipeline = utils.OperationNode("dilate", parents=[self], c="#4cc9f0") 1532 return self 1533 1534 def magnitude(self) -> Self: 1535 """Colapses components with magnitude function.""" 1536 imgm = vtki.new("ImageMagnitude") 1537 imgm.SetInputData(self.dataset) 1538 imgm.Update() 1539 self._update(imgm.GetOutput()) 1540 self.pipeline = utils.OperationNode("magnitude", parents=[self], c="#4cc9f0") 1541 return self 1542 1543 def topoints(self) -> "vedo.Points": 1544 """ 1545 Extract all image voxels as points. 1546 This function takes an input `Volume` and creates an `Mesh` 1547 that contains the points and the point attributes. 1548 1549 Examples: 1550 - [vol2points.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/vol2points.py) 1551 """ 1552 v2p = vtki.new("ImageToPoints") 1553 v2p.SetInputData(self.dataset) 1554 v2p.Update() 1555 mpts = vedo.Points(v2p.GetOutput()) 1556 mpts.pipeline = utils.OperationNode("topoints", parents=[self], c="#4cc9f0:#e9c46a") 1557 return mpts 1558 1559 def euclidean_distance(self, anisotropy=False, max_distance=None) -> "Volume": 1560 """ 1561 Implementation of the Euclidean DT (Distance Transform) using Saito's algorithm. 1562 The distance map produced contains the square of the Euclidean distance values. 1563 The algorithm has a O(n^(D+1)) complexity over n x n x...x n images in D dimensions. 1564 1565 Check out also: https://en.wikipedia.org/wiki/Distance_transform 1566 1567 Arguments: 1568 anisotropy : bool 1569 used to define whether Spacing should be used in the 1570 computation of the distances. 1571 max_distance : bool 1572 any distance bigger than max_distance will not be 1573 computed but set to this specified value instead. 1574 1575 Examples: 1576 - [euclidian_dist.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/euclidian_dist.py) 1577 """ 1578 euv = vtki.new("ImageEuclideanDistance") 1579 euv.SetInputData(self.dataset) 1580 euv.SetConsiderAnisotropy(anisotropy) 1581 if max_distance is not None: 1582 euv.InitializeOn() 1583 euv.SetMaximumDistance(max_distance) 1584 euv.SetAlgorithmToSaito() 1585 euv.Update() 1586 vol = Volume(euv.GetOutput()) 1587 vol.pipeline = utils.OperationNode("euclidean_distance", parents=[self], c="#4cc9f0") 1588 return vol 1589 1590 def correlation_with(self, vol2: "Volume", dim=2) -> "Volume": 1591 """ 1592 Find the correlation between two volumetric data sets. 1593 Keyword `dim` determines whether the correlation will be 3D, 2D or 1D. 1594 The default is a 2D Correlation. 1595 1596 The output size will match the size of the first input. 1597 The second input is considered the correlation kernel. 1598 """ 1599 imc = vtki.new("ImageCorrelation") 1600 imc.SetInput1Data(self.dataset) 1601 imc.SetInput2Data(vol2.dataset) 1602 imc.SetDimensionality(dim) 1603 imc.Update() 1604 vol = Volume(imc.GetOutput()) 1605 1606 vol.pipeline = utils.OperationNode("correlation_with", parents=[self, vol2], c="#4cc9f0") 1607 return vol 1608 1609 def scale_voxels(self, scale=1) -> Self: 1610 """Scale the voxel content by factor `scale`.""" 1611 rsl = vtki.new("ImageReslice") 1612 rsl.SetInputData(self.dataset) 1613 rsl.SetScalarScale(scale) 1614 rsl.Update() 1615 self._update(rsl.GetOutput()) 1616 self.pipeline = utils.OperationNode( 1617 "scale_voxels", comment=f"scale={scale}", parents=[self], c="#4cc9f0" 1618 ) 1619 return self
33class Volume(VolumeAlgorithms, VolumeVisual): 34 """ 35 Class to describe dataset that are defined on "voxels", 36 the 3D equivalent of 2D pixels. 37 """ 38 def __init__( 39 self, 40 inputobj=None, 41 dims=None, 42 origin=None, 43 spacing=None, 44 ) -> None: 45 """ 46 This class can be initialized with a numpy object, 47 a `vtkImageData` or a list of 2D bmp files. 48 49 Arguments: 50 origin : (list) 51 set volume origin coordinates 52 spacing : (list) 53 voxel dimensions in x, y and z. 54 dims : (list) 55 specify the dimensions of the volume. 56 57 Example: 58 ```python 59 from vedo import Volume 60 vol = Volume("path/to/mydata/rec*.bmp") 61 vol.show() 62 ``` 63 64 Examples: 65 - [numpy2volume1.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/numpy2volume1.py) 66 67 ![](https://vedo.embl.es/images/volumetric/numpy2volume1.png) 68 69 - [read_volume2.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/read_volume2.py) 70 71 ![](https://vedo.embl.es/images/volumetric/read_volume2.png) 72 73 .. note:: 74 if a `list` of values is used for `alphas` this is interpreted 75 as a transfer function along the range of the scalar. 76 """ 77 super().__init__() 78 79 self.name = "Volume" 80 self.filename = "" 81 self.file_size = "" 82 83 self.info = {} 84 self.time = time.time() 85 86 self.actor = vtki.vtkVolume() 87 self.actor.retrieve_object = weak_ref_to(self) 88 self.properties = self.actor.GetProperty() 89 90 self.transform = None 91 self.point_locator = None 92 self.cell_locator = None 93 self.line_locator = None 94 95 ################### 96 if isinstance(inputobj, str): 97 if "https://" in inputobj: 98 inputobj = vedo.file_io.download(inputobj, verbose=False) # fpath 99 elif os.path.isfile(inputobj): 100 self.filename = inputobj 101 else: 102 inputobj = sorted(glob.glob(inputobj)) 103 104 ################### 105 inputtype = str(type(inputobj)) 106 107 # print('Volume inputtype', inputtype, c='b') 108 109 if inputobj is None: 110 img = vtki.vtkImageData() 111 112 elif utils.is_sequence(inputobj): 113 114 if isinstance(inputobj[0], str) and ".bmp" in inputobj[0].lower(): 115 # scan sequence of BMP files 116 ima = vtki.new("ImageAppend") 117 ima.SetAppendAxis(2) 118 pb = utils.ProgressBar(0, len(inputobj)) 119 for i in pb.range(): 120 f = inputobj[i] 121 if "_rec_spr" in f: # OPT specific 122 continue 123 picr = vtki.new("BMPReader") 124 picr.SetFileName(f) 125 picr.Update() 126 mgf = vtki.new("ImageMagnitude") 127 mgf.SetInputData(picr.GetOutput()) 128 mgf.Update() 129 ima.AddInputData(mgf.GetOutput()) 130 pb.print("loading...") 131 ima.Update() 132 img = ima.GetOutput() 133 134 else: 135 136 if len(inputobj.shape) == 1: 137 varr = utils.numpy2vtk(inputobj) 138 else: 139 varr = utils.numpy2vtk(inputobj.ravel(order="F")) 140 varr.SetName("input_scalars") 141 142 img = vtki.vtkImageData() 143 if dims is not None: 144 img.SetDimensions(dims[2], dims[1], dims[0]) 145 else: 146 if len(inputobj.shape) == 1: 147 vedo.logger.error("must set dimensions (dims keyword) in Volume") 148 raise RuntimeError() 149 img.SetDimensions(inputobj.shape) 150 img.GetPointData().AddArray(varr) 151 img.GetPointData().SetActiveScalars(varr.GetName()) 152 153 elif isinstance(inputobj, vtki.vtkImageData): 154 img = inputobj 155 156 elif isinstance(inputobj, str): 157 if "https://" in inputobj: 158 inputobj = vedo.file_io.download(inputobj, verbose=False) 159 img = vedo.file_io.loadImageData(inputobj) 160 self.filename = inputobj 161 162 else: 163 vedo.logger.error(f"cannot understand input type {inputtype}") 164 return 165 166 if dims is not None: 167 img.SetDimensions(dims) 168 169 if origin is not None: 170 img.SetOrigin(origin) 171 172 if spacing is not None: 173 img.SetSpacing(spacing) 174 175 self.dataset = img 176 self.transform = None 177 178 ##################################### 179 mapper = vtki.new("SmartVolumeMapper") 180 mapper.SetInputData(img) 181 self.actor.SetMapper(mapper) 182 183 if img.GetPointData().GetScalars(): 184 if img.GetPointData().GetScalars().GetNumberOfComponents() == 1: 185 self.properties.SetShade(True) 186 self.properties.SetInterpolationType(1) 187 self.cmap("RdBu_r") 188 self.alpha([0.0, 0.0, 0.2, 0.4, 0.8, 1.0]) 189 self.alpha_gradient(None) 190 self.properties.SetScalarOpacityUnitDistance(1.0) 191 192 self.pipeline = utils.OperationNode( 193 "Volume", comment=f"dims={tuple(self.dimensions())}", c="#4cc9f0" 194 ) 195 ####################################################################### 196 197 @property 198 def mapper(self): 199 """Return the underlying `vtkMapper` object.""" 200 return self.actor.GetMapper() 201 202 @mapper.setter 203 def mapper(self, mapper): 204 """ 205 Set the underlying `vtkMapper` object. 206 207 Arguments: 208 mapper : (str, vtkMapper) 209 either 'gpu', 'opengl_gpu', 'fixed' or 'smart' 210 """ 211 if isinstance(mapper, 212 (vtki.get_class("Mapper"), vtki.get_class("ImageResliceMapper")) 213 ): 214 pass 215 elif mapper is None: 216 mapper = vtki.new("SmartVolumeMapper") 217 elif "gpu" in mapper: 218 mapper = vtki.new("GPUVolumeRayCastMapper") 219 elif "opengl_gpu" in mapper: 220 mapper = vtki.new("OpenGLGPUVolumeRayCastMapper") 221 elif "smart" in mapper: 222 mapper = vtki.new("SmartVolumeMapper") 223 elif "fixed" in mapper: 224 mapper = vtki.new("FixedPointVolumeRayCastMapper") 225 else: 226 print("Error unknown mapper type", [mapper]) 227 raise RuntimeError() 228 self.actor.SetMapper(mapper) 229 230 def c(self, *args, **kwargs) -> Self: 231 """Deprecated. Use `Volume.cmap()` instead.""" 232 vedo.logger.warning("Volume.c() is deprecated, use Volume.cmap() instead") 233 return self.cmap(*args, **kwargs) 234 235 def _update(self, data, reset_locators=False): 236 # reset_locators here is dummy 237 self.dataset = data 238 self.mapper.SetInputData(data) 239 self.dataset.GetPointData().Modified() 240 self.mapper.Modified() 241 self.mapper.Update() 242 return self 243 244 def __str__(self): 245 """Print a summary for the `Volume` object.""" 246 module = self.__class__.__module__ 247 name = self.__class__.__name__ 248 out = vedo.printc( 249 f"{module}.{name} at ({hex(self.memory_address())})".ljust(75), 250 c="c", bold=True, invert=True, return_string=True, 251 ) 252 out += "\x1b[0m\x1b[36;1m" 253 254 out+= "name".ljust(14) + ": " + str(self.name) + "\n" 255 if self.filename: 256 out+= "filename".ljust(14) + ": " + str(self.filename) + "\n" 257 258 out+= "dimensions".ljust(14) + ": " + str(self.shape) + "\n" 259 260 out+= "origin".ljust(14) + ": " 261 out+= utils.precision(self.origin(), 6) + "\n" 262 263 out+= "center".ljust(14) + ": " 264 out+= utils.precision(self.center(), 6) + "\n" 265 266 out+= "spacing".ljust(14) + ": " 267 out+= utils.precision(self.spacing(), 6) + "\n" 268 269 bnds = self.bounds() 270 bx1, bx2 = utils.precision(bnds[0], 3), utils.precision(bnds[1], 3) 271 by1, by2 = utils.precision(bnds[2], 3), utils.precision(bnds[3], 3) 272 bz1, bz2 = utils.precision(bnds[4], 3), utils.precision(bnds[5], 3) 273 out+= "bounds".ljust(14) + ":" 274 out+= " x=(" + bx1 + ", " + bx2 + ")," 275 out+= " y=(" + by1 + ", " + by2 + ")," 276 out+= " z=(" + bz1 + ", " + bz2 + ")\n" 277 278 out+= "memory size".ljust(14) + ": " 279 out+= str(int(self.dataset.GetActualMemorySize()/1024+0.5))+" MB\n" 280 281 st = self.dataset.GetScalarTypeAsString() 282 out+= "scalar size".ljust(14) + ": " 283 out+= str(self.dataset.GetScalarSize()) + f" bytes ({st})\n" 284 out+= "scalar range".ljust(14) + ": " 285 out+= str(self.dataset.GetScalarRange()) + "\n" 286 287 #utils.print_histogram(self, logscale=True, bins=8, height=15, c="b", bold=True) 288 return out.rstrip() + "\x1b[0m" 289 290 def _repr_html_(self): 291 """ 292 HTML representation of the Volume object for Jupyter Notebooks. 293 294 Returns: 295 HTML text with the image and some properties. 296 """ 297 import io 298 import base64 299 from PIL import Image 300 301 library_name = "vedo.volume.Volume" 302 help_url = "https://vedo.embl.es/docs/vedo/volume.html" 303 304 arr = self.thumbnail(azimuth=0, elevation=-60, zoom=1.4, axes=True) 305 306 im = Image.fromarray(arr) 307 buffered = io.BytesIO() 308 im.save(buffered, format="PNG", quality=100) 309 encoded = base64.b64encode(buffered.getvalue()).decode("utf-8") 310 url = "data:image/png;base64," + encoded 311 image = f"<img src='{url}'></img>" 312 313 # statisitics 314 bounds = "<br/>".join( 315 [ 316 utils.precision(min_x, 4) + " ... " + utils.precision(max_x, 4) 317 for min_x, max_x in zip(self.bounds()[::2], self.bounds()[1::2]) 318 ] 319 ) 320 321 help_text = "" 322 if self.name: 323 help_text += f"<b> {self.name}:   </b>" 324 help_text += '<b><a href="' + help_url + '" target="_blank">' + library_name + "</a></b>" 325 if self.filename: 326 dots = "" 327 if len(self.filename) > 30: 328 dots = "..." 329 help_text += f"<br/><code><i>({dots}{self.filename[-30:]})</i></code>" 330 331 pdata = "" 332 if self.dataset.GetPointData().GetScalars(): 333 if self.dataset.GetPointData().GetScalars().GetName(): 334 name = self.dataset.GetPointData().GetScalars().GetName() 335 pdata = "<tr><td><b> point data array </b></td><td>" + name + "</td></tr>" 336 337 cdata = "" 338 if self.dataset.GetCellData().GetScalars(): 339 if self.dataset.GetCellData().GetScalars().GetName(): 340 name = self.dataset.GetCellData().GetScalars().GetName() 341 cdata = "<tr><td><b> voxel data array </b></td><td>" + name + "</td></tr>" 342 343 img = self.dataset 344 345 allt = [ 346 "<table>", 347 "<tr>", 348 "<td>", 349 image, 350 "</td>", 351 "<td style='text-align: center; vertical-align: center;'><br/>", 352 help_text, 353 "<table>", 354 "<tr><td><b> bounds </b> <br/> (x/y/z) </td><td>" + str(bounds) + "</td></tr>", 355 "<tr><td><b> dimensions </b></td><td>" + str(img.GetDimensions()) + "</td></tr>", 356 "<tr><td><b> voxel spacing </b></td><td>" 357 + utils.precision(img.GetSpacing(), 3) 358 + "</td></tr>", 359 "<tr><td><b> in memory size </b></td><td>" 360 + str(int(img.GetActualMemorySize() / 1024)) 361 + "MB</td></tr>", 362 pdata, 363 cdata, 364 "<tr><td><b> scalar range </b></td><td>" 365 + utils.precision(img.GetScalarRange(), 4) 366 + "</td></tr>", 367 "</table>", 368 "</table>", 369 ] 370 return "\n".join(allt) 371 372 def copy(self, deep=True) -> "Volume": 373 """Return a copy of the Volume. Alias of `clone()`.""" 374 return self.clone(deep=deep) 375 376 def clone(self, deep=True) -> "Volume": 377 """Return a clone copy of the Volume. Alias of `copy()`.""" 378 if deep: 379 newimg = vtki.vtkImageData() 380 newimg.CopyStructure(self.dataset) 381 newimg.CopyAttributes(self.dataset) 382 newvol = Volume(newimg) 383 else: 384 newvol = Volume(self.dataset) 385 386 prop = vtki.vtkVolumeProperty() 387 prop.DeepCopy(self.properties) 388 newvol.actor.SetProperty(prop) 389 newvol.properties = prop 390 391 newvol.pipeline = utils.OperationNode("clone", parents=[self], c="#bbd0ff", shape="diamond") 392 return newvol 393 394 def astype(self, dtype: Union[str, int]) -> Self: 395 """ 396 Reset the type of the scalars array. 397 398 Arguments: 399 dtype : (str) 400 the type of the scalars array in 401 ["int8", "uint8", "int16", "uint16", "int32", "uint32", "float32", "float64"] 402 """ 403 if dtype in ["int8", "uint8", "int16", "uint16", "int32", "uint32", "float32", "float64"]: 404 caster = vtki.new("ImageCast") 405 caster.SetInputData(self.dataset) 406 caster.SetOutputScalarType(int(vtki.array_types[dtype])) 407 caster.ClampOverflowOn() 408 caster.Update() 409 self._update(caster.GetOutput()) 410 self.pipeline = utils.OperationNode(f"astype({dtype})", parents=[self], c="#4cc9f0") 411 else: 412 vedo.logger.error(f"astype(): unknown type {dtype}") 413 raise ValueError() 414 return self 415 416 def component_weight(self, i: int, weight: float) -> Self: 417 """Set the scalar component weight in range [0,1].""" 418 self.properties.SetComponentWeight(i, weight) 419 return self 420 421 def xslice(self, i: int) -> Mesh: 422 """Extract the slice at index `i` of volume along x-axis.""" 423 vslice = vtki.new("ImageDataGeometryFilter") 424 vslice.SetInputData(self.dataset) 425 nx, ny, nz = self.dataset.GetDimensions() 426 if i > nx - 1: 427 i = nx - 1 428 vslice.SetExtent(i, i, 0, ny, 0, nz) 429 vslice.Update() 430 m = Mesh(vslice.GetOutput()) 431 m.pipeline = utils.OperationNode(f"xslice {i}", parents=[self], c="#4cc9f0:#e9c46a") 432 return m 433 434 def yslice(self, j: int) -> Mesh: 435 """Extract the slice at index `j` of volume along y-axis.""" 436 vslice = vtki.new("ImageDataGeometryFilter") 437 vslice.SetInputData(self.dataset) 438 nx, ny, nz = self.dataset.GetDimensions() 439 if j > ny - 1: 440 j = ny - 1 441 vslice.SetExtent(0, nx, j, j, 0, nz) 442 vslice.Update() 443 m = Mesh(vslice.GetOutput()) 444 m.pipeline = utils.OperationNode(f"yslice {j}", parents=[self], c="#4cc9f0:#e9c46a") 445 return m 446 447 def zslice(self, k: int) -> Mesh: 448 """Extract the slice at index `i` of volume along z-axis.""" 449 vslice = vtki.new("ImageDataGeometryFilter") 450 vslice.SetInputData(self.dataset) 451 nx, ny, nz = self.dataset.GetDimensions() 452 if k > nz - 1: 453 k = nz - 1 454 vslice.SetExtent(0, nx, 0, ny, k, k) 455 vslice.Update() 456 m = Mesh(vslice.GetOutput()) 457 m.pipeline = utils.OperationNode(f"zslice {k}", parents=[self], c="#4cc9f0:#e9c46a") 458 return m 459 460 def slice_plane(self, origin: List[float], normal: List[float], autocrop=False, border=0.5, mode="linear") -> Mesh: 461 """ 462 Extract the slice along a given plane position and normal. 463 464 Two metadata arrays are added to the output Mesh: 465 - "shape" : contains the shape of the slice 466 - "original_bounds" : contains the original bounds of the slice 467 One can access them with e.g. `myslice.metadata["shape"]`. 468 469 Arguments: 470 origin : (list) 471 position of the plane 472 normal : (list) 473 normal to the plane 474 autocrop : (bool) 475 crop the output to the minimal possible size 476 border : (float) 477 add a border to the output slice 478 mode : (str) 479 interpolation mode, one of the following: "linear", "nearest", "cubic" 480 481 Example: 482 - [slice_plane1.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/slice_plane1.py) 483 484 ![](https://vedo.embl.es/images/volumetric/slicePlane1.gif) 485 486 - [slice_plane2.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/slice_plane2.py) 487 488 ![](https://vedo.embl.es/images/volumetric/slicePlane2.png) 489 490 - [slice_plane3.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/slice_plane3.py) 491 492 ![](https://vedo.embl.es/images/volumetric/slicePlane3.jpg) 493 """ 494 newaxis = utils.versor(normal) 495 pos = np.array(origin) 496 initaxis = (0, 0, 1) 497 crossvec = np.cross(initaxis, newaxis) 498 angle = np.arccos(np.dot(initaxis, newaxis)) 499 T = vtki.vtkTransform() 500 T.PostMultiply() 501 T.RotateWXYZ(np.rad2deg(angle), crossvec.tolist()) 502 T.Translate(pos.tolist()) 503 504 reslice = vtki.new("ImageReslice") 505 reslice.SetResliceAxes(T.GetMatrix()) 506 reslice.SetInputData(self.dataset) 507 reslice.SetOutputDimensionality(2) 508 reslice.SetTransformInputSampling(True) 509 reslice.SetGenerateStencilOutput(False) 510 if border: 511 reslice.SetBorder(True) 512 reslice.SetBorderThickness(border) 513 else: 514 reslice.SetBorder(False) 515 if mode == "linear": 516 reslice.SetInterpolationModeToLinear() 517 elif mode == "nearest": 518 reslice.SetInterpolationModeToNearestNeighbor() 519 elif mode == "cubic": 520 reslice.SetInterpolationModeToCubic() 521 else: 522 vedo.logger.error(f"in slice_plane(): unknown interpolation mode {mode}") 523 raise ValueError() 524 reslice.SetAutoCropOutput(not autocrop) 525 reslice.Update() 526 img = reslice.GetOutput() 527 528 vslice = vtki.new("ImageDataGeometryFilter") 529 vslice.SetInputData(img) 530 vslice.Update() 531 532 msh = Mesh(vslice.GetOutput()).apply_transform(T) 533 msh.properties.LightingOff() 534 535 d0, d1, _ = img.GetDimensions() 536 varr1 = utils.numpy2vtk([d1, d0], name="shape") 537 varr2 = utils.numpy2vtk(img.GetBounds(), name="original_bounds") 538 msh.dataset.GetFieldData().AddArray(varr1) 539 msh.dataset.GetFieldData().AddArray(varr2) 540 msh.pipeline = utils.OperationNode("slice_plane", parents=[self], c="#4cc9f0:#e9c46a") 541 return msh 542 543 def slab(self, slice_range=(), axis='z', operation="mean") -> Mesh: 544 """ 545 Extract a slab from a `Volume` by combining 546 all of the slices of an image to create a single slice. 547 548 Returns a `Mesh` containing metadata which 549 can be accessed with e.g. `mesh.metadata["slab_range"]`. 550 551 Metadata: 552 slab_range : (list) 553 contains the range of slices extracted 554 slab_axis : (str) 555 contains the axis along which the slab was extracted 556 slab_operation : (str) 557 contains the operation performed on the slab 558 slab_bounding_box : (list) 559 contains the bounding box of the slab 560 561 Arguments: 562 slice_range : (list) 563 range of slices to extract 564 axis : (str) 565 axis along which to extract the slab 566 operation : (str) 567 operation to perform on the slab, 568 allowed values are: "sum", "min", "max", "mean". 569 570 Example: 571 - [slab.py](https://github.com/marcomusy/vedo/blob/master/examples/volumetric/slab_vol.py) 572 573 ![](https://vedo.embl.es/images/volumetric/slab_vol.jpg) 574 """ 575 if len(slice_range) != 2: 576 vedo.logger.error("in slab(): slice_range is empty or invalid") 577 raise ValueError() 578 579 islab = vtki.new("ImageSlab") 580 islab.SetInputData(self.dataset) 581 582 if operation in ["+", "add", "sum"]: 583 islab.SetOperationToSum() 584 elif "min" in operation: 585 islab.SetOperationToMin() 586 elif "max" in operation: 587 islab.SetOperationToMax() 588 elif "mean" in operation: 589 islab.SetOperationToMean() 590 else: 591 vedo.logger.error(f"in slab(): unknown operation {operation}") 592 raise ValueError() 593 594 dims = self.dimensions() 595 if axis == 'x': 596 islab.SetOrientationToX() 597 if slice_range[0] > dims[0]-1: 598 slice_range[0] = dims[0]-1 599 if slice_range[1] > dims[0]-1: 600 slice_range[1] = dims[0]-1 601 elif axis == 'y': 602 islab.SetOrientationToY() 603 if slice_range[0] > dims[1]-1: 604 slice_range[0] = dims[1]-1 605 if slice_range[1] > dims[1]-1: 606 slice_range[1] = dims[1]-1 607 elif axis == 'z': 608 islab.SetOrientationToZ() 609 if slice_range[0] > dims[2]-1: 610 slice_range[0] = dims[2]-1 611 if slice_range[1] > dims[2]-1: 612 slice_range[1] = dims[2]-1 613 else: 614 vedo.logger.error(f"Error in slab(): unknown axis {axis}") 615 raise RuntimeError() 616 617 islab.SetSliceRange(slice_range) 618 islab.Update() 619 620 msh = Mesh(islab.GetOutput()).lighting('off') 621 msh.mapper.SetLookupTable(utils.ctf2lut(self, msh)) 622 msh.mapper.SetScalarRange(self.scalar_range()) 623 624 msh.metadata["slab_range"] = slice_range 625 msh.metadata["slab_axis"] = axis 626 msh.metadata["slab_operation"] = operation 627 628 # compute bounds of slab 629 origin = list(self.origin()) 630 spacing = list(self.spacing()) 631 if axis == 'x': 632 msh.metadata["slab_bounding_box"] = [ 633 origin[0] + slice_range[0]*spacing[0], 634 origin[0] + slice_range[1]*spacing[0], 635 origin[1], 636 origin[1] + dims[1]*spacing[1], 637 origin[2], 638 origin[2] + dims[2]*spacing[2], 639 ] 640 elif axis == 'y': 641 msh.metadata["slab_bounding_box"] = [ 642 origin[0], 643 origin[0] + dims[0]*spacing[0], 644 origin[1] + slice_range[0]*spacing[1], 645 origin[1] + slice_range[1]*spacing[1], 646 origin[2], 647 origin[2] + dims[2]*spacing[2], 648 ] 649 elif axis == 'z': 650 msh.metadata["slab_bounding_box"] = [ 651 origin[0], 652 origin[0] + dims[0]*spacing[0], 653 origin[1], 654 origin[1] + dims[1]*spacing[1], 655 origin[2] + slice_range[0]*spacing[2], 656 origin[2] + slice_range[1]*spacing[2], 657 ] 658 659 msh.pipeline = utils.OperationNode( 660 f"slab{slice_range}", 661 comment=f"axis={axis}, operation={operation}", 662 parents=[self], 663 c="#4cc9f0:#e9c46a", 664 ) 665 msh.name = "SlabMesh" 666 return msh 667 668 669 def warp( 670 self, 671 source: Union["vedo.Points", List], 672 target: Union["vedo.Points", List], 673 sigma=1, mode="3d", fit=True, 674 ) -> Self: 675 """ 676 Warp volume scalars within a Volume by specifying 677 source and target sets of points. 678 679 Arguments: 680 source : (Points, list) 681 the list of source points 682 target : (Points, list) 683 the list of target points 684 fit : (bool) 685 fit/adapt the old bounding box to the warped geometry 686 """ 687 if isinstance(source, vedo.Points): 688 source = source.vertices 689 if isinstance(target, vedo.Points): 690 target = target.vertices 691 692 NLT = transformations.NonLinearTransform() 693 NLT.source_points = source 694 NLT.target_points = target 695 NLT.sigma = sigma 696 NLT.mode = mode 697 698 self.apply_transform(NLT, fit=fit) 699 self.pipeline = utils.OperationNode("warp", parents=[self], c="#4cc9f0") 700 return self 701 702 def apply_transform( 703 self, 704 T: Union[transformations.LinearTransform, transformations.NonLinearTransform], 705 fit=True, interpolation="cubic", 706 ) -> Self: 707 """ 708 Apply a transform to the scalars in the volume. 709 710 Arguments: 711 T : (LinearTransform, NonLinearTransform) 712 The transformation to be applied 713 fit : (bool) 714 fit/adapt the old bounding box to the modified geometry 715 interpolation : (str) 716 one of the following: "nearest", "linear", "cubic" 717 """ 718 if utils.is_sequence(T): 719 T = transformations.LinearTransform(T) 720 721 TI = T.compute_inverse() 722 723 reslice = vtki.new("ImageReslice") 724 reslice.SetInputData(self.dataset) 725 reslice.SetResliceTransform(TI.T) 726 reslice.SetOutputDimensionality(3) 727 if "lin" in interpolation.lower(): 728 reslice.SetInterpolationModeToLinear() 729 elif "near" in interpolation.lower(): 730 reslice.SetInterpolationModeToNearestNeighbor() 731 elif "cubic" in interpolation.lower(): 732 reslice.SetInterpolationModeToCubic() 733 else: 734 vedo.logger.error( 735 f"in apply_transform: unknown interpolation mode {interpolation}") 736 raise ValueError() 737 reslice.SetAutoCropOutput(fit) 738 reslice.Update() 739 self._update(reslice.GetOutput()) 740 self.transform = T 741 self.pipeline = utils.OperationNode( 742 "apply_transform", parents=[self], c="#4cc9f0") 743 return self 744 745 def imagedata(self) -> vtki.vtkImageData: 746 """ 747 DEPRECATED: 748 Use `Volume.dataset` instead. 749 750 Return the underlying `vtkImagaData` object. 751 """ 752 print("Volume.imagedata() is deprecated, use Volume.dataset instead") 753 return self.dataset 754 755 def modified(self) -> Self: 756 """ 757 Mark the object as modified. 758 759 Example: 760 761 - [numpy2volume0.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/numpy2volume0.py) 762 """ 763 scals = self.dataset.GetPointData().GetScalars() 764 if scals: 765 scals.Modified() 766 return self 767 768 def tonumpy(self) -> np.ndarray: 769 """ 770 Get read-write access to voxels of a Volume object as a numpy array. 771 772 When you set values in the output image, you don't want numpy to reallocate the array 773 but instead set values in the existing array, so use the [:] operator. 774 775 Example: 776 `arr[:] = arr*2 + 15` 777 778 If the array is modified add a call to: 779 `volume.modified()` 780 when all your modifications are completed. 781 """ 782 narray_shape = tuple(reversed(self.dataset.GetDimensions())) 783 784 scals = self.dataset.GetPointData().GetScalars() 785 comps = scals.GetNumberOfComponents() 786 if comps == 1: 787 narray = utils.vtk2numpy(scals).reshape(narray_shape) 788 narray = np.transpose(narray, axes=[2, 1, 0]) 789 else: 790 narray = utils.vtk2numpy(scals).reshape(*narray_shape, comps) 791 narray = np.transpose(narray, axes=[2, 1, 0, 3]) 792 793 # narray = utils.vtk2numpy(self.dataset.GetPointData().GetScalars()).reshape(narray_shape) 794 # narray = np.transpose(narray, axes=[2, 1, 0]) 795 return narray 796 797 @property 798 def shape(self) -> np.ndarray: 799 """Return the nr. of voxels in the 3 dimensions.""" 800 return np.array(self.dataset.GetDimensions()) 801 802 def dimensions(self) -> np.ndarray: 803 """Return the nr. of voxels in the 3 dimensions.""" 804 return np.array(self.dataset.GetDimensions()) 805 806 def scalar_range(self) -> np.ndarray: 807 """Return the range of the scalar values.""" 808 return np.array(self.dataset.GetScalarRange()) 809 810 def spacing(self, s=None) -> Union[Self, Iterable[float]]: 811 """Set/get the voxels size in the 3 dimensions.""" 812 if s is not None: 813 self.dataset.SetSpacing(s) 814 return self 815 return np.array(self.dataset.GetSpacing()) 816 817 def origin(self, s=None) -> Union[Self, Iterable[float]]: 818 """ 819 Set/get the origin of the volumetric dataset. 820 821 The origin is the position in world coordinates of the point index (0,0,0). 822 This point does not have to be part of the dataset, in other words, 823 the dataset extent does not have to start at (0,0,0) and the origin 824 can be outside of the dataset bounding box. 825 The origin plus spacing determine the position in space of the points. 826 """ 827 if s is not None: 828 self.dataset.SetOrigin(s) 829 return self 830 return np.array(self.dataset.GetOrigin()) 831 832 def pos(self, p=None) -> Union[Self, Iterable[float]]: 833 """Set/get the position of the volumetric dataset.""" 834 if p is not None: 835 self.origin(p) 836 return self 837 return self.origin() 838 839 def center(self) -> np.ndarray: 840 """Get the center of the volumetric dataset.""" 841 # note that this does not have the set method like origin and spacing 842 return np.array(self.dataset.GetCenter()) 843 844 def shift(self, s: list) -> Self: 845 """Shift the volumetric dataset by a vector.""" 846 self.origin(self.origin() + np.array(s)) 847 return self 848 849 def rotate_x(self, angle: float, rad=False, around=None) -> Self: 850 """ 851 Rotate around x-axis. If angle is in radians set `rad=True`. 852 853 Use `around` to define a pivoting point. 854 """ 855 if angle == 0: 856 return self 857 LT = transformations.LinearTransform().rotate_x(angle, rad, around) 858 return self.apply_transform(LT, fit=True, interpolation="linear") 859 860 def rotate_y(self, angle: float, rad=False, around=None) -> Self: 861 """ 862 Rotate around y-axis. If angle is in radians set `rad=True`. 863 864 Use `around` to define a pivoting point. 865 """ 866 if angle == 0: 867 return self 868 LT = transformations.LinearTransform().rotate_y(angle, rad, around) 869 return self.apply_transform(LT, fit=True, interpolation="linear") 870 871 def rotate_z(self, angle: float, rad=False, around=None) -> Self: 872 """ 873 Rotate around z-axis. If angle is in radians set `rad=True`. 874 875 Use `around` to define a pivoting point. 876 """ 877 if angle == 0: 878 return self 879 LT = transformations.LinearTransform().rotate_z(angle, rad, around) 880 return self.apply_transform(LT, fit=True, interpolation="linear") 881 882 def get_cell_from_ijk(self, ijk: list) -> int: 883 """ 884 Get the voxel id number at the given ijk coordinates. 885 886 Arguments: 887 ijk : (list) 888 the ijk coordinates of the voxel 889 """ 890 return self.dataset.ComputeCellId(ijk) 891 892 def get_point_from_ijk(self, ijk: list) -> int: 893 """ 894 Get the point id number at the given ijk coordinates. 895 896 Arguments: 897 ijk : (list) 898 the ijk coordinates of the voxel 899 """ 900 return self.dataset.ComputePointId(ijk) 901 902 def permute_axes(self, x: int, y: int, z: int) -> Self: 903 """ 904 Reorder the axes of the Volume by specifying 905 the input axes which are supposed to become the new X, Y, and Z. 906 """ 907 imp = vtki.new("ImagePermute") 908 imp.SetFilteredAxes(x, y, z) 909 imp.SetInputData(self.dataset) 910 imp.Update() 911 self._update(imp.GetOutput()) 912 self.pipeline = utils.OperationNode( 913 f"permute_axes({(x,y,z)})", parents=[self], c="#4cc9f0" 914 ) 915 return self 916 917 def resample(self, new_spacing: List[float], interpolation=1) -> Self: 918 """ 919 Resamples a `Volume` to be larger or smaller. 920 921 This method modifies the spacing of the input. 922 Linear interpolation is used to resample the data. 923 924 Arguments: 925 new_spacing : (list) 926 a list of 3 new spacings for the 3 axes 927 interpolation : (int) 928 0=nearest_neighbor, 1=linear, 2=cubic 929 """ 930 rsp = vtki.new("ImageResample") 931 oldsp = self.spacing() 932 for i in range(3): 933 if oldsp[i] != new_spacing[i]: 934 rsp.SetAxisOutputSpacing(i, new_spacing[i]) 935 rsp.InterpolateOn() 936 rsp.SetInterpolationMode(interpolation) 937 rsp.OptimizationOn() 938 rsp.Update() 939 self._update(rsp.GetOutput()) 940 self.pipeline = utils.OperationNode( 941 "resample", comment=f"spacing: {tuple(new_spacing)}", parents=[self], c="#4cc9f0" 942 ) 943 return self 944 945 946 def threshold(self, above=None, below=None, replace=None, replace_value=None) -> Self: 947 """ 948 Binary or continuous volume thresholding. 949 Find the voxels that contain a value above/below the input values 950 and replace them with a new value (default is 0). 951 """ 952 th = vtki.new("ImageThreshold") 953 th.SetInputData(self.dataset) 954 955 # sanity checks 956 if above is not None and below is not None: 957 if above == below: 958 return self 959 if above > below: 960 vedo.logger.warning("in volume.threshold(), above > below, skip.") 961 return self 962 963 ## cases 964 if below is not None and above is not None: 965 th.ThresholdBetween(above, below) 966 967 elif above is not None: 968 th.ThresholdByUpper(above) 969 970 elif below is not None: 971 th.ThresholdByLower(below) 972 973 ## 974 if replace is not None: 975 th.SetReplaceIn(True) 976 th.SetInValue(replace) 977 else: 978 th.SetReplaceIn(False) 979 980 if replace_value is not None: 981 th.SetReplaceOut(True) 982 th.SetOutValue(replace_value) 983 else: 984 th.SetReplaceOut(False) 985 986 th.Update() 987 self._update(th.GetOutput()) 988 self.pipeline = utils.OperationNode("threshold", parents=[self], c="#4cc9f0") 989 return self 990 991 def crop(self, left=None, right=None, back=None, front=None, bottom=None, top=None, VOI=()) -> Self: 992 """ 993 Crop a `Volume` object. 994 995 Arguments: 996 left : (float) 997 fraction to crop from the left plane (negative x) 998 right : (float) 999 fraction to crop from the right plane (positive x) 1000 back : (float) 1001 fraction to crop from the back plane (negative y) 1002 front : (float) 1003 fraction to crop from the front plane (positive y) 1004 bottom : (float) 1005 fraction to crop from the bottom plane (negative z) 1006 top : (float) 1007 fraction to crop from the top plane (positive z) 1008 VOI : (list) 1009 extract Volume Of Interest expressed in voxel numbers 1010 1011 Example: 1012 `vol.crop(VOI=(xmin, xmax, ymin, ymax, zmin, zmax)) # all integers nrs` 1013 """ 1014 extractVOI = vtki.new("ExtractVOI") 1015 extractVOI.SetInputData(self.dataset) 1016 1017 if VOI: 1018 extractVOI.SetVOI(VOI) 1019 else: 1020 d = self.dataset.GetDimensions() 1021 bx0, bx1, by0, by1, bz0, bz1 = 0, d[0]-1, 0, d[1]-1, 0, d[2]-1 1022 if left is not None: bx0 = int((d[0]-1)*left) 1023 if right is not None: bx1 = int((d[0]-1)*(1-right)) 1024 if back is not None: by0 = int((d[1]-1)*back) 1025 if front is not None: by1 = int((d[1]-1)*(1-front)) 1026 if bottom is not None: bz0 = int((d[2]-1)*bottom) 1027 if top is not None: bz1 = int((d[2]-1)*(1-top)) 1028 extractVOI.SetVOI(bx0, bx1, by0, by1, bz0, bz1) 1029 extractVOI.Update() 1030 self._update(extractVOI.GetOutput()) 1031 1032 self.pipeline = utils.OperationNode( 1033 "crop", parents=[self], c="#4cc9f0", comment=f"dims={tuple(self.dimensions())}" 1034 ) 1035 return self 1036 1037 def append(self, *volumes, axis="z", preserve_extents=False) -> Self: 1038 """ 1039 Take the components from multiple inputs and merges them into one output. 1040 Except for the append axis, all inputs must have the same extent. 1041 All inputs must have the same number of scalar components. 1042 The output has the same origin and spacing as the first input. 1043 The origin and spacing of all other inputs are ignored. 1044 All inputs must have the same scalar type. 1045 1046 Arguments: 1047 axis : (int, str) 1048 axis expanded to hold the multiple images 1049 preserve_extents : (bool) 1050 if True, the extent of the inputs is used to place 1051 the image in the output. The whole extent of the output is the union of the input 1052 whole extents. Any portion of the output not covered by the inputs is set to zero. 1053 The origin and spacing is taken from the first input. 1054 1055 Example: 1056 ```python 1057 from vedo import Volume, dataurl 1058 vol = Volume(dataurl+'embryo.tif') 1059 vol.append(vol, axis='x').show().close() 1060 ``` 1061 ![](https://vedo.embl.es/images/feats/volume_append.png) 1062 """ 1063 ima = vtki.new("ImageAppend") 1064 ima.SetInputData(self.dataset) 1065 # if not utils.is_sequence(volumes): 1066 # volumes = [volumes] 1067 for volume in volumes: 1068 if isinstance(volume, vtki.vtkImageData): 1069 ima.AddInputData(volume) 1070 else: 1071 ima.AddInputData(volume.dataset) 1072 ima.SetPreserveExtents(preserve_extents) 1073 if axis == "x": 1074 axis = 0 1075 elif axis == "y": 1076 axis = 1 1077 elif axis == "z": 1078 axis = 2 1079 ima.SetAppendAxis(axis) 1080 ima.Update() 1081 self._update(ima.GetOutput()) 1082 1083 self.pipeline = utils.OperationNode( 1084 "append", 1085 parents=[self, *volumes], 1086 c="#4cc9f0", 1087 comment=f"dims={tuple(self.dimensions())}", 1088 ) 1089 return self 1090 1091 def pad(self, voxels=10, value=0) -> Self: 1092 """ 1093 Add the specified number of voxels at the `Volume` borders. 1094 Voxels can be a list formatted as `[nx0, nx1, ny0, ny1, nz0, nz1]`. 1095 1096 Arguments: 1097 voxels : (int, list) 1098 number of voxels to be added (or a list of length 4) 1099 value : (int) 1100 intensity value (gray-scale color) of the padding 1101 1102 Example: 1103 ```python 1104 from vedo import Volume, dataurl, show 1105 iso = Volume(dataurl+'embryo.tif').isosurface() 1106 vol = iso.binarize(spacing=(100, 100, 100)).pad(10) 1107 vol.dilate([15,15,15]) 1108 show(iso, vol.isosurface(), N=2, axes=1) 1109 ``` 1110 ![](https://vedo.embl.es/images/volumetric/volume_pad.png) 1111 """ 1112 x0, x1, y0, y1, z0, z1 = self.dataset.GetExtent() 1113 pf = vtki.new("ImageConstantPad") 1114 pf.SetInputData(self.dataset) 1115 pf.SetConstant(value) 1116 if utils.is_sequence(voxels): 1117 pf.SetOutputWholeExtent( 1118 x0 - voxels[0], x1 + voxels[1], 1119 y0 - voxels[2], y1 + voxels[3], 1120 z0 - voxels[4], z1 + voxels[5], 1121 ) 1122 else: 1123 pf.SetOutputWholeExtent( 1124 x0 - voxels, x1 + voxels, 1125 y0 - voxels, y1 + voxels, 1126 z0 - voxels, z1 + voxels, 1127 ) 1128 pf.Update() 1129 self._update(pf.GetOutput()) 1130 self.pipeline = utils.OperationNode( 1131 "pad", comment=f"{voxels} voxels", parents=[self], c="#f28482" 1132 ) 1133 return self 1134 1135 def resize(self, newdims: List[int]) -> Self: 1136 """Increase or reduce the number of voxels of a Volume with interpolation.""" 1137 rsz = vtki.new("ImageResize") 1138 rsz.SetResizeMethodToOutputDimensions() 1139 rsz.SetInputData(self.dataset) 1140 rsz.SetOutputDimensions(newdims) 1141 rsz.Update() 1142 self.dataset = rsz.GetOutput() 1143 self._update(self.dataset) 1144 self.pipeline = utils.OperationNode( 1145 "resize", parents=[self], c="#4cc9f0", comment=f"dims={tuple(self.dimensions())}" 1146 ) 1147 return self 1148 1149 def normalize(self) -> Self: 1150 """Normalize that scalar components for each point.""" 1151 norm = vtki.new("ImageNormalize") 1152 norm.SetInputData(self.dataset) 1153 norm.Update() 1154 self._update(norm.GetOutput()) 1155 self.pipeline = utils.OperationNode("normalize", parents=[self], c="#4cc9f0") 1156 return self 1157 1158 def mirror(self, axis="x") -> Self: 1159 """ 1160 Mirror flip along one of the cartesian axes. 1161 """ 1162 img = self.dataset 1163 1164 ff = vtki.new("ImageFlip") 1165 ff.SetInputData(img) 1166 if axis.lower() == "x": 1167 ff.SetFilteredAxis(0) 1168 elif axis.lower() == "y": 1169 ff.SetFilteredAxis(1) 1170 elif axis.lower() == "z": 1171 ff.SetFilteredAxis(2) 1172 else: 1173 vedo.logger.error("mirror must be set to either x, y, z or n") 1174 raise RuntimeError() 1175 ff.Update() 1176 self._update(ff.GetOutput()) 1177 self.pipeline = utils.OperationNode(f"mirror {axis}", parents=[self], c="#4cc9f0") 1178 return self 1179 1180 def operation(self, operation: str, volume2=None) -> "Volume": 1181 """ 1182 Perform operations with `Volume` objects. 1183 Keyword `volume2` can be a constant `float`. 1184 1185 Possible operations are: 1186 ``` 1187 and, or, xor, nand, nor, not, 1188 +, -, /, 1/x, sin, cos, exp, log, 1189 abs, **2, sqrt, min, max, atan, atan2, median, 1190 mag, dot, gradient, divergence, laplacian. 1191 ``` 1192 1193 Example: 1194 ```py 1195 from vedo import Box, show 1196 vol1 = Box(size=(35,10, 5)).binarize() 1197 vol2 = Box(size=( 5,10,35)).binarize() 1198 vol = vol1.operation("xor", vol2) 1199 show([[vol1, vol2], 1200 ["vol1 xor vol2", vol]], 1201 N=2, axes=1, viewup="z", 1202 ).close() 1203 ``` 1204 1205 Note: 1206 For logic operations, the two volumes must have the same bounds. 1207 If they do not, a larger image is created to contain both and the 1208 volumes are resampled onto the larger image before the operation is 1209 performed. This can be slow and memory intensive. 1210 1211 See also: 1212 - [volume_operations.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/volume_operations.py) 1213 """ 1214 op = operation.lower() 1215 image1 = self.dataset 1216 1217 if op in ["and", "or", "xor", "nand", "nor"]: 1218 1219 if not np.allclose(image1.GetBounds(), volume2.dataset.GetBounds()): 1220 # create a larger image to contain both 1221 b1 = image1.GetBounds() 1222 b2 = volume2.dataset.GetBounds() 1223 b = [ 1224 min(b1[0], b2[0]), 1225 max(b1[1], b2[1]), 1226 min(b1[2], b2[2]), 1227 max(b1[3], b2[3]), 1228 min(b1[4], b2[4]), 1229 max(b1[5], b2[5]), 1230 ] 1231 dims1 = image1.GetDimensions() 1232 dims2 = volume2.dataset.GetDimensions() 1233 dims = [max(dims1[0], dims2[0]), max(dims1[1], dims2[1]), max(dims1[2], dims2[2])] 1234 1235 image = vtki.vtkImageData() 1236 image.SetDimensions(dims) 1237 spacing = ( 1238 (b[1] - b[0]) / dims[0], 1239 (b[3] - b[2]) / dims[1], 1240 (b[5] - b[4]) / dims[2], 1241 ) 1242 image.SetSpacing(spacing) 1243 image.SetOrigin((b[0], b[2], b[4])) 1244 image.AllocateScalars(vtki.VTK_UNSIGNED_CHAR, 1) 1245 image.GetPointData().GetScalars().FillComponent(0, 0) 1246 1247 interp1 = vtki.new("ImageReslice") 1248 interp1.SetInputData(image1) 1249 interp1.SetOutputExtent(image.GetExtent()) 1250 interp1.SetOutputOrigin(image.GetOrigin()) 1251 interp1.SetOutputSpacing(image.GetSpacing()) 1252 interp1.SetInterpolationModeToNearestNeighbor() 1253 interp1.Update() 1254 imageA = interp1.GetOutput() 1255 1256 interp2 = vtki.new("ImageReslice") 1257 interp2.SetInputData(volume2.dataset) 1258 interp2.SetOutputExtent(image.GetExtent()) 1259 interp2.SetOutputOrigin(image.GetOrigin()) 1260 interp2.SetOutputSpacing(image.GetSpacing()) 1261 interp2.SetInterpolationModeToNearestNeighbor() 1262 interp2.Update() 1263 imageB = interp2.GetOutput() 1264 1265 else: 1266 imageA = image1 1267 imageB = volume2.dataset 1268 1269 img_logic = vtki.new("ImageLogic") 1270 img_logic.SetInput1Data(imageA) 1271 img_logic.SetInput2Data(imageB) 1272 img_logic.SetOperation(["and", "or", "xor", "nand", "nor"].index(op)) 1273 img_logic.Update() 1274 1275 out_vol = Volume(img_logic.GetOutput()) 1276 out_vol.pipeline = utils.OperationNode( 1277 "operation", comment=f"{op}", parents=[self, volume2], c="#4cc9f0", shape="cylinder" 1278 ) 1279 return out_vol ###################################################### 1280 1281 if volume2 and isinstance(volume2, Volume): 1282 # assert image1.GetScalarType() == volume2.dataset.GetScalarType(), "volumes have different scalar types" 1283 # make sure they have the same bounds: 1284 assert np.allclose(image1.GetBounds(), volume2.dataset.GetBounds()), "volumes have different bounds" 1285 # make sure they have the same spacing: 1286 assert np.allclose(image1.GetSpacing(), volume2.dataset.GetSpacing()), "volumes have different spacing" 1287 # make sure they have the same origin: 1288 assert np.allclose(image1.GetOrigin(), volume2.dataset.GetOrigin()), "volumes have different origin" 1289 1290 mf = None 1291 if op in ["median"]: 1292 mf = vtki.new("ImageMedian3D") 1293 mf.SetInputData(image1) 1294 elif op in ["mag"]: 1295 mf = vtki.new("ImageMagnitude") 1296 mf.SetInputData(image1) 1297 elif op in ["dot"]: 1298 mf = vtki.new("ImageDotProduct") 1299 mf.SetInput1Data(image1) 1300 mf.SetInput2Data(volume2.dataset) 1301 elif op in ["grad", "gradient"]: 1302 mf = vtki.new("ImageGradient") 1303 mf.SetDimensionality(3) 1304 mf.SetInputData(image1) 1305 elif op in ["div", "divergence"]: 1306 mf = vtki.new("ImageDivergence") 1307 mf.SetInputData(image1) 1308 elif op in ["laplacian"]: 1309 mf = vtki.new("ImageLaplacian") 1310 mf.SetDimensionality(3) 1311 mf.SetInputData(image1) 1312 elif op in ["not"]: 1313 mf = vtki.new("ImageLogic") 1314 mf.SetInput1Data(image1) 1315 mf.SetOperation(4) 1316 1317 if mf is not None: 1318 mf.Update() 1319 vol = Volume(mf.GetOutput()) 1320 vol.pipeline = utils.OperationNode( 1321 "operation", comment=f"{op}", parents=[self], c="#4cc9f0", shape="cylinder" 1322 ) 1323 return vol ###################################################### 1324 1325 mat = vtki.new("ImageMathematics") 1326 mat.SetInput1Data(image1) 1327 1328 K = None 1329 1330 if utils.is_number(volume2): 1331 K = volume2 1332 mat.SetConstantK(K) 1333 mat.SetConstantC(K) 1334 1335 elif volume2 is not None: # assume image2 is a constant value 1336 mat.SetInput2Data(volume2.dataset) 1337 1338 # ########################### 1339 if op in ["+", "add", "plus"]: 1340 if K: 1341 mat.SetOperationToAddConstant() 1342 else: 1343 mat.SetOperationToAdd() 1344 1345 elif op in ["-", "subtract", "minus"]: 1346 if K: 1347 mat.SetConstantC(-float(K)) 1348 mat.SetOperationToAddConstant() 1349 else: 1350 mat.SetOperationToSubtract() 1351 1352 elif op in ["*", "multiply", "times"]: 1353 if K: 1354 mat.SetOperationToMultiplyByK() 1355 else: 1356 mat.SetOperationToMultiply() 1357 1358 elif op in ["/", "divide"]: 1359 if K: 1360 mat.SetConstantK(1.0 / K) 1361 mat.SetOperationToMultiplyByK() 1362 else: 1363 mat.SetOperationToDivide() 1364 1365 elif op in ["1/x", "invert"]: 1366 mat.SetOperationToInvert() 1367 elif op in ["sin"]: 1368 mat.SetOperationToSin() 1369 elif op in ["cos"]: 1370 mat.SetOperationToCos() 1371 elif op in ["exp"]: 1372 mat.SetOperationToExp() 1373 elif op in ["log"]: 1374 mat.SetOperationToLog() 1375 elif op in ["abs"]: 1376 mat.SetOperationToAbsoluteValue() 1377 elif op in ["**2", "square"]: 1378 mat.SetOperationToSquare() 1379 elif op in ["sqrt", "sqr"]: 1380 mat.SetOperationToSquareRoot() 1381 elif op in ["min"]: 1382 mat.SetOperationToMin() 1383 elif op in ["max"]: 1384 mat.SetOperationToMax() 1385 elif op in ["atan"]: 1386 mat.SetOperationToATAN() 1387 elif op in ["atan2"]: 1388 mat.SetOperationToATAN2() 1389 else: 1390 vedo.logger.error(f"unknown operation {operation}") 1391 raise RuntimeError() 1392 mat.Update() 1393 1394 self._update(mat.GetOutput()) 1395 1396 self.pipeline = utils.OperationNode( 1397 "operation", comment=f"{op}", parents=[self, volume2], shape="cylinder", c="#4cc9f0" 1398 ) 1399 return self 1400 1401 def frequency_pass_filter(self, low_cutoff=None, high_cutoff=None, order=1) -> Self: 1402 """ 1403 Low-pass and high-pass filtering become trivial in the frequency domain. 1404 A portion of the pixels/voxels are simply masked or attenuated. 1405 This function applies a high pass Butterworth filter that attenuates the 1406 frequency domain image. 1407 1408 The gradual attenuation of the filter is important. 1409 A simple high-pass filter would simply mask a set of pixels in the frequency domain, 1410 but the abrupt transition would cause a ringing effect in the spatial domain. 1411 1412 Arguments: 1413 low_cutoff : (list) 1414 the cutoff frequencies for x, y and z 1415 high_cutoff : (list) 1416 the cutoff frequencies for x, y and z 1417 order : (int) 1418 order determines sharpness of the cutoff curve 1419 """ 1420 # https://lorensen.github.io/VTKExamples/site/Cxx/ImageProcessing/IdealHighPass 1421 fft = vtki.new("ImageFFT") 1422 fft.SetInputData(self.dataset) 1423 fft.Update() 1424 out = fft.GetOutput() 1425 1426 if high_cutoff: 1427 blp = vtki.new("ImageButterworthLowPass") 1428 blp.SetInputData(out) 1429 blp.SetCutOff(high_cutoff) 1430 blp.SetOrder(order) 1431 blp.Update() 1432 out = blp.GetOutput() 1433 1434 if low_cutoff: 1435 bhp = vtki.new("ImageButterworthHighPass") 1436 bhp.SetInputData(out) 1437 bhp.SetCutOff(low_cutoff) 1438 bhp.SetOrder(order) 1439 bhp.Update() 1440 out = bhp.GetOutput() 1441 1442 rfft = vtki.new("ImageRFFT") 1443 rfft.SetInputData(out) 1444 rfft.Update() 1445 1446 ecomp = vtki.new("ImageExtractComponents") 1447 ecomp.SetInputData(rfft.GetOutput()) 1448 ecomp.SetComponents(0) 1449 ecomp.Update() 1450 self._update(ecomp.GetOutput()) 1451 self.pipeline = utils.OperationNode("frequency_pass_filter", parents=[self], c="#4cc9f0") 1452 return self 1453 1454 def smooth_gaussian(self, sigma=(2, 2, 2), radius=None) -> Self: 1455 """ 1456 Performs a convolution of the input Volume with a gaussian. 1457 1458 Arguments: 1459 sigma : (float, list) 1460 standard deviation(s) in voxel units. 1461 A list can be given to smooth in the three direction differently. 1462 radius : (float, list) 1463 radius factor(s) determine how far out the gaussian 1464 kernel will go before being clamped to zero. A list can be given too. 1465 """ 1466 gsf = vtki.new("ImageGaussianSmooth") 1467 gsf.SetDimensionality(3) 1468 gsf.SetInputData(self.dataset) 1469 if utils.is_sequence(sigma): 1470 gsf.SetStandardDeviations(sigma) 1471 else: 1472 gsf.SetStandardDeviation(sigma) 1473 if radius is not None: 1474 if utils.is_sequence(radius): 1475 gsf.SetRadiusFactors(radius) 1476 else: 1477 gsf.SetRadiusFactor(radius) 1478 gsf.Update() 1479 self._update(gsf.GetOutput()) 1480 self.pipeline = utils.OperationNode("smooth_gaussian", parents=[self], c="#4cc9f0") 1481 return self 1482 1483 def smooth_median(self, neighbours=(2, 2, 2)) -> Self: 1484 """ 1485 Median filter that replaces each pixel with the median value 1486 from a rectangular neighborhood around that pixel. 1487 """ 1488 imgm = vtki.new("ImageMedian3D") 1489 imgm.SetInputData(self.dataset) 1490 if utils.is_sequence(neighbours): 1491 imgm.SetKernelSize(neighbours[0], neighbours[1], neighbours[2]) 1492 else: 1493 imgm.SetKernelSize(neighbours, neighbours, neighbours) 1494 imgm.Update() 1495 self._update(imgm.GetOutput()) 1496 self.pipeline = utils.OperationNode("smooth_median", parents=[self], c="#4cc9f0") 1497 return self 1498 1499 def erode(self, neighbours=(2, 2, 2)) -> Self: 1500 """ 1501 Replace a voxel with the minimum over an ellipsoidal neighborhood of voxels. 1502 If `neighbours` of an axis is 1, no processing is done on that axis. 1503 1504 Examples: 1505 - [erode_dilate.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/erode_dilate.py) 1506 1507 ![](https://vedo.embl.es/images/volumetric/erode_dilate.png) 1508 """ 1509 ver = vtki.new("ImageContinuousErode3D") 1510 ver.SetInputData(self.dataset) 1511 ver.SetKernelSize(neighbours[0], neighbours[1], neighbours[2]) 1512 ver.Update() 1513 self._update(ver.GetOutput()) 1514 self.pipeline = utils.OperationNode("erode", parents=[self], c="#4cc9f0") 1515 return self 1516 1517 def dilate(self, neighbours=(2, 2, 2)) -> Self: 1518 """ 1519 Replace a voxel with the maximum over an ellipsoidal neighborhood of voxels. 1520 If `neighbours` of an axis is 1, no processing is done on that axis. 1521 1522 Check also `erode()` and `pad()`. 1523 1524 Examples: 1525 - [erode_dilate.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/erode_dilate.py) 1526 """ 1527 ver = vtki.new("ImageContinuousDilate3D") 1528 ver.SetInputData(self.dataset) 1529 ver.SetKernelSize(neighbours[0], neighbours[1], neighbours[2]) 1530 ver.Update() 1531 self._update(ver.GetOutput()) 1532 self.pipeline = utils.OperationNode("dilate", parents=[self], c="#4cc9f0") 1533 return self 1534 1535 def magnitude(self) -> Self: 1536 """Colapses components with magnitude function.""" 1537 imgm = vtki.new("ImageMagnitude") 1538 imgm.SetInputData(self.dataset) 1539 imgm.Update() 1540 self._update(imgm.GetOutput()) 1541 self.pipeline = utils.OperationNode("magnitude", parents=[self], c="#4cc9f0") 1542 return self 1543 1544 def topoints(self) -> "vedo.Points": 1545 """ 1546 Extract all image voxels as points. 1547 This function takes an input `Volume` and creates an `Mesh` 1548 that contains the points and the point attributes. 1549 1550 Examples: 1551 - [vol2points.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/vol2points.py) 1552 """ 1553 v2p = vtki.new("ImageToPoints") 1554 v2p.SetInputData(self.dataset) 1555 v2p.Update() 1556 mpts = vedo.Points(v2p.GetOutput()) 1557 mpts.pipeline = utils.OperationNode("topoints", parents=[self], c="#4cc9f0:#e9c46a") 1558 return mpts 1559 1560 def euclidean_distance(self, anisotropy=False, max_distance=None) -> "Volume": 1561 """ 1562 Implementation of the Euclidean DT (Distance Transform) using Saito's algorithm. 1563 The distance map produced contains the square of the Euclidean distance values. 1564 The algorithm has a O(n^(D+1)) complexity over n x n x...x n images in D dimensions. 1565 1566 Check out also: https://en.wikipedia.org/wiki/Distance_transform 1567 1568 Arguments: 1569 anisotropy : bool 1570 used to define whether Spacing should be used in the 1571 computation of the distances. 1572 max_distance : bool 1573 any distance bigger than max_distance will not be 1574 computed but set to this specified value instead. 1575 1576 Examples: 1577 - [euclidian_dist.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/euclidian_dist.py) 1578 """ 1579 euv = vtki.new("ImageEuclideanDistance") 1580 euv.SetInputData(self.dataset) 1581 euv.SetConsiderAnisotropy(anisotropy) 1582 if max_distance is not None: 1583 euv.InitializeOn() 1584 euv.SetMaximumDistance(max_distance) 1585 euv.SetAlgorithmToSaito() 1586 euv.Update() 1587 vol = Volume(euv.GetOutput()) 1588 vol.pipeline = utils.OperationNode("euclidean_distance", parents=[self], c="#4cc9f0") 1589 return vol 1590 1591 def correlation_with(self, vol2: "Volume", dim=2) -> "Volume": 1592 """ 1593 Find the correlation between two volumetric data sets. 1594 Keyword `dim` determines whether the correlation will be 3D, 2D or 1D. 1595 The default is a 2D Correlation. 1596 1597 The output size will match the size of the first input. 1598 The second input is considered the correlation kernel. 1599 """ 1600 imc = vtki.new("ImageCorrelation") 1601 imc.SetInput1Data(self.dataset) 1602 imc.SetInput2Data(vol2.dataset) 1603 imc.SetDimensionality(dim) 1604 imc.Update() 1605 vol = Volume(imc.GetOutput()) 1606 1607 vol.pipeline = utils.OperationNode("correlation_with", parents=[self, vol2], c="#4cc9f0") 1608 return vol 1609 1610 def scale_voxels(self, scale=1) -> Self: 1611 """Scale the voxel content by factor `scale`.""" 1612 rsl = vtki.new("ImageReslice") 1613 rsl.SetInputData(self.dataset) 1614 rsl.SetScalarScale(scale) 1615 rsl.Update() 1616 self._update(rsl.GetOutput()) 1617 self.pipeline = utils.OperationNode( 1618 "scale_voxels", comment=f"scale={scale}", parents=[self], c="#4cc9f0" 1619 ) 1620 return self
Class to describe dataset that are defined on "voxels", the 3D equivalent of 2D pixels.
38 def __init__( 39 self, 40 inputobj=None, 41 dims=None, 42 origin=None, 43 spacing=None, 44 ) -> None: 45 """ 46 This class can be initialized with a numpy object, 47 a `vtkImageData` or a list of 2D bmp files. 48 49 Arguments: 50 origin : (list) 51 set volume origin coordinates 52 spacing : (list) 53 voxel dimensions in x, y and z. 54 dims : (list) 55 specify the dimensions of the volume. 56 57 Example: 58 ```python 59 from vedo import Volume 60 vol = Volume("path/to/mydata/rec*.bmp") 61 vol.show() 62 ``` 63 64 Examples: 65 - [numpy2volume1.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/numpy2volume1.py) 66 67 ![](https://vedo.embl.es/images/volumetric/numpy2volume1.png) 68 69 - [read_volume2.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/read_volume2.py) 70 71 ![](https://vedo.embl.es/images/volumetric/read_volume2.png) 72 73 .. note:: 74 if a `list` of values is used for `alphas` this is interpreted 75 as a transfer function along the range of the scalar. 76 """ 77 super().__init__() 78 79 self.name = "Volume" 80 self.filename = "" 81 self.file_size = "" 82 83 self.info = {} 84 self.time = time.time() 85 86 self.actor = vtki.vtkVolume() 87 self.actor.retrieve_object = weak_ref_to(self) 88 self.properties = self.actor.GetProperty() 89 90 self.transform = None 91 self.point_locator = None 92 self.cell_locator = None 93 self.line_locator = None 94 95 ################### 96 if isinstance(inputobj, str): 97 if "https://" in inputobj: 98 inputobj = vedo.file_io.download(inputobj, verbose=False) # fpath 99 elif os.path.isfile(inputobj): 100 self.filename = inputobj 101 else: 102 inputobj = sorted(glob.glob(inputobj)) 103 104 ################### 105 inputtype = str(type(inputobj)) 106 107 # print('Volume inputtype', inputtype, c='b') 108 109 if inputobj is None: 110 img = vtki.vtkImageData() 111 112 elif utils.is_sequence(inputobj): 113 114 if isinstance(inputobj[0], str) and ".bmp" in inputobj[0].lower(): 115 # scan sequence of BMP files 116 ima = vtki.new("ImageAppend") 117 ima.SetAppendAxis(2) 118 pb = utils.ProgressBar(0, len(inputobj)) 119 for i in pb.range(): 120 f = inputobj[i] 121 if "_rec_spr" in f: # OPT specific 122 continue 123 picr = vtki.new("BMPReader") 124 picr.SetFileName(f) 125 picr.Update() 126 mgf = vtki.new("ImageMagnitude") 127 mgf.SetInputData(picr.GetOutput()) 128 mgf.Update() 129 ima.AddInputData(mgf.GetOutput()) 130 pb.print("loading...") 131 ima.Update() 132 img = ima.GetOutput() 133 134 else: 135 136 if len(inputobj.shape) == 1: 137 varr = utils.numpy2vtk(inputobj) 138 else: 139 varr = utils.numpy2vtk(inputobj.ravel(order="F")) 140 varr.SetName("input_scalars") 141 142 img = vtki.vtkImageData() 143 if dims is not None: 144 img.SetDimensions(dims[2], dims[1], dims[0]) 145 else: 146 if len(inputobj.shape) == 1: 147 vedo.logger.error("must set dimensions (dims keyword) in Volume") 148 raise RuntimeError() 149 img.SetDimensions(inputobj.shape) 150 img.GetPointData().AddArray(varr) 151 img.GetPointData().SetActiveScalars(varr.GetName()) 152 153 elif isinstance(inputobj, vtki.vtkImageData): 154 img = inputobj 155 156 elif isinstance(inputobj, str): 157 if "https://" in inputobj: 158 inputobj = vedo.file_io.download(inputobj, verbose=False) 159 img = vedo.file_io.loadImageData(inputobj) 160 self.filename = inputobj 161 162 else: 163 vedo.logger.error(f"cannot understand input type {inputtype}") 164 return 165 166 if dims is not None: 167 img.SetDimensions(dims) 168 169 if origin is not None: 170 img.SetOrigin(origin) 171 172 if spacing is not None: 173 img.SetSpacing(spacing) 174 175 self.dataset = img 176 self.transform = None 177 178 ##################################### 179 mapper = vtki.new("SmartVolumeMapper") 180 mapper.SetInputData(img) 181 self.actor.SetMapper(mapper) 182 183 if img.GetPointData().GetScalars(): 184 if img.GetPointData().GetScalars().GetNumberOfComponents() == 1: 185 self.properties.SetShade(True) 186 self.properties.SetInterpolationType(1) 187 self.cmap("RdBu_r") 188 self.alpha([0.0, 0.0, 0.2, 0.4, 0.8, 1.0]) 189 self.alpha_gradient(None) 190 self.properties.SetScalarOpacityUnitDistance(1.0) 191 192 self.pipeline = utils.OperationNode( 193 "Volume", comment=f"dims={tuple(self.dimensions())}", c="#4cc9f0" 194 ) 195 #######################################################################
This class can be initialized with a numpy object,
a vtkImageData
or a list of 2D bmp files.
Arguments:
- origin : (list) set volume origin coordinates
- spacing : (list) voxel dimensions in x, y and z.
- dims : (list) specify the dimensions of the volume.
Example:
from vedo import Volume vol = Volume("path/to/mydata/rec*.bmp") vol.show()
Examples:
if a list
of values is used for alphas
this is interpreted
as a transfer function along the range of the scalar.
197 @property 198 def mapper(self): 199 """Return the underlying `vtkMapper` object.""" 200 return self.actor.GetMapper()
Return the underlying vtkMapper
object.
230 def c(self, *args, **kwargs) -> Self: 231 """Deprecated. Use `Volume.cmap()` instead.""" 232 vedo.logger.warning("Volume.c() is deprecated, use Volume.cmap() instead") 233 return self.cmap(*args, **kwargs)
Deprecated. Use Volume.cmap()
instead.
372 def copy(self, deep=True) -> "Volume": 373 """Return a copy of the Volume. Alias of `clone()`.""" 374 return self.clone(deep=deep)
Return a copy of the Volume. Alias of clone()
.
376 def clone(self, deep=True) -> "Volume": 377 """Return a clone copy of the Volume. Alias of `copy()`.""" 378 if deep: 379 newimg = vtki.vtkImageData() 380 newimg.CopyStructure(self.dataset) 381 newimg.CopyAttributes(self.dataset) 382 newvol = Volume(newimg) 383 else: 384 newvol = Volume(self.dataset) 385 386 prop = vtki.vtkVolumeProperty() 387 prop.DeepCopy(self.properties) 388 newvol.actor.SetProperty(prop) 389 newvol.properties = prop 390 391 newvol.pipeline = utils.OperationNode("clone", parents=[self], c="#bbd0ff", shape="diamond") 392 return newvol
Return a clone copy of the Volume. Alias of copy()
.
394 def astype(self, dtype: Union[str, int]) -> Self: 395 """ 396 Reset the type of the scalars array. 397 398 Arguments: 399 dtype : (str) 400 the type of the scalars array in 401 ["int8", "uint8", "int16", "uint16", "int32", "uint32", "float32", "float64"] 402 """ 403 if dtype in ["int8", "uint8", "int16", "uint16", "int32", "uint32", "float32", "float64"]: 404 caster = vtki.new("ImageCast") 405 caster.SetInputData(self.dataset) 406 caster.SetOutputScalarType(int(vtki.array_types[dtype])) 407 caster.ClampOverflowOn() 408 caster.Update() 409 self._update(caster.GetOutput()) 410 self.pipeline = utils.OperationNode(f"astype({dtype})", parents=[self], c="#4cc9f0") 411 else: 412 vedo.logger.error(f"astype(): unknown type {dtype}") 413 raise ValueError() 414 return self
Reset the type of the scalars array.
Arguments:
- dtype : (str) the type of the scalars array in ["int8", "uint8", "int16", "uint16", "int32", "uint32", "float32", "float64"]
416 def component_weight(self, i: int, weight: float) -> Self: 417 """Set the scalar component weight in range [0,1].""" 418 self.properties.SetComponentWeight(i, weight) 419 return self
Set the scalar component weight in range [0,1].
421 def xslice(self, i: int) -> Mesh: 422 """Extract the slice at index `i` of volume along x-axis.""" 423 vslice = vtki.new("ImageDataGeometryFilter") 424 vslice.SetInputData(self.dataset) 425 nx, ny, nz = self.dataset.GetDimensions() 426 if i > nx - 1: 427 i = nx - 1 428 vslice.SetExtent(i, i, 0, ny, 0, nz) 429 vslice.Update() 430 m = Mesh(vslice.GetOutput()) 431 m.pipeline = utils.OperationNode(f"xslice {i}", parents=[self], c="#4cc9f0:#e9c46a") 432 return m
Extract the slice at index i
of volume along x-axis.
434 def yslice(self, j: int) -> Mesh: 435 """Extract the slice at index `j` of volume along y-axis.""" 436 vslice = vtki.new("ImageDataGeometryFilter") 437 vslice.SetInputData(self.dataset) 438 nx, ny, nz = self.dataset.GetDimensions() 439 if j > ny - 1: 440 j = ny - 1 441 vslice.SetExtent(0, nx, j, j, 0, nz) 442 vslice.Update() 443 m = Mesh(vslice.GetOutput()) 444 m.pipeline = utils.OperationNode(f"yslice {j}", parents=[self], c="#4cc9f0:#e9c46a") 445 return m
Extract the slice at index j
of volume along y-axis.
447 def zslice(self, k: int) -> Mesh: 448 """Extract the slice at index `i` of volume along z-axis.""" 449 vslice = vtki.new("ImageDataGeometryFilter") 450 vslice.SetInputData(self.dataset) 451 nx, ny, nz = self.dataset.GetDimensions() 452 if k > nz - 1: 453 k = nz - 1 454 vslice.SetExtent(0, nx, 0, ny, k, k) 455 vslice.Update() 456 m = Mesh(vslice.GetOutput()) 457 m.pipeline = utils.OperationNode(f"zslice {k}", parents=[self], c="#4cc9f0:#e9c46a") 458 return m
Extract the slice at index i
of volume along z-axis.
460 def slice_plane(self, origin: List[float], normal: List[float], autocrop=False, border=0.5, mode="linear") -> Mesh: 461 """ 462 Extract the slice along a given plane position and normal. 463 464 Two metadata arrays are added to the output Mesh: 465 - "shape" : contains the shape of the slice 466 - "original_bounds" : contains the original bounds of the slice 467 One can access them with e.g. `myslice.metadata["shape"]`. 468 469 Arguments: 470 origin : (list) 471 position of the plane 472 normal : (list) 473 normal to the plane 474 autocrop : (bool) 475 crop the output to the minimal possible size 476 border : (float) 477 add a border to the output slice 478 mode : (str) 479 interpolation mode, one of the following: "linear", "nearest", "cubic" 480 481 Example: 482 - [slice_plane1.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/slice_plane1.py) 483 484 ![](https://vedo.embl.es/images/volumetric/slicePlane1.gif) 485 486 - [slice_plane2.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/slice_plane2.py) 487 488 ![](https://vedo.embl.es/images/volumetric/slicePlane2.png) 489 490 - [slice_plane3.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/slice_plane3.py) 491 492 ![](https://vedo.embl.es/images/volumetric/slicePlane3.jpg) 493 """ 494 newaxis = utils.versor(normal) 495 pos = np.array(origin) 496 initaxis = (0, 0, 1) 497 crossvec = np.cross(initaxis, newaxis) 498 angle = np.arccos(np.dot(initaxis, newaxis)) 499 T = vtki.vtkTransform() 500 T.PostMultiply() 501 T.RotateWXYZ(np.rad2deg(angle), crossvec.tolist()) 502 T.Translate(pos.tolist()) 503 504 reslice = vtki.new("ImageReslice") 505 reslice.SetResliceAxes(T.GetMatrix()) 506 reslice.SetInputData(self.dataset) 507 reslice.SetOutputDimensionality(2) 508 reslice.SetTransformInputSampling(True) 509 reslice.SetGenerateStencilOutput(False) 510 if border: 511 reslice.SetBorder(True) 512 reslice.SetBorderThickness(border) 513 else: 514 reslice.SetBorder(False) 515 if mode == "linear": 516 reslice.SetInterpolationModeToLinear() 517 elif mode == "nearest": 518 reslice.SetInterpolationModeToNearestNeighbor() 519 elif mode == "cubic": 520 reslice.SetInterpolationModeToCubic() 521 else: 522 vedo.logger.error(f"in slice_plane(): unknown interpolation mode {mode}") 523 raise ValueError() 524 reslice.SetAutoCropOutput(not autocrop) 525 reslice.Update() 526 img = reslice.GetOutput() 527 528 vslice = vtki.new("ImageDataGeometryFilter") 529 vslice.SetInputData(img) 530 vslice.Update() 531 532 msh = Mesh(vslice.GetOutput()).apply_transform(T) 533 msh.properties.LightingOff() 534 535 d0, d1, _ = img.GetDimensions() 536 varr1 = utils.numpy2vtk([d1, d0], name="shape") 537 varr2 = utils.numpy2vtk(img.GetBounds(), name="original_bounds") 538 msh.dataset.GetFieldData().AddArray(varr1) 539 msh.dataset.GetFieldData().AddArray(varr2) 540 msh.pipeline = utils.OperationNode("slice_plane", parents=[self], c="#4cc9f0:#e9c46a") 541 return msh
Extract the slice along a given plane position and normal.
Two metadata arrays are added to the output Mesh:
- "shape" : contains the shape of the slice
- "original_bounds" : contains the original bounds of the slice
One can access them with e.g. myslice.metadata["shape"]
.
Arguments:
- origin : (list) position of the plane
- normal : (list) normal to the plane
- autocrop : (bool) crop the output to the minimal possible size
- border : (float) add a border to the output slice
- mode : (str) interpolation mode, one of the following: "linear", "nearest", "cubic"
Example:
543 def slab(self, slice_range=(), axis='z', operation="mean") -> Mesh: 544 """ 545 Extract a slab from a `Volume` by combining 546 all of the slices of an image to create a single slice. 547 548 Returns a `Mesh` containing metadata which 549 can be accessed with e.g. `mesh.metadata["slab_range"]`. 550 551 Metadata: 552 slab_range : (list) 553 contains the range of slices extracted 554 slab_axis : (str) 555 contains the axis along which the slab was extracted 556 slab_operation : (str) 557 contains the operation performed on the slab 558 slab_bounding_box : (list) 559 contains the bounding box of the slab 560 561 Arguments: 562 slice_range : (list) 563 range of slices to extract 564 axis : (str) 565 axis along which to extract the slab 566 operation : (str) 567 operation to perform on the slab, 568 allowed values are: "sum", "min", "max", "mean". 569 570 Example: 571 - [slab.py](https://github.com/marcomusy/vedo/blob/master/examples/volumetric/slab_vol.py) 572 573 ![](https://vedo.embl.es/images/volumetric/slab_vol.jpg) 574 """ 575 if len(slice_range) != 2: 576 vedo.logger.error("in slab(): slice_range is empty or invalid") 577 raise ValueError() 578 579 islab = vtki.new("ImageSlab") 580 islab.SetInputData(self.dataset) 581 582 if operation in ["+", "add", "sum"]: 583 islab.SetOperationToSum() 584 elif "min" in operation: 585 islab.SetOperationToMin() 586 elif "max" in operation: 587 islab.SetOperationToMax() 588 elif "mean" in operation: 589 islab.SetOperationToMean() 590 else: 591 vedo.logger.error(f"in slab(): unknown operation {operation}") 592 raise ValueError() 593 594 dims = self.dimensions() 595 if axis == 'x': 596 islab.SetOrientationToX() 597 if slice_range[0] > dims[0]-1: 598 slice_range[0] = dims[0]-1 599 if slice_range[1] > dims[0]-1: 600 slice_range[1] = dims[0]-1 601 elif axis == 'y': 602 islab.SetOrientationToY() 603 if slice_range[0] > dims[1]-1: 604 slice_range[0] = dims[1]-1 605 if slice_range[1] > dims[1]-1: 606 slice_range[1] = dims[1]-1 607 elif axis == 'z': 608 islab.SetOrientationToZ() 609 if slice_range[0] > dims[2]-1: 610 slice_range[0] = dims[2]-1 611 if slice_range[1] > dims[2]-1: 612 slice_range[1] = dims[2]-1 613 else: 614 vedo.logger.error(f"Error in slab(): unknown axis {axis}") 615 raise RuntimeError() 616 617 islab.SetSliceRange(slice_range) 618 islab.Update() 619 620 msh = Mesh(islab.GetOutput()).lighting('off') 621 msh.mapper.SetLookupTable(utils.ctf2lut(self, msh)) 622 msh.mapper.SetScalarRange(self.scalar_range()) 623 624 msh.metadata["slab_range"] = slice_range 625 msh.metadata["slab_axis"] = axis 626 msh.metadata["slab_operation"] = operation 627 628 # compute bounds of slab 629 origin = list(self.origin()) 630 spacing = list(self.spacing()) 631 if axis == 'x': 632 msh.metadata["slab_bounding_box"] = [ 633 origin[0] + slice_range[0]*spacing[0], 634 origin[0] + slice_range[1]*spacing[0], 635 origin[1], 636 origin[1] + dims[1]*spacing[1], 637 origin[2], 638 origin[2] + dims[2]*spacing[2], 639 ] 640 elif axis == 'y': 641 msh.metadata["slab_bounding_box"] = [ 642 origin[0], 643 origin[0] + dims[0]*spacing[0], 644 origin[1] + slice_range[0]*spacing[1], 645 origin[1] + slice_range[1]*spacing[1], 646 origin[2], 647 origin[2] + dims[2]*spacing[2], 648 ] 649 elif axis == 'z': 650 msh.metadata["slab_bounding_box"] = [ 651 origin[0], 652 origin[0] + dims[0]*spacing[0], 653 origin[1], 654 origin[1] + dims[1]*spacing[1], 655 origin[2] + slice_range[0]*spacing[2], 656 origin[2] + slice_range[1]*spacing[2], 657 ] 658 659 msh.pipeline = utils.OperationNode( 660 f"slab{slice_range}", 661 comment=f"axis={axis}, operation={operation}", 662 parents=[self], 663 c="#4cc9f0:#e9c46a", 664 ) 665 msh.name = "SlabMesh" 666 return msh
Extract a slab from a Volume
by combining
all of the slices of an image to create a single slice.
Returns a Mesh
containing metadata which
can be accessed with e.g. mesh.metadata["slab_range"]
.
Metadata:
slab_range : (list) contains the range of slices extracted slab_axis : (str) contains the axis along which the slab was extracted slab_operation : (str) contains the operation performed on the slab slab_bounding_box : (list) contains the bounding box of the slab
Arguments:
- slice_range : (list) range of slices to extract
- axis : (str) axis along which to extract the slab
- operation : (str) operation to perform on the slab, allowed values are: "sum", "min", "max", "mean".
Example:
669 def warp( 670 self, 671 source: Union["vedo.Points", List], 672 target: Union["vedo.Points", List], 673 sigma=1, mode="3d", fit=True, 674 ) -> Self: 675 """ 676 Warp volume scalars within a Volume by specifying 677 source and target sets of points. 678 679 Arguments: 680 source : (Points, list) 681 the list of source points 682 target : (Points, list) 683 the list of target points 684 fit : (bool) 685 fit/adapt the old bounding box to the warped geometry 686 """ 687 if isinstance(source, vedo.Points): 688 source = source.vertices 689 if isinstance(target, vedo.Points): 690 target = target.vertices 691 692 NLT = transformations.NonLinearTransform() 693 NLT.source_points = source 694 NLT.target_points = target 695 NLT.sigma = sigma 696 NLT.mode = mode 697 698 self.apply_transform(NLT, fit=fit) 699 self.pipeline = utils.OperationNode("warp", parents=[self], c="#4cc9f0") 700 return self
Warp volume scalars within a Volume by specifying source and target sets of points.
Arguments:
- source : (Points, list) the list of source points
- target : (Points, list) the list of target points
- fit : (bool) fit/adapt the old bounding box to the warped geometry
702 def apply_transform( 703 self, 704 T: Union[transformations.LinearTransform, transformations.NonLinearTransform], 705 fit=True, interpolation="cubic", 706 ) -> Self: 707 """ 708 Apply a transform to the scalars in the volume. 709 710 Arguments: 711 T : (LinearTransform, NonLinearTransform) 712 The transformation to be applied 713 fit : (bool) 714 fit/adapt the old bounding box to the modified geometry 715 interpolation : (str) 716 one of the following: "nearest", "linear", "cubic" 717 """ 718 if utils.is_sequence(T): 719 T = transformations.LinearTransform(T) 720 721 TI = T.compute_inverse() 722 723 reslice = vtki.new("ImageReslice") 724 reslice.SetInputData(self.dataset) 725 reslice.SetResliceTransform(TI.T) 726 reslice.SetOutputDimensionality(3) 727 if "lin" in interpolation.lower(): 728 reslice.SetInterpolationModeToLinear() 729 elif "near" in interpolation.lower(): 730 reslice.SetInterpolationModeToNearestNeighbor() 731 elif "cubic" in interpolation.lower(): 732 reslice.SetInterpolationModeToCubic() 733 else: 734 vedo.logger.error( 735 f"in apply_transform: unknown interpolation mode {interpolation}") 736 raise ValueError() 737 reslice.SetAutoCropOutput(fit) 738 reslice.Update() 739 self._update(reslice.GetOutput()) 740 self.transform = T 741 self.pipeline = utils.OperationNode( 742 "apply_transform", parents=[self], c="#4cc9f0") 743 return self
Apply a transform to the scalars in the volume.
Arguments:
- T : (LinearTransform, NonLinearTransform) The transformation to be applied
- fit : (bool) fit/adapt the old bounding box to the modified geometry
- interpolation : (str) one of the following: "nearest", "linear", "cubic"
745 def imagedata(self) -> vtki.vtkImageData: 746 """ 747 DEPRECATED: 748 Use `Volume.dataset` instead. 749 750 Return the underlying `vtkImagaData` object. 751 """ 752 print("Volume.imagedata() is deprecated, use Volume.dataset instead") 753 return self.dataset
DEPRECATED:
Use Volume.dataset
instead.
Return the underlying vtkImagaData
object.
755 def modified(self) -> Self: 756 """ 757 Mark the object as modified. 758 759 Example: 760 761 - [numpy2volume0.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/numpy2volume0.py) 762 """ 763 scals = self.dataset.GetPointData().GetScalars() 764 if scals: 765 scals.Modified() 766 return self
768 def tonumpy(self) -> np.ndarray: 769 """ 770 Get read-write access to voxels of a Volume object as a numpy array. 771 772 When you set values in the output image, you don't want numpy to reallocate the array 773 but instead set values in the existing array, so use the [:] operator. 774 775 Example: 776 `arr[:] = arr*2 + 15` 777 778 If the array is modified add a call to: 779 `volume.modified()` 780 when all your modifications are completed. 781 """ 782 narray_shape = tuple(reversed(self.dataset.GetDimensions())) 783 784 scals = self.dataset.GetPointData().GetScalars() 785 comps = scals.GetNumberOfComponents() 786 if comps == 1: 787 narray = utils.vtk2numpy(scals).reshape(narray_shape) 788 narray = np.transpose(narray, axes=[2, 1, 0]) 789 else: 790 narray = utils.vtk2numpy(scals).reshape(*narray_shape, comps) 791 narray = np.transpose(narray, axes=[2, 1, 0, 3]) 792 793 # narray = utils.vtk2numpy(self.dataset.GetPointData().GetScalars()).reshape(narray_shape) 794 # narray = np.transpose(narray, axes=[2, 1, 0]) 795 return narray
Get read-write access to voxels of a Volume object as a numpy array.
When you set values in the output image, you don't want numpy to reallocate the array but instead set values in the existing array, so use the [:] operator.
Example:
arr[:] = arr*2 + 15
If the array is modified add a call to:
volume.modified()
when all your modifications are completed.
797 @property 798 def shape(self) -> np.ndarray: 799 """Return the nr. of voxels in the 3 dimensions.""" 800 return np.array(self.dataset.GetDimensions())
Return the nr. of voxels in the 3 dimensions.
802 def dimensions(self) -> np.ndarray: 803 """Return the nr. of voxels in the 3 dimensions.""" 804 return np.array(self.dataset.GetDimensions())
Return the nr. of voxels in the 3 dimensions.
806 def scalar_range(self) -> np.ndarray: 807 """Return the range of the scalar values.""" 808 return np.array(self.dataset.GetScalarRange())
Return the range of the scalar values.
810 def spacing(self, s=None) -> Union[Self, Iterable[float]]: 811 """Set/get the voxels size in the 3 dimensions.""" 812 if s is not None: 813 self.dataset.SetSpacing(s) 814 return self 815 return np.array(self.dataset.GetSpacing())
Set/get the voxels size in the 3 dimensions.
817 def origin(self, s=None) -> Union[Self, Iterable[float]]: 818 """ 819 Set/get the origin of the volumetric dataset. 820 821 The origin is the position in world coordinates of the point index (0,0,0). 822 This point does not have to be part of the dataset, in other words, 823 the dataset extent does not have to start at (0,0,0) and the origin 824 can be outside of the dataset bounding box. 825 The origin plus spacing determine the position in space of the points. 826 """ 827 if s is not None: 828 self.dataset.SetOrigin(s) 829 return self 830 return np.array(self.dataset.GetOrigin())
Set/get the origin of the volumetric dataset.
The origin is the position in world coordinates of the point index (0,0,0). This point does not have to be part of the dataset, in other words, the dataset extent does not have to start at (0,0,0) and the origin can be outside of the dataset bounding box. The origin plus spacing determine the position in space of the points.
832 def pos(self, p=None) -> Union[Self, Iterable[float]]: 833 """Set/get the position of the volumetric dataset.""" 834 if p is not None: 835 self.origin(p) 836 return self 837 return self.origin()
Set/get the position of the volumetric dataset.
839 def center(self) -> np.ndarray: 840 """Get the center of the volumetric dataset.""" 841 # note that this does not have the set method like origin and spacing 842 return np.array(self.dataset.GetCenter())
Get the center of the volumetric dataset.
844 def shift(self, s: list) -> Self: 845 """Shift the volumetric dataset by a vector.""" 846 self.origin(self.origin() + np.array(s)) 847 return self
Shift the volumetric dataset by a vector.
849 def rotate_x(self, angle: float, rad=False, around=None) -> Self: 850 """ 851 Rotate around x-axis. If angle is in radians set `rad=True`. 852 853 Use `around` to define a pivoting point. 854 """ 855 if angle == 0: 856 return self 857 LT = transformations.LinearTransform().rotate_x(angle, rad, around) 858 return self.apply_transform(LT, fit=True, interpolation="linear")
Rotate around x-axis. If angle is in radians set rad=True
.
Use around
to define a pivoting point.
860 def rotate_y(self, angle: float, rad=False, around=None) -> Self: 861 """ 862 Rotate around y-axis. If angle is in radians set `rad=True`. 863 864 Use `around` to define a pivoting point. 865 """ 866 if angle == 0: 867 return self 868 LT = transformations.LinearTransform().rotate_y(angle, rad, around) 869 return self.apply_transform(LT, fit=True, interpolation="linear")
Rotate around y-axis. If angle is in radians set rad=True
.
Use around
to define a pivoting point.
871 def rotate_z(self, angle: float, rad=False, around=None) -> Self: 872 """ 873 Rotate around z-axis. If angle is in radians set `rad=True`. 874 875 Use `around` to define a pivoting point. 876 """ 877 if angle == 0: 878 return self 879 LT = transformations.LinearTransform().rotate_z(angle, rad, around) 880 return self.apply_transform(LT, fit=True, interpolation="linear")
Rotate around z-axis. If angle is in radians set rad=True
.
Use around
to define a pivoting point.
882 def get_cell_from_ijk(self, ijk: list) -> int: 883 """ 884 Get the voxel id number at the given ijk coordinates. 885 886 Arguments: 887 ijk : (list) 888 the ijk coordinates of the voxel 889 """ 890 return self.dataset.ComputeCellId(ijk)
Get the voxel id number at the given ijk coordinates.
Arguments:
- ijk : (list) the ijk coordinates of the voxel
892 def get_point_from_ijk(self, ijk: list) -> int: 893 """ 894 Get the point id number at the given ijk coordinates. 895 896 Arguments: 897 ijk : (list) 898 the ijk coordinates of the voxel 899 """ 900 return self.dataset.ComputePointId(ijk)
Get the point id number at the given ijk coordinates.
Arguments:
- ijk : (list) the ijk coordinates of the voxel
902 def permute_axes(self, x: int, y: int, z: int) -> Self: 903 """ 904 Reorder the axes of the Volume by specifying 905 the input axes which are supposed to become the new X, Y, and Z. 906 """ 907 imp = vtki.new("ImagePermute") 908 imp.SetFilteredAxes(x, y, z) 909 imp.SetInputData(self.dataset) 910 imp.Update() 911 self._update(imp.GetOutput()) 912 self.pipeline = utils.OperationNode( 913 f"permute_axes({(x,y,z)})", parents=[self], c="#4cc9f0" 914 ) 915 return self
Reorder the axes of the Volume by specifying the input axes which are supposed to become the new X, Y, and Z.
917 def resample(self, new_spacing: List[float], interpolation=1) -> Self: 918 """ 919 Resamples a `Volume` to be larger or smaller. 920 921 This method modifies the spacing of the input. 922 Linear interpolation is used to resample the data. 923 924 Arguments: 925 new_spacing : (list) 926 a list of 3 new spacings for the 3 axes 927 interpolation : (int) 928 0=nearest_neighbor, 1=linear, 2=cubic 929 """ 930 rsp = vtki.new("ImageResample") 931 oldsp = self.spacing() 932 for i in range(3): 933 if oldsp[i] != new_spacing[i]: 934 rsp.SetAxisOutputSpacing(i, new_spacing[i]) 935 rsp.InterpolateOn() 936 rsp.SetInterpolationMode(interpolation) 937 rsp.OptimizationOn() 938 rsp.Update() 939 self._update(rsp.GetOutput()) 940 self.pipeline = utils.OperationNode( 941 "resample", comment=f"spacing: {tuple(new_spacing)}", parents=[self], c="#4cc9f0" 942 ) 943 return self
Resamples a Volume
to be larger or smaller.
This method modifies the spacing of the input. Linear interpolation is used to resample the data.
Arguments:
- new_spacing : (list) a list of 3 new spacings for the 3 axes
- interpolation : (int) 0=nearest_neighbor, 1=linear, 2=cubic
946 def threshold(self, above=None, below=None, replace=None, replace_value=None) -> Self: 947 """ 948 Binary or continuous volume thresholding. 949 Find the voxels that contain a value above/below the input values 950 and replace them with a new value (default is 0). 951 """ 952 th = vtki.new("ImageThreshold") 953 th.SetInputData(self.dataset) 954 955 # sanity checks 956 if above is not None and below is not None: 957 if above == below: 958 return self 959 if above > below: 960 vedo.logger.warning("in volume.threshold(), above > below, skip.") 961 return self 962 963 ## cases 964 if below is not None and above is not None: 965 th.ThresholdBetween(above, below) 966 967 elif above is not None: 968 th.ThresholdByUpper(above) 969 970 elif below is not None: 971 th.ThresholdByLower(below) 972 973 ## 974 if replace is not None: 975 th.SetReplaceIn(True) 976 th.SetInValue(replace) 977 else: 978 th.SetReplaceIn(False) 979 980 if replace_value is not None: 981 th.SetReplaceOut(True) 982 th.SetOutValue(replace_value) 983 else: 984 th.SetReplaceOut(False) 985 986 th.Update() 987 self._update(th.GetOutput()) 988 self.pipeline = utils.OperationNode("threshold", parents=[self], c="#4cc9f0") 989 return self
Binary or continuous volume thresholding. Find the voxels that contain a value above/below the input values and replace them with a new value (default is 0).
991 def crop(self, left=None, right=None, back=None, front=None, bottom=None, top=None, VOI=()) -> Self: 992 """ 993 Crop a `Volume` object. 994 995 Arguments: 996 left : (float) 997 fraction to crop from the left plane (negative x) 998 right : (float) 999 fraction to crop from the right plane (positive x) 1000 back : (float) 1001 fraction to crop from the back plane (negative y) 1002 front : (float) 1003 fraction to crop from the front plane (positive y) 1004 bottom : (float) 1005 fraction to crop from the bottom plane (negative z) 1006 top : (float) 1007 fraction to crop from the top plane (positive z) 1008 VOI : (list) 1009 extract Volume Of Interest expressed in voxel numbers 1010 1011 Example: 1012 `vol.crop(VOI=(xmin, xmax, ymin, ymax, zmin, zmax)) # all integers nrs` 1013 """ 1014 extractVOI = vtki.new("ExtractVOI") 1015 extractVOI.SetInputData(self.dataset) 1016 1017 if VOI: 1018 extractVOI.SetVOI(VOI) 1019 else: 1020 d = self.dataset.GetDimensions() 1021 bx0, bx1, by0, by1, bz0, bz1 = 0, d[0]-1, 0, d[1]-1, 0, d[2]-1 1022 if left is not None: bx0 = int((d[0]-1)*left) 1023 if right is not None: bx1 = int((d[0]-1)*(1-right)) 1024 if back is not None: by0 = int((d[1]-1)*back) 1025 if front is not None: by1 = int((d[1]-1)*(1-front)) 1026 if bottom is not None: bz0 = int((d[2]-1)*bottom) 1027 if top is not None: bz1 = int((d[2]-1)*(1-top)) 1028 extractVOI.SetVOI(bx0, bx1, by0, by1, bz0, bz1) 1029 extractVOI.Update() 1030 self._update(extractVOI.GetOutput()) 1031 1032 self.pipeline = utils.OperationNode( 1033 "crop", parents=[self], c="#4cc9f0", comment=f"dims={tuple(self.dimensions())}" 1034 ) 1035 return self
Crop a Volume
object.
Arguments:
- left : (float) fraction to crop from the left plane (negative x)
- right : (float) fraction to crop from the right plane (positive x)
- back : (float) fraction to crop from the back plane (negative y)
- front : (float) fraction to crop from the front plane (positive y)
- bottom : (float) fraction to crop from the bottom plane (negative z)
- top : (float) fraction to crop from the top plane (positive z)
- VOI : (list) extract Volume Of Interest expressed in voxel numbers
Example:
vol.crop(VOI=(xmin, xmax, ymin, ymax, zmin, zmax)) # all integers nrs
1037 def append(self, *volumes, axis="z", preserve_extents=False) -> Self: 1038 """ 1039 Take the components from multiple inputs and merges them into one output. 1040 Except for the append axis, all inputs must have the same extent. 1041 All inputs must have the same number of scalar components. 1042 The output has the same origin and spacing as the first input. 1043 The origin and spacing of all other inputs are ignored. 1044 All inputs must have the same scalar type. 1045 1046 Arguments: 1047 axis : (int, str) 1048 axis expanded to hold the multiple images 1049 preserve_extents : (bool) 1050 if True, the extent of the inputs is used to place 1051 the image in the output. The whole extent of the output is the union of the input 1052 whole extents. Any portion of the output not covered by the inputs is set to zero. 1053 The origin and spacing is taken from the first input. 1054 1055 Example: 1056 ```python 1057 from vedo import Volume, dataurl 1058 vol = Volume(dataurl+'embryo.tif') 1059 vol.append(vol, axis='x').show().close() 1060 ``` 1061 ![](https://vedo.embl.es/images/feats/volume_append.png) 1062 """ 1063 ima = vtki.new("ImageAppend") 1064 ima.SetInputData(self.dataset) 1065 # if not utils.is_sequence(volumes): 1066 # volumes = [volumes] 1067 for volume in volumes: 1068 if isinstance(volume, vtki.vtkImageData): 1069 ima.AddInputData(volume) 1070 else: 1071 ima.AddInputData(volume.dataset) 1072 ima.SetPreserveExtents(preserve_extents) 1073 if axis == "x": 1074 axis = 0 1075 elif axis == "y": 1076 axis = 1 1077 elif axis == "z": 1078 axis = 2 1079 ima.SetAppendAxis(axis) 1080 ima.Update() 1081 self._update(ima.GetOutput()) 1082 1083 self.pipeline = utils.OperationNode( 1084 "append", 1085 parents=[self, *volumes], 1086 c="#4cc9f0", 1087 comment=f"dims={tuple(self.dimensions())}", 1088 ) 1089 return self
Take the components from multiple inputs and merges them into one output. Except for the append axis, all inputs must have the same extent. All inputs must have the same number of scalar components. The output has the same origin and spacing as the first input. The origin and spacing of all other inputs are ignored. All inputs must have the same scalar type.
Arguments:
- axis : (int, str) axis expanded to hold the multiple images
- preserve_extents : (bool) if True, the extent of the inputs is used to place the image in the output. The whole extent of the output is the union of the input whole extents. Any portion of the output not covered by the inputs is set to zero. The origin and spacing is taken from the first input.
Example:
from vedo import Volume, dataurl vol = Volume(dataurl+'embryo.tif') vol.append(vol, axis='x').show().close()
1091 def pad(self, voxels=10, value=0) -> Self: 1092 """ 1093 Add the specified number of voxels at the `Volume` borders. 1094 Voxels can be a list formatted as `[nx0, nx1, ny0, ny1, nz0, nz1]`. 1095 1096 Arguments: 1097 voxels : (int, list) 1098 number of voxels to be added (or a list of length 4) 1099 value : (int) 1100 intensity value (gray-scale color) of the padding 1101 1102 Example: 1103 ```python 1104 from vedo import Volume, dataurl, show 1105 iso = Volume(dataurl+'embryo.tif').isosurface() 1106 vol = iso.binarize(spacing=(100, 100, 100)).pad(10) 1107 vol.dilate([15,15,15]) 1108 show(iso, vol.isosurface(), N=2, axes=1) 1109 ``` 1110 ![](https://vedo.embl.es/images/volumetric/volume_pad.png) 1111 """ 1112 x0, x1, y0, y1, z0, z1 = self.dataset.GetExtent() 1113 pf = vtki.new("ImageConstantPad") 1114 pf.SetInputData(self.dataset) 1115 pf.SetConstant(value) 1116 if utils.is_sequence(voxels): 1117 pf.SetOutputWholeExtent( 1118 x0 - voxels[0], x1 + voxels[1], 1119 y0 - voxels[2], y1 + voxels[3], 1120 z0 - voxels[4], z1 + voxels[5], 1121 ) 1122 else: 1123 pf.SetOutputWholeExtent( 1124 x0 - voxels, x1 + voxels, 1125 y0 - voxels, y1 + voxels, 1126 z0 - voxels, z1 + voxels, 1127 ) 1128 pf.Update() 1129 self._update(pf.GetOutput()) 1130 self.pipeline = utils.OperationNode( 1131 "pad", comment=f"{voxels} voxels", parents=[self], c="#f28482" 1132 ) 1133 return self
Add the specified number of voxels at the Volume
borders.
Voxels can be a list formatted as [nx0, nx1, ny0, ny1, nz0, nz1]
.
Arguments:
- voxels : (int, list) number of voxels to be added (or a list of length 4)
- value : (int) intensity value (gray-scale color) of the padding
Example:
from vedo import Volume, dataurl, show iso = Volume(dataurl+'embryo.tif').isosurface() vol = iso.binarize(spacing=(100, 100, 100)).pad(10) vol.dilate([15,15,15]) show(iso, vol.isosurface(), N=2, axes=1)
1135 def resize(self, newdims: List[int]) -> Self: 1136 """Increase or reduce the number of voxels of a Volume with interpolation.""" 1137 rsz = vtki.new("ImageResize") 1138 rsz.SetResizeMethodToOutputDimensions() 1139 rsz.SetInputData(self.dataset) 1140 rsz.SetOutputDimensions(newdims) 1141 rsz.Update() 1142 self.dataset = rsz.GetOutput() 1143 self._update(self.dataset) 1144 self.pipeline = utils.OperationNode( 1145 "resize", parents=[self], c="#4cc9f0", comment=f"dims={tuple(self.dimensions())}" 1146 ) 1147 return self
Increase or reduce the number of voxels of a Volume with interpolation.
1149 def normalize(self) -> Self: 1150 """Normalize that scalar components for each point.""" 1151 norm = vtki.new("ImageNormalize") 1152 norm.SetInputData(self.dataset) 1153 norm.Update() 1154 self._update(norm.GetOutput()) 1155 self.pipeline = utils.OperationNode("normalize", parents=[self], c="#4cc9f0") 1156 return self
Normalize that scalar components for each point.
1158 def mirror(self, axis="x") -> Self: 1159 """ 1160 Mirror flip along one of the cartesian axes. 1161 """ 1162 img = self.dataset 1163 1164 ff = vtki.new("ImageFlip") 1165 ff.SetInputData(img) 1166 if axis.lower() == "x": 1167 ff.SetFilteredAxis(0) 1168 elif axis.lower() == "y": 1169 ff.SetFilteredAxis(1) 1170 elif axis.lower() == "z": 1171 ff.SetFilteredAxis(2) 1172 else: 1173 vedo.logger.error("mirror must be set to either x, y, z or n") 1174 raise RuntimeError() 1175 ff.Update() 1176 self._update(ff.GetOutput()) 1177 self.pipeline = utils.OperationNode(f"mirror {axis}", parents=[self], c="#4cc9f0") 1178 return self
Mirror flip along one of the cartesian axes.
1180 def operation(self, operation: str, volume2=None) -> "Volume": 1181 """ 1182 Perform operations with `Volume` objects. 1183 Keyword `volume2` can be a constant `float`. 1184 1185 Possible operations are: 1186 ``` 1187 and, or, xor, nand, nor, not, 1188 +, -, /, 1/x, sin, cos, exp, log, 1189 abs, **2, sqrt, min, max, atan, atan2, median, 1190 mag, dot, gradient, divergence, laplacian. 1191 ``` 1192 1193 Example: 1194 ```py 1195 from vedo import Box, show 1196 vol1 = Box(size=(35,10, 5)).binarize() 1197 vol2 = Box(size=( 5,10,35)).binarize() 1198 vol = vol1.operation("xor", vol2) 1199 show([[vol1, vol2], 1200 ["vol1 xor vol2", vol]], 1201 N=2, axes=1, viewup="z", 1202 ).close() 1203 ``` 1204 1205 Note: 1206 For logic operations, the two volumes must have the same bounds. 1207 If they do not, a larger image is created to contain both and the 1208 volumes are resampled onto the larger image before the operation is 1209 performed. This can be slow and memory intensive. 1210 1211 See also: 1212 - [volume_operations.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/volume_operations.py) 1213 """ 1214 op = operation.lower() 1215 image1 = self.dataset 1216 1217 if op in ["and", "or", "xor", "nand", "nor"]: 1218 1219 if not np.allclose(image1.GetBounds(), volume2.dataset.GetBounds()): 1220 # create a larger image to contain both 1221 b1 = image1.GetBounds() 1222 b2 = volume2.dataset.GetBounds() 1223 b = [ 1224 min(b1[0], b2[0]), 1225 max(b1[1], b2[1]), 1226 min(b1[2], b2[2]), 1227 max(b1[3], b2[3]), 1228 min(b1[4], b2[4]), 1229 max(b1[5], b2[5]), 1230 ] 1231 dims1 = image1.GetDimensions() 1232 dims2 = volume2.dataset.GetDimensions() 1233 dims = [max(dims1[0], dims2[0]), max(dims1[1], dims2[1]), max(dims1[2], dims2[2])] 1234 1235 image = vtki.vtkImageData() 1236 image.SetDimensions(dims) 1237 spacing = ( 1238 (b[1] - b[0]) / dims[0], 1239 (b[3] - b[2]) / dims[1], 1240 (b[5] - b[4]) / dims[2], 1241 ) 1242 image.SetSpacing(spacing) 1243 image.SetOrigin((b[0], b[2], b[4])) 1244 image.AllocateScalars(vtki.VTK_UNSIGNED_CHAR, 1) 1245 image.GetPointData().GetScalars().FillComponent(0, 0) 1246 1247 interp1 = vtki.new("ImageReslice") 1248 interp1.SetInputData(image1) 1249 interp1.SetOutputExtent(image.GetExtent()) 1250 interp1.SetOutputOrigin(image.GetOrigin()) 1251 interp1.SetOutputSpacing(image.GetSpacing()) 1252 interp1.SetInterpolationModeToNearestNeighbor() 1253 interp1.Update() 1254 imageA = interp1.GetOutput() 1255 1256 interp2 = vtki.new("ImageReslice") 1257 interp2.SetInputData(volume2.dataset) 1258 interp2.SetOutputExtent(image.GetExtent()) 1259 interp2.SetOutputOrigin(image.GetOrigin()) 1260 interp2.SetOutputSpacing(image.GetSpacing()) 1261 interp2.SetInterpolationModeToNearestNeighbor() 1262 interp2.Update() 1263 imageB = interp2.GetOutput() 1264 1265 else: 1266 imageA = image1 1267 imageB = volume2.dataset 1268 1269 img_logic = vtki.new("ImageLogic") 1270 img_logic.SetInput1Data(imageA) 1271 img_logic.SetInput2Data(imageB) 1272 img_logic.SetOperation(["and", "or", "xor", "nand", "nor"].index(op)) 1273 img_logic.Update() 1274 1275 out_vol = Volume(img_logic.GetOutput()) 1276 out_vol.pipeline = utils.OperationNode( 1277 "operation", comment=f"{op}", parents=[self, volume2], c="#4cc9f0", shape="cylinder" 1278 ) 1279 return out_vol ###################################################### 1280 1281 if volume2 and isinstance(volume2, Volume): 1282 # assert image1.GetScalarType() == volume2.dataset.GetScalarType(), "volumes have different scalar types" 1283 # make sure they have the same bounds: 1284 assert np.allclose(image1.GetBounds(), volume2.dataset.GetBounds()), "volumes have different bounds" 1285 # make sure they have the same spacing: 1286 assert np.allclose(image1.GetSpacing(), volume2.dataset.GetSpacing()), "volumes have different spacing" 1287 # make sure they have the same origin: 1288 assert np.allclose(image1.GetOrigin(), volume2.dataset.GetOrigin()), "volumes have different origin" 1289 1290 mf = None 1291 if op in ["median"]: 1292 mf = vtki.new("ImageMedian3D") 1293 mf.SetInputData(image1) 1294 elif op in ["mag"]: 1295 mf = vtki.new("ImageMagnitude") 1296 mf.SetInputData(image1) 1297 elif op in ["dot"]: 1298 mf = vtki.new("ImageDotProduct") 1299 mf.SetInput1Data(image1) 1300 mf.SetInput2Data(volume2.dataset) 1301 elif op in ["grad", "gradient"]: 1302 mf = vtki.new("ImageGradient") 1303 mf.SetDimensionality(3) 1304 mf.SetInputData(image1) 1305 elif op in ["div", "divergence"]: 1306 mf = vtki.new("ImageDivergence") 1307 mf.SetInputData(image1) 1308 elif op in ["laplacian"]: 1309 mf = vtki.new("ImageLaplacian") 1310 mf.SetDimensionality(3) 1311 mf.SetInputData(image1) 1312 elif op in ["not"]: 1313 mf = vtki.new("ImageLogic") 1314 mf.SetInput1Data(image1) 1315 mf.SetOperation(4) 1316 1317 if mf is not None: 1318 mf.Update() 1319 vol = Volume(mf.GetOutput()) 1320 vol.pipeline = utils.OperationNode( 1321 "operation", comment=f"{op}", parents=[self], c="#4cc9f0", shape="cylinder" 1322 ) 1323 return vol ###################################################### 1324 1325 mat = vtki.new("ImageMathematics") 1326 mat.SetInput1Data(image1) 1327 1328 K = None 1329 1330 if utils.is_number(volume2): 1331 K = volume2 1332 mat.SetConstantK(K) 1333 mat.SetConstantC(K) 1334 1335 elif volume2 is not None: # assume image2 is a constant value 1336 mat.SetInput2Data(volume2.dataset) 1337 1338 # ########################### 1339 if op in ["+", "add", "plus"]: 1340 if K: 1341 mat.SetOperationToAddConstant() 1342 else: 1343 mat.SetOperationToAdd() 1344 1345 elif op in ["-", "subtract", "minus"]: 1346 if K: 1347 mat.SetConstantC(-float(K)) 1348 mat.SetOperationToAddConstant() 1349 else: 1350 mat.SetOperationToSubtract() 1351 1352 elif op in ["*", "multiply", "times"]: 1353 if K: 1354 mat.SetOperationToMultiplyByK() 1355 else: 1356 mat.SetOperationToMultiply() 1357 1358 elif op in ["/", "divide"]: 1359 if K: 1360 mat.SetConstantK(1.0 / K) 1361 mat.SetOperationToMultiplyByK() 1362 else: 1363 mat.SetOperationToDivide() 1364 1365 elif op in ["1/x", "invert"]: 1366 mat.SetOperationToInvert() 1367 elif op in ["sin"]: 1368 mat.SetOperationToSin() 1369 elif op in ["cos"]: 1370 mat.SetOperationToCos() 1371 elif op in ["exp"]: 1372 mat.SetOperationToExp() 1373 elif op in ["log"]: 1374 mat.SetOperationToLog() 1375 elif op in ["abs"]: 1376 mat.SetOperationToAbsoluteValue() 1377 elif op in ["**2", "square"]: 1378 mat.SetOperationToSquare() 1379 elif op in ["sqrt", "sqr"]: 1380 mat.SetOperationToSquareRoot() 1381 elif op in ["min"]: 1382 mat.SetOperationToMin() 1383 elif op in ["max"]: 1384 mat.SetOperationToMax() 1385 elif op in ["atan"]: 1386 mat.SetOperationToATAN() 1387 elif op in ["atan2"]: 1388 mat.SetOperationToATAN2() 1389 else: 1390 vedo.logger.error(f"unknown operation {operation}") 1391 raise RuntimeError() 1392 mat.Update() 1393 1394 self._update(mat.GetOutput()) 1395 1396 self.pipeline = utils.OperationNode( 1397 "operation", comment=f"{op}", parents=[self, volume2], shape="cylinder", c="#4cc9f0" 1398 ) 1399 return self
Perform operations with Volume
objects.
Keyword volume2
can be a constant float
.
Possible operations are:
and, or, xor, nand, nor, not,
+, -, /, 1/x, sin, cos, exp, log,
abs, **2, sqrt, min, max, atan, atan2, median,
mag, dot, gradient, divergence, laplacian.
Example:
from vedo import Box, show
vol1 = Box(size=(35,10, 5)).binarize()
vol2 = Box(size=( 5,10,35)).binarize()
vol = vol1.operation("xor", vol2)
show([[vol1, vol2],
["vol1 xor vol2", vol]],
N=2, axes=1, viewup="z",
).close()
Note:
For logic operations, the two volumes must have the same bounds. If they do not, a larger image is created to contain both and the volumes are resampled onto the larger image before the operation is performed. This can be slow and memory intensive.
See also:
1401 def frequency_pass_filter(self, low_cutoff=None, high_cutoff=None, order=1) -> Self: 1402 """ 1403 Low-pass and high-pass filtering become trivial in the frequency domain. 1404 A portion of the pixels/voxels are simply masked or attenuated. 1405 This function applies a high pass Butterworth filter that attenuates the 1406 frequency domain image. 1407 1408 The gradual attenuation of the filter is important. 1409 A simple high-pass filter would simply mask a set of pixels in the frequency domain, 1410 but the abrupt transition would cause a ringing effect in the spatial domain. 1411 1412 Arguments: 1413 low_cutoff : (list) 1414 the cutoff frequencies for x, y and z 1415 high_cutoff : (list) 1416 the cutoff frequencies for x, y and z 1417 order : (int) 1418 order determines sharpness of the cutoff curve 1419 """ 1420 # https://lorensen.github.io/VTKExamples/site/Cxx/ImageProcessing/IdealHighPass 1421 fft = vtki.new("ImageFFT") 1422 fft.SetInputData(self.dataset) 1423 fft.Update() 1424 out = fft.GetOutput() 1425 1426 if high_cutoff: 1427 blp = vtki.new("ImageButterworthLowPass") 1428 blp.SetInputData(out) 1429 blp.SetCutOff(high_cutoff) 1430 blp.SetOrder(order) 1431 blp.Update() 1432 out = blp.GetOutput() 1433 1434 if low_cutoff: 1435 bhp = vtki.new("ImageButterworthHighPass") 1436 bhp.SetInputData(out) 1437 bhp.SetCutOff(low_cutoff) 1438 bhp.SetOrder(order) 1439 bhp.Update() 1440 out = bhp.GetOutput() 1441 1442 rfft = vtki.new("ImageRFFT") 1443 rfft.SetInputData(out) 1444 rfft.Update() 1445 1446 ecomp = vtki.new("ImageExtractComponents") 1447 ecomp.SetInputData(rfft.GetOutput()) 1448 ecomp.SetComponents(0) 1449 ecomp.Update() 1450 self._update(ecomp.GetOutput()) 1451 self.pipeline = utils.OperationNode("frequency_pass_filter", parents=[self], c="#4cc9f0") 1452 return self
Low-pass and high-pass filtering become trivial in the frequency domain. A portion of the pixels/voxels are simply masked or attenuated. This function applies a high pass Butterworth filter that attenuates the frequency domain image.
The gradual attenuation of the filter is important. A simple high-pass filter would simply mask a set of pixels in the frequency domain, but the abrupt transition would cause a ringing effect in the spatial domain.
Arguments:
- low_cutoff : (list) the cutoff frequencies for x, y and z
- high_cutoff : (list) the cutoff frequencies for x, y and z
- order : (int) order determines sharpness of the cutoff curve
1454 def smooth_gaussian(self, sigma=(2, 2, 2), radius=None) -> Self: 1455 """ 1456 Performs a convolution of the input Volume with a gaussian. 1457 1458 Arguments: 1459 sigma : (float, list) 1460 standard deviation(s) in voxel units. 1461 A list can be given to smooth in the three direction differently. 1462 radius : (float, list) 1463 radius factor(s) determine how far out the gaussian 1464 kernel will go before being clamped to zero. A list can be given too. 1465 """ 1466 gsf = vtki.new("ImageGaussianSmooth") 1467 gsf.SetDimensionality(3) 1468 gsf.SetInputData(self.dataset) 1469 if utils.is_sequence(sigma): 1470 gsf.SetStandardDeviations(sigma) 1471 else: 1472 gsf.SetStandardDeviation(sigma) 1473 if radius is not None: 1474 if utils.is_sequence(radius): 1475 gsf.SetRadiusFactors(radius) 1476 else: 1477 gsf.SetRadiusFactor(radius) 1478 gsf.Update() 1479 self._update(gsf.GetOutput()) 1480 self.pipeline = utils.OperationNode("smooth_gaussian", parents=[self], c="#4cc9f0") 1481 return self
Performs a convolution of the input Volume with a gaussian.
Arguments:
- sigma : (float, list) standard deviation(s) in voxel units. A list can be given to smooth in the three direction differently.
- radius : (float, list) radius factor(s) determine how far out the gaussian kernel will go before being clamped to zero. A list can be given too.
1483 def smooth_median(self, neighbours=(2, 2, 2)) -> Self: 1484 """ 1485 Median filter that replaces each pixel with the median value 1486 from a rectangular neighborhood around that pixel. 1487 """ 1488 imgm = vtki.new("ImageMedian3D") 1489 imgm.SetInputData(self.dataset) 1490 if utils.is_sequence(neighbours): 1491 imgm.SetKernelSize(neighbours[0], neighbours[1], neighbours[2]) 1492 else: 1493 imgm.SetKernelSize(neighbours, neighbours, neighbours) 1494 imgm.Update() 1495 self._update(imgm.GetOutput()) 1496 self.pipeline = utils.OperationNode("smooth_median", parents=[self], c="#4cc9f0") 1497 return self
Median filter that replaces each pixel with the median value from a rectangular neighborhood around that pixel.
1499 def erode(self, neighbours=(2, 2, 2)) -> Self: 1500 """ 1501 Replace a voxel with the minimum over an ellipsoidal neighborhood of voxels. 1502 If `neighbours` of an axis is 1, no processing is done on that axis. 1503 1504 Examples: 1505 - [erode_dilate.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/erode_dilate.py) 1506 1507 ![](https://vedo.embl.es/images/volumetric/erode_dilate.png) 1508 """ 1509 ver = vtki.new("ImageContinuousErode3D") 1510 ver.SetInputData(self.dataset) 1511 ver.SetKernelSize(neighbours[0], neighbours[1], neighbours[2]) 1512 ver.Update() 1513 self._update(ver.GetOutput()) 1514 self.pipeline = utils.OperationNode("erode", parents=[self], c="#4cc9f0") 1515 return self
Replace a voxel with the minimum over an ellipsoidal neighborhood of voxels.
If neighbours
of an axis is 1, no processing is done on that axis.
Examples:
1517 def dilate(self, neighbours=(2, 2, 2)) -> Self: 1518 """ 1519 Replace a voxel with the maximum over an ellipsoidal neighborhood of voxels. 1520 If `neighbours` of an axis is 1, no processing is done on that axis. 1521 1522 Check also `erode()` and `pad()`. 1523 1524 Examples: 1525 - [erode_dilate.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/erode_dilate.py) 1526 """ 1527 ver = vtki.new("ImageContinuousDilate3D") 1528 ver.SetInputData(self.dataset) 1529 ver.SetKernelSize(neighbours[0], neighbours[1], neighbours[2]) 1530 ver.Update() 1531 self._update(ver.GetOutput()) 1532 self.pipeline = utils.OperationNode("dilate", parents=[self], c="#4cc9f0") 1533 return self
Replace a voxel with the maximum over an ellipsoidal neighborhood of voxels.
If neighbours
of an axis is 1, no processing is done on that axis.
Examples:
1535 def magnitude(self) -> Self: 1536 """Colapses components with magnitude function.""" 1537 imgm = vtki.new("ImageMagnitude") 1538 imgm.SetInputData(self.dataset) 1539 imgm.Update() 1540 self._update(imgm.GetOutput()) 1541 self.pipeline = utils.OperationNode("magnitude", parents=[self], c="#4cc9f0") 1542 return self
Colapses components with magnitude function.
1544 def topoints(self) -> "vedo.Points": 1545 """ 1546 Extract all image voxels as points. 1547 This function takes an input `Volume` and creates an `Mesh` 1548 that contains the points and the point attributes. 1549 1550 Examples: 1551 - [vol2points.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/vol2points.py) 1552 """ 1553 v2p = vtki.new("ImageToPoints") 1554 v2p.SetInputData(self.dataset) 1555 v2p.Update() 1556 mpts = vedo.Points(v2p.GetOutput()) 1557 mpts.pipeline = utils.OperationNode("topoints", parents=[self], c="#4cc9f0:#e9c46a") 1558 return mpts
Extract all image voxels as points.
This function takes an input Volume
and creates an Mesh
that contains the points and the point attributes.
Examples:
1560 def euclidean_distance(self, anisotropy=False, max_distance=None) -> "Volume": 1561 """ 1562 Implementation of the Euclidean DT (Distance Transform) using Saito's algorithm. 1563 The distance map produced contains the square of the Euclidean distance values. 1564 The algorithm has a O(n^(D+1)) complexity over n x n x...x n images in D dimensions. 1565 1566 Check out also: https://en.wikipedia.org/wiki/Distance_transform 1567 1568 Arguments: 1569 anisotropy : bool 1570 used to define whether Spacing should be used in the 1571 computation of the distances. 1572 max_distance : bool 1573 any distance bigger than max_distance will not be 1574 computed but set to this specified value instead. 1575 1576 Examples: 1577 - [euclidian_dist.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/euclidian_dist.py) 1578 """ 1579 euv = vtki.new("ImageEuclideanDistance") 1580 euv.SetInputData(self.dataset) 1581 euv.SetConsiderAnisotropy(anisotropy) 1582 if max_distance is not None: 1583 euv.InitializeOn() 1584 euv.SetMaximumDistance(max_distance) 1585 euv.SetAlgorithmToSaito() 1586 euv.Update() 1587 vol = Volume(euv.GetOutput()) 1588 vol.pipeline = utils.OperationNode("euclidean_distance", parents=[self], c="#4cc9f0") 1589 return vol
Implementation of the Euclidean DT (Distance Transform) using Saito's algorithm. The distance map produced contains the square of the Euclidean distance values. The algorithm has a O(n^(D+1)) complexity over n x n x...x n images in D dimensions.
Check out also: https://en.wikipedia.org/wiki/Distance_transform
Arguments:
- anisotropy : bool used to define whether Spacing should be used in the computation of the distances.
- max_distance : bool any distance bigger than max_distance will not be computed but set to this specified value instead.
Examples:
1591 def correlation_with(self, vol2: "Volume", dim=2) -> "Volume": 1592 """ 1593 Find the correlation between two volumetric data sets. 1594 Keyword `dim` determines whether the correlation will be 3D, 2D or 1D. 1595 The default is a 2D Correlation. 1596 1597 The output size will match the size of the first input. 1598 The second input is considered the correlation kernel. 1599 """ 1600 imc = vtki.new("ImageCorrelation") 1601 imc.SetInput1Data(self.dataset) 1602 imc.SetInput2Data(vol2.dataset) 1603 imc.SetDimensionality(dim) 1604 imc.Update() 1605 vol = Volume(imc.GetOutput()) 1606 1607 vol.pipeline = utils.OperationNode("correlation_with", parents=[self, vol2], c="#4cc9f0") 1608 return vol
Find the correlation between two volumetric data sets.
Keyword dim
determines whether the correlation will be 3D, 2D or 1D.
The default is a 2D Correlation.
The output size will match the size of the first input. The second input is considered the correlation kernel.
1610 def scale_voxels(self, scale=1) -> Self: 1611 """Scale the voxel content by factor `scale`.""" 1612 rsl = vtki.new("ImageReslice") 1613 rsl.SetInputData(self.dataset) 1614 rsl.SetScalarScale(scale) 1615 rsl.Update() 1616 self._update(rsl.GetOutput()) 1617 self.pipeline = utils.OperationNode( 1618 "scale_voxels", comment=f"scale={scale}", parents=[self], c="#4cc9f0" 1619 ) 1620 return self
Scale the voxel content by factor scale
.
Inherited Members
- vedo.core.CommonAlgorithms
- pointdata
- celldata
- metadata
- memory_address
- memory_size
- box
- update_dataset
- xbounds
- ybounds
- zbounds
- diagonal_size
- average_size
- center_of_mass
- copy_data_from
- inputdata
- npoints
- nvertices
- ncells
- points
- cell_centers
- lines
- lines_as_flat_array
- mark_boundaries
- find_cells_in_bounds
- find_cells_along_line
- find_cells_along_plane
- keep_cell_types
- map_cells_to_points
- vertices
- coordinates
- cells_as_flat_array
- cells
- map_points_to_cells
- resample_data_from
- interpolate_data_from
- add_ids
- gradient
- divergence
- vorticity
- probe
- compute_cell_size
- generate_random_data
- integrate_data
- write
- signed_distance
- unsigned_distance
- smooth_data
- compute_streamlines