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